commit dfa85ede7e21af0595659fcdc1e0e54133820854 Author: Md Asif Date: Wed Sep 18 10:17:07 2024 +0530 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c426c32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..ba5b4c8 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,37 @@ +val kotlin_version: String by project +val logback_version: String by project + +plugins { + kotlin("jvm") version "2.0.20" + id("io.ktor.plugin") version "3.0.0-rc-1" + id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20" +} + +group = "net.ipksindia" +version = "0.0.1" + +application { + mainClass.set("net.ipksindia.ApplicationKt") + + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("io.ktor:ktor-server-core-jvm") + implementation("io.ktor:ktor-server-content-negotiation-jvm") + implementation("io.ktor:ktor-serialization-kotlinx-json-jvm") + implementation("io.ktor:ktor-server-netty-jvm") + implementation("ch.qos.logback:logback-classic:$logback_version") + implementation("com.oracle.database.jdbc:ojdbc8:21.1.0.0") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") + implementation("redis.clients:jedis:5.1.2") + implementation("org.slf4j:slf4j-api:2.0.16") + testImplementation("io.ktor:ktor-server-test-host-jvm") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..5275062 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +kotlin.code.style=official +ktor_version=3.0.0-rc-1 +kotlin_version=2.0.20 +logback_version=1.4.14 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e411586 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..712011b --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "neft-server" diff --git a/src/main/kotlin/net/ipksindia/Application.kt b/src/main/kotlin/net/ipksindia/Application.kt new file mode 100644 index 0000000..a5c26c3 --- /dev/null +++ b/src/main/kotlin/net/ipksindia/Application.kt @@ -0,0 +1,16 @@ +package net.ipksindia + +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import net.ipksindia.plugins.* + +fun main() { + embeddedServer(Netty, port = 8088, host = "0.0.0.0", module = Application::module) + .start(wait = true) +} + +fun Application.module() { + configureSerialization() + configureRouting() +} diff --git a/src/main/kotlin/net/ipksindia/NeftRequestProcessor.kt b/src/main/kotlin/net/ipksindia/NeftRequestProcessor.kt new file mode 100644 index 0000000..0ce5a08 --- /dev/null +++ b/src/main/kotlin/net/ipksindia/NeftRequestProcessor.kt @@ -0,0 +1,80 @@ +package net.ipksindia + +import TransactionExecutor +import TransactionFactory +import dao.TellerDao +import net.ipksindia.dao.TransactionDao +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import redis.clients.jedis.JedisPooled +import response.TransactionSuccessResponse + +class NeftRequestProcessor { + + companion object { + private val logger: Logger = LoggerFactory.getLogger(NeftRequestProcessor::class.java) + private const val PROCESSED_TRANSACTION_LIST = "ipks:processed_outward_transactions" + private val migratedDCCBCodes = listOf("0012") + + fun process(transactionNumber: String): Pair? { + + val outwardTransaction = TransactionDao().getTransactionRequest(transactionNumber) ?: run { + logger.error("TXN: #{} FAILED REASON: Transaction not found", transactionNumber) + return null + } + logger.info("TXN: #{} FOUND", transactionNumber) + val jedis = JedisPooled("localhost", 6379) + val processedTransactionList = jedis.smembers(PROCESSED_TRANSACTION_LIST) + val dccbCode = outwardTransaction.dccbCode.padStart(4, '0') + if(dccbCode !in migratedDCCBCodes) { + logger.error("TXN: #{} FAILED REASON: DCCB Code not migrated", transactionNumber) + return null + } + val branchCode = outwardTransaction.branchCode.padStart(3, '0') + if (transactionNumber in processedTransactionList) { + logger.error("TXN: #{} FAILED REASON: Transaction already processed", transactionNumber) + return null + } + val makerTeller = TellerDao.getTeller(dccbCode, branchCode) ?: run { + logger.error("TXN: #{} FAILED REASON: Teller not found", transactionNumber) + return null + } + val transactionPair = TransactionFactory(outwardTransaction, makerTeller).createTransactionPair() + val (transferResponse, neftResponse) = try { + TransactionExecutor().executePair(transactionPair) + } catch (e: Exception) { + logger.error("TXN: #{} FAILED REASON: {}", transactionNumber, e.message) + return null + } + + logger.info( + "TXN: #{} TRF_TXN: {} NEFT_TXN: {}", + transactionNumber, + transferResponse.status, + neftResponse.status + ) + + jedis.sadd(PROCESSED_TRANSACTION_LIST, outwardTransaction.transactionNumber) + + if (transferResponse.status == "SUCCESS" && neftResponse.status == "SUCCESS") { + val transferQueueNumber = (transferResponse as TransactionSuccessResponse).response.queueId + val neftQueueNumber = (neftResponse as TransactionSuccessResponse).response.queueId + try { + TransactionDao().updateSuccessTransaction(outwardTransaction, transferQueueNumber, neftQueueNumber) + logger.info("TXN: #{} UPDATED RESULTS SUCCESSFULLY", transactionNumber) + } catch (e: Exception) { + logger.error( + "TXN: #{} QUEUE INITIATED BUT FAILED TO UPDATE RESULT: {}", + transactionNumber, + e.message + ) + } + + return Pair(transferQueueNumber, neftQueueNumber) + } else { + logger.error("TXN: #{} QUEUE INITIATED BUT FAILED TO UPDATE RESULT", transactionNumber) + return null + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/ipksindia/TellerNotFoundException.kt b/src/main/kotlin/net/ipksindia/TellerNotFoundException.kt new file mode 100644 index 0000000..bbad8c0 --- /dev/null +++ b/src/main/kotlin/net/ipksindia/TellerNotFoundException.kt @@ -0,0 +1 @@ +class TellerNotFoundException(s: String) : Exception(s) \ No newline at end of file diff --git a/src/main/kotlin/net/ipksindia/TransactionExecutor.kt b/src/main/kotlin/net/ipksindia/TransactionExecutor.kt new file mode 100644 index 0000000..9132050 --- /dev/null +++ b/src/main/kotlin/net/ipksindia/TransactionExecutor.kt @@ -0,0 +1,60 @@ + +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import model.NeftTransaction +import model.TransferTransaction +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import response.TransactionFailureResponse +import response.TransactionResponse +import response.TransactionSuccessResponse +import java.io.IOException + + +class TransactionExecutor() { + + private val protocol = "http" + private val host = "localhost" + private val port = "8080" + private val rootRoute = "WESTBANGAL/api" + private val remoteUrl = "$protocol://$host:$port/$rootRoute" + + fun executePair(transactionPair: Pair): Pair { + + val transferTransaction = transactionPair.first + val neftTransaction = transactionPair.second + val transferResponse = execute(Json.encodeToString(transferTransaction)) + val neftResponse = execute(Json.encodeToString(neftTransaction)) + return Pair(transferResponse, neftResponse) + } + + private fun execute(postBody: String): TransactionResponse { + val transferRoute = "IPKSNeftRtgsApiTransfer" + val transferURL = "$remoteUrl/$transferRoute" + val jsonMediaType = "application/json; charset=utf-8".toMediaType() + + val httpClient = OkHttpClient() + + val request = Request.Builder() + .url(transferURL) + .post(postBody.toRequestBody(jsonMediaType)) + .build() + + val responseBody = httpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw IOException("Unexpected code $response") + } + response.body!!.string() + } + + val response = if(responseBody.contains("SUCCESS")) { + Json.decodeFromString(responseBody) + } else { + Json.decodeFromString(responseBody) + } + return response + } + +} \ No newline at end of file diff --git a/src/main/kotlin/net/ipksindia/TransactionFactory.kt b/src/main/kotlin/net/ipksindia/TransactionFactory.kt new file mode 100644 index 0000000..9aa343a --- /dev/null +++ b/src/main/kotlin/net/ipksindia/TransactionFactory.kt @@ -0,0 +1,67 @@ + +import enums.TransactionType +import model.NeftTransaction +import model.Teller +import model.TransactionRequest +import model.TransferTransaction +import java.time.format.DateTimeFormatter + +class TransactionFactory(private val transactionRequest: TransactionRequest, private val teller: Teller) { + + private val date = transactionRequest.date.format(DateTimeFormatter.ofPattern("dd-MM-yyyy")) + private val rrn = transactionRequest.date.format(DateTimeFormatter.ofPattern("ddMM")) + transactionRequest.transactionNumber.takeLast(4) + + private fun createTransferTransaction(): TransferTransaction { + return TransferTransaction( + bankCode = transactionRequest.dccbCode.padStart(4, '0'), + branchCode = transactionRequest.branchCode.padStart(3,'0'), + cbsTellerId = teller.tellerId, + cbsTellerUserIdType = teller.userType, + queIdType = "5", + description = "${TransactionType.TRANSFER.code}For Checking", + priority = "1", + cbsTellerCapability = teller.capability, + txnScreenNo = TransactionType.TRANSFER.code.padStart(6, '0'), + txnAmt = transactionRequest.amount, + txnDate = date, + sourceAcctNo = transactionRequest.pacsCurrentAccountNumber, + destinationAcctNo = transactionRequest.linkedCBSAccountNumber, + narration = "TRF to member A/C for NEFT RTGS", + sourceTxnNo = "1045", + sourceStat = "A/P", + apiType = "OUTWARD_QUEUE_POSTING", + remitterName = transactionRequest.remitterName, + rrn = rrn + "1" + ) + + } + + private fun createNEFTTransaction(): NeftTransaction { + return NeftTransaction( + bankCode = transactionRequest.dccbCode, + branchCode = transactionRequest.branchCode.padStart(3,'0'), + cbsTellerId = teller.tellerId, + cbsTellerUserIdType = teller.userType, + queIdType = "5", + description = "${TransactionType.NEFT.code}For Checking", + priority = "1", + cbsTellerCapability = teller.capability, + txnScreenNo = TransactionType.NEFT.code.padStart(6, '0'), + txnAmt = transactionRequest.amount, + txnDate = date, + sourceAcctNo = transactionRequest.linkedCBSAccountNumber, + destinationAcctNo = transactionRequest.neftBeneficiaryAccountNumber, + narration = "TRF to member A/C for NEFT RTGS", + sourceTxnNo = TransactionType.NEFT.code, + sourceStat = "A/P", + apiType = "OUTWARD_QUEUE_POSTING", + remitterName = transactionRequest.remitterName, + ifscCode = transactionRequest.ifscCode, + rrn = rrn + "2" + ) + } + + fun createTransactionPair(): Pair { + return Pair(createTransferTransaction(), createNEFTTransaction()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/ipksindia/dao/TellerDao.kt b/src/main/kotlin/net/ipksindia/dao/TellerDao.kt new file mode 100644 index 0000000..02782ff --- /dev/null +++ b/src/main/kotlin/net/ipksindia/dao/TellerDao.kt @@ -0,0 +1,46 @@ +package dao + +import model.Teller + +class TellerDao { + companion object { + private val tellerMap = mapOf( + "0016" to mapOf( + "008" to "118" + ), + "0012" to mapOf( + "008" to "8", + "022" to "22", + "012" to "12", + "014" to "14", + "003" to "1003", + "015" to "15", + "013" to "13", + "018" to "18", + "001" to "1001", + "004" to "4", + "017" to "1234", + "005" to "5", + "011" to "11", + "020" to "1234", + "021" to "1234", + "016" to "016", + "009" to "9", + "010" to "10", + "007" to "7", + "006" to "6" + ) + + ) + fun getTeller(dccbCode: String, branchCode: String): Teller? { + val branchList = tellerMap[dccbCode] ?: return null + val tellerId = branchList[branchCode] ?: return null + val teller = Teller( + tellerId, + dccbCode, + branchCode + ) + return teller + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/ipksindia/dao/TransactionDao.kt b/src/main/kotlin/net/ipksindia/dao/TransactionDao.kt new file mode 100644 index 0000000..e62abb4 --- /dev/null +++ b/src/main/kotlin/net/ipksindia/dao/TransactionDao.kt @@ -0,0 +1,216 @@ +package net.ipksindia.dao + +import model.TransactionRequest +import java.sql.Date +import java.sql.DriverManager +import java.sql.ResultSet +import java.util.* + +class TransactionDao { + private val transactionRequestQuery = """ + SELECT + txn_no, + TRIM(src_ac_no) AS src_ac_no, + TRIM(dest_ac_no) AS dest_ac_no, + ifsc_code, + txn_amt, + txn_date, + t.teller_id, + CASE + WHEN t.ifsc_code LIKE 'WBSC%' THEN 'FAILED' + ELSE 'RECEIVED' + END AS status, + SUBSTR(REGEXP_REPLACE(REGEXP_REPLACE(UPPER(beneficiary_name), '[^A-Z0-9 ]', ''), ' {2,}', ' '), 1, 35) AS beneficiary_name, + beneficiary_add, + t.pacs_id, + comm_txn_no, + comm_txn_amt, + dccb_code, + TO_NUMBER(cbs_br_code) AS br_code, + SUBSTR(REGEXP_REPLACE(REGEXP_REPLACE(UPPER(remm_name), '[^A-Z0-9 ]', ''), ' {2,}', ' '),1,35) AS remitter_name, + ipks_accno AS pacs_acc_no, + da.link_accno AS cbs_sb_acc_no, + 'pacs_db' AS db_name + FROM neft_rtgs_txn t + JOIN dep_account da ON t.ipks_accno = da.key_1 + WHERE + t.txn_date = (SELECT system_date FROM system_date) + AND t.STATUS = 'A' + AND t.bank_channel = 'SCB' + AND da.link_accno IS NOT NULL + """.trimIndent() + + private val singleTransactionRequestQuery = """ + SELECT + txn_no, + TRIM(src_ac_no) AS src_ac_no, + TRIM(dest_ac_no) AS dest_ac_no, + ifsc_code, + txn_amt, + txn_date, + t.teller_id, + CASE + WHEN t.ifsc_code LIKE 'WBSC%' THEN 'FAILED' + ELSE 'RECEIVED' + END AS status, + SUBSTR(REGEXP_REPLACE(REGEXP_REPLACE(UPPER(beneficiary_name), '[^A-Z0-9 ]', ''), ' {2,}', ' '), 1, 35) AS beneficiary_name, + beneficiary_add, + t.pacs_id, + comm_txn_no, + comm_txn_amt, + dccb_code, + TO_NUMBER(cbs_br_code) AS br_code, + SUBSTR(REGEXP_REPLACE(REGEXP_REPLACE(UPPER(remm_name), '[^A-Z0-9 ]', ''), ' {2,}', ' '),1,35) AS remitter_name, + ipks_accno AS pacs_acc_no, + da.link_accno AS cbs_sb_acc_no, + 'pacs_db' AS db_name + FROM neft_rtgs_txn t + JOIN dep_account da ON t.ipks_accno = da.key_1 + WHERE + t.txn_no = ? + """.trimIndent() + + private val transactionUpdateQuery = """ + INSERT INTO neft_rtgs_txn_queue ( + txn_no, + src_ac_no, + dest_ac_no, + ifsc_code, + txn_amt, + txn_date, + teller_id, + status, + beneficiary_name, + beneficiary_add, + pacs_id, + comm_txn_no, + comm_txn_amt, + dccb_code, + br_code, + remitter_name, + cbs_queue_no, + pacs_acc_no, + cbs_queue_no2 + ) VALUES ( + ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,? + ) + """.trimIndent() + + fun updateSuccessTransaction(request: TransactionRequest, transferQueueNumber: String, neftQueueNumber: String) { + val dbUrl = getDatabaseUrl() + val (dbUser, dbPassword) = getUserCredentials() + + DriverManager.getConnection(dbUrl, dbUser, dbPassword).use { connection -> + connection.prepareStatement(transactionUpdateQuery).also { + it.setString(1, request.transactionNumber) + it.setString(2, request.pacsCurrentAccountNumber) + it.setString(3, request.neftBeneficiaryAccountNumber) + it.setString(4, request.ifscCode) + it.setString(5, request.amount) + it.setDate(6, Date.valueOf(request.date)) + it.setString(7, request.tellerId) + it.setString(8, "PROCESSED") + it.setString(9, request.beneficiaryName) + it.setString(10, request.beneficiaryAddress) + it.setString(11, request.pacsId) + it.setString(12, request.commissionTransactionNumber) + it.setString(13, request.commissionAmount) + it.setString(14, request.dccbCode) + it.setString(15, request.branchCode) + it.setString(16, request.remitterName) + it.setString(17, transferQueueNumber) + it.setString(18, request.pacsAccountNumber) + it.setString(19, neftQueueNumber) + }.use { + it.executeUpdate() + } + + } + } + fun getTransactionRequests(): List { + + val transactionList: List + val dbUrl = getDatabaseUrl() + val (dbUser, dbPassword) = getUserCredentials() + + DriverManager.getConnection(dbUrl, dbUser, dbPassword).use { connection -> + connection.prepareStatement(transactionRequestQuery).executeQuery().use { + transactionList = mapToObject(it) + } + } + + return transactionList + } + fun getTransactionRequest(transactionNumber: String): TransactionRequest? { + val dbUrl = getDatabaseUrl() + val (dbUser, dbPassword) = getUserCredentials() + + return DriverManager.getConnection(dbUrl, dbUser, dbPassword).use { connection -> + connection.prepareStatement(singleTransactionRequestQuery).apply { + setString(1, transactionNumber) + }.executeQuery().use { + mapToObject(it).firstOrNull() + } + } + } + private fun getDatabaseUrl(): String { + val prop = loadProp() + + val dbHost = getProp(prop, "DB_HOST") + val dbPort = getProp(prop, "DB_PORT") + val dbName = getProp(prop, "DB_NAME") + + return "jdbc:oracle:thin:@$dbHost:$dbPort:$dbName" + } + + private fun getUserCredentials(): Pair { + val prop = loadProp() + + val dbUser = getProp(prop, "DB_USER") + val dbPassword = getProp(prop, "DB_PASSWORD") + + return Pair(dbUser, dbPassword) + } + + private fun loadProp(): Properties { + + val props = javaClass.classLoader.getResourceAsStream("application.properties").use { + Properties().apply { load(it) } + } + return props + } + + private fun getProp(prop: Properties, key: String): String { + return prop.getProperty(key) ?: throw RuntimeException("property $prop not found") + } + + private fun mapToObject(rs: ResultSet): List { + val list = mutableListOf() + while (rs.next()) { + val transactionRequest = TransactionRequest( + transactionNumber = rs.getString("txn_no"), + pacsCurrentAccountNumber = rs.getString("src_ac_no"), + neftBeneficiaryAccountNumber = rs.getString("dest_ac_no"), + ifscCode = rs.getString("ifsc_code"), + amount = rs.getString("txn_amt"), + date = rs.getDate("txn_date").toLocalDate(), + tellerId = rs.getString("teller_id"), + status = rs.getString("status"), + beneficiaryName = rs.getString("beneficiary_name"), + beneficiaryAddress = rs.getString("beneficiary_add"), + pacsId = rs.getString("pacs_id"), + commissionTransactionNumber = rs.getString("comm_txn_no"), + commissionAmount = rs.getString("comm_txn_amt"), + dccbCode = rs.getString("dccb_code"), + branchCode = rs.getString("br_Code"), + remitterName = rs.getString("remitter_name"), + pacsAccountNumber = rs.getString("pacs_acc_no"), + linkedCBSAccountNumber = rs.getString("cbs_sb_acc_no"), + ) + list.add(transactionRequest) + } + return list + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/net/ipksindia/enums/TransactionType.kt b/src/main/kotlin/net/ipksindia/enums/TransactionType.kt new file mode 100644 index 0000000..98adec3 --- /dev/null +++ b/src/main/kotlin/net/ipksindia/enums/TransactionType.kt @@ -0,0 +1,7 @@ +package enums + +enum class TransactionType(val code: String) { + TRANSFER("01045"), + NEFT("20066"), + RTGS("20035") +} diff --git a/src/main/kotlin/net/ipksindia/model/NeftTransaction.kt b/src/main/kotlin/net/ipksindia/model/NeftTransaction.kt new file mode 100644 index 0000000..56129ee --- /dev/null +++ b/src/main/kotlin/net/ipksindia/model/NeftTransaction.kt @@ -0,0 +1,30 @@ +package model + +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import org.ipks.model.Transaction + +@Serializable +class NeftTransaction( + override val bankCode: String, + override val branchCode: String, + override val cbsTellerId: String, + override val cbsTellerUserIdType: String, + override val queIdType: String, + override val description: String, + override val priority: String, + override val cbsTellerCapability: String, + override val txnScreenNo: String, + override val txnAmt: String, + override val txnDate: String, + override val sourceAcctNo: String, + override val destinationAcctNo: String, + override val narration: String, + override val sourceTxnNo: String, + override val sourceStat: String, + override val apiType: String, + override val remitterName: String, + val ifscCode: String, + override val rrn: String, + @Transient override var queueNo: String = "" +) : Transaction \ No newline at end of file diff --git a/src/main/kotlin/net/ipksindia/model/OutwardNeftRequest.kt b/src/main/kotlin/net/ipksindia/model/OutwardNeftRequest.kt new file mode 100644 index 0000000..3734052 --- /dev/null +++ b/src/main/kotlin/net/ipksindia/model/OutwardNeftRequest.kt @@ -0,0 +1,8 @@ +package net.ipksindia.model + +import kotlinx.serialization.Serializable + +@Serializable +data class OutwardNeftRequest( + val transactionNumber: String +) diff --git a/src/main/kotlin/net/ipksindia/model/Teller.kt b/src/main/kotlin/net/ipksindia/model/Teller.kt new file mode 100644 index 0000000..22fe5d3 --- /dev/null +++ b/src/main/kotlin/net/ipksindia/model/Teller.kt @@ -0,0 +1,10 @@ +package model + +data class Teller( + val tellerId: String, + val password: String, + val branch: String, + val dccbCode: String = "", + val userType: String = "50", + val capability: String = "9" +) \ No newline at end of file diff --git a/src/main/kotlin/net/ipksindia/model/Transaction.kt b/src/main/kotlin/net/ipksindia/model/Transaction.kt new file mode 100644 index 0000000..36b145a --- /dev/null +++ b/src/main/kotlin/net/ipksindia/model/Transaction.kt @@ -0,0 +1,24 @@ +package org.ipks.model + +interface Transaction { + val bankCode: String + val branchCode: String + val cbsTellerId: String + val cbsTellerUserIdType: String + val queIdType: String + val description: String + val priority: String + val cbsTellerCapability: String + val txnScreenNo: String + val txnAmt: String + val txnDate: String + val sourceAcctNo: String + val destinationAcctNo: String + val narration: String + val sourceTxnNo: String + val sourceStat: String + val apiType: String + val remitterName: String + val rrn: String + val queueNo: String? +} diff --git a/src/main/kotlin/net/ipksindia/model/TransactionRequest.kt b/src/main/kotlin/net/ipksindia/model/TransactionRequest.kt new file mode 100644 index 0000000..99104d0 --- /dev/null +++ b/src/main/kotlin/net/ipksindia/model/TransactionRequest.kt @@ -0,0 +1,26 @@ +package model + +import java.time.LocalDate + + +data class TransactionRequest( + val transactionNumber: String, + val pacsCurrentAccountNumber: String, + val neftBeneficiaryAccountNumber: String, + val ifscCode: String, + val amount: String, + val date: LocalDate, + val tellerId: String, + val status: String, + val beneficiaryName: String, + val beneficiaryAddress: String?, + val pacsId: String, + val commissionTransactionNumber: String?, + val commissionAmount: String?, + val dccbCode: String, + val branchCode: String, + val remitterName: String, + val pacsAccountNumber: String, + val linkedCBSAccountNumber: String +) + diff --git a/src/main/kotlin/net/ipksindia/model/TransferTransaction.kt b/src/main/kotlin/net/ipksindia/model/TransferTransaction.kt new file mode 100644 index 0000000..4b4d7d6 --- /dev/null +++ b/src/main/kotlin/net/ipksindia/model/TransferTransaction.kt @@ -0,0 +1,29 @@ +package model + +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import org.ipks.model.Transaction + +@Serializable +class TransferTransaction( + override val bankCode: String, + override val branchCode: String, + override val cbsTellerId: String, + override val cbsTellerUserIdType: String, + override val queIdType: String, + override val description: String, + override val priority: String, + override val cbsTellerCapability: String, + override val txnScreenNo: String, + override val txnAmt: String, + override val txnDate: String, + override val sourceAcctNo: String, + override val destinationAcctNo: String, + override val narration: String, + override val sourceTxnNo: String, + override val sourceStat: String, + override val apiType: String, + override val remitterName: String, + override val rrn: String, + @Transient override var queueNo: String = "" +) : Transaction \ No newline at end of file diff --git a/src/main/kotlin/net/ipksindia/plugins/Routing.kt b/src/main/kotlin/net/ipksindia/plugins/Routing.kt new file mode 100644 index 0000000..5ee7475 --- /dev/null +++ b/src/main/kotlin/net/ipksindia/plugins/Routing.kt @@ -0,0 +1,19 @@ +package net.ipksindia.plugins + +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import net.ipksindia.NeftRequestProcessor +import net.ipksindia.model.OutwardNeftRequest + +fun Application.configureRouting() { + routing { + post("/neftOutward") { + val neftRequest = call.receive() + val transactionNumber = neftRequest.transactionNumber + val response = NeftRequestProcessor.process(transactionNumber) ?: Pair("500", "Error doing outward neft" ) + call.respond("${response.first}\n${response.second}") + } + } +} diff --git a/src/main/kotlin/net/ipksindia/plugins/Serialization.kt b/src/main/kotlin/net/ipksindia/plugins/Serialization.kt new file mode 100644 index 0000000..6e3cf72 --- /dev/null +++ b/src/main/kotlin/net/ipksindia/plugins/Serialization.kt @@ -0,0 +1,18 @@ +package net.ipksindia.plugins + +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Application.configureSerialization() { + install(ContentNegotiation) { + json() + } + routing { + get("/json/kotlinx-serialization") { + call.respond(mapOf("hello" to "world")) + } + } +} diff --git a/src/main/kotlin/net/ipksindia/response/ResponseData.kt b/src/main/kotlin/net/ipksindia/response/ResponseData.kt new file mode 100644 index 0000000..2f5f5b2 --- /dev/null +++ b/src/main/kotlin/net/ipksindia/response/ResponseData.kt @@ -0,0 +1,16 @@ +package response + +import kotlinx.serialization.Serializable + + +@Serializable +data class ResponseData ( + val transactionDate: Int, + val sourceStat: String, + val journalId: Int, + val queueId: String, + val error: Int, + val apiType: String, + val errorMsg: String, + val txnScreenNo: Int +) \ No newline at end of file diff --git a/src/main/kotlin/net/ipksindia/response/TransactionFailureResponse.kt b/src/main/kotlin/net/ipksindia/response/TransactionFailureResponse.kt new file mode 100644 index 0000000..4aa9438 --- /dev/null +++ b/src/main/kotlin/net/ipksindia/response/TransactionFailureResponse.kt @@ -0,0 +1,12 @@ +package response + +import kotlinx.serialization.Serializable + + +@Serializable +data class TransactionFailureResponse( + override val status: String, + override val message: String, + val response: String, + override val error: Int +): TransactionResponse \ No newline at end of file diff --git a/src/main/kotlin/net/ipksindia/response/TransactionResponse.kt b/src/main/kotlin/net/ipksindia/response/TransactionResponse.kt new file mode 100644 index 0000000..15743d1 --- /dev/null +++ b/src/main/kotlin/net/ipksindia/response/TransactionResponse.kt @@ -0,0 +1,7 @@ +package response + +interface TransactionResponse { + val status: String + val message: String + val error: Int +} \ No newline at end of file diff --git a/src/main/kotlin/net/ipksindia/response/TransactionSuccessResponse.kt b/src/main/kotlin/net/ipksindia/response/TransactionSuccessResponse.kt new file mode 100644 index 0000000..9b951ec --- /dev/null +++ b/src/main/kotlin/net/ipksindia/response/TransactionSuccessResponse.kt @@ -0,0 +1,11 @@ +package response +import kotlinx.serialization.Serializable + + +@Serializable +data class TransactionSuccessResponse( + override val status: String, + override val message: String, + val response: ResponseData, + override val error: Int +): TransactionResponse diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..37a4bbe --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,5 @@ +DB_NAME=IPKSDB +DB_HOST=localhost +DB_PORT=1521 +DB_USER=pacs_db +DB_PASSWORD=pacs_db diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..3e11d78 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/src/test/kotlin/net/ipksindia/ApplicationTest.kt b/src/test/kotlin/net/ipksindia/ApplicationTest.kt new file mode 100644 index 0000000..3d897a0 --- /dev/null +++ b/src/test/kotlin/net/ipksindia/ApplicationTest.kt @@ -0,0 +1,21 @@ +package net.ipksindia + +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.server.testing.* +import kotlin.test.* +import net.ipksindia.plugins.* + +class ApplicationTest { + @Test + fun testRoot() = testApplication { + application { + configureRouting() + } + client.get("/").apply { + assertEquals(HttpStatusCode.OK, status) + assertEquals("Hello World!", bodyAsText()) + } + } +}