commit 53eba06299d1f3a5809bc0e368b0b9add3b3ec7c Author: Ramon Ramirez Date: Thu Jan 22 17:22:38 2026 -0400 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..94810d0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +* +!target/*-runner +!target/*-runner.jar +!target/lib/* +!target/quarkus-app/* \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91a800a --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env + +# Plugin directory +/.quarkus/cli/plugins/ +# TLS Certificates +.certs/ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8dea6c2 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/README.md b/README.md new file mode 100644 index 0000000..75c4582 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# bus-create-security-notification + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: + +```shell script +./mvnw quarkus:dev +``` + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at . + +## Packaging and running the application + +The application can be packaged using: + +```shell script +./mvnw package +``` + +It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory. + +The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`. + +If you want to build an _über-jar_, execute the following command: + +```shell script +./mvnw package -Dquarkus.package.jar.type=uber-jar +``` + +The application, packaged as an _über-jar_, is now runnable using `java -jar target/*-runner.jar`. + +## Creating a native executable + +You can create a native executable using: + +```shell script +./mvnw package -Dnative +``` + +Or, if you don't have GraalVM installed, you can run the native executable build in a container using: + +```shell script +./mvnw package -Dnative -Dquarkus.native.container-build=true +``` + +You can then execute your native executable with: `./target/bus-create-security-notification-1.0-native-quarkus-jdk17-runner` + +If you want to learn more about building native executables, please consult . diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + 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" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5bad0ab --- /dev/null +++ b/pom.xml @@ -0,0 +1,156 @@ + + + 4.0.0 + com.banesco + bus-create-security-notification + 1.0-native-quarkus-jdk17 + bus-create-security-notification + API Business - Create payment single request + + + 17 + 17 + 17 + 3.14.1 + 1.18.2 + UTF-8 + UTF-8 + quarkus-bom + io.quarkus.platform + 3.22.3 + 1.18.42 + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + ${quarkus.platform.group-id} + quarkus-camel-bom + ${quarkus.platform.version} + pom + import + + + + + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-rest-jackson + + + io.quarkus + quarkus-hibernate-validator + + + io.quarkus + quarkus-smallrye-openapi + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-config-yaml + + + io.quarkus + quarkus-rest-client + + + io.quarkus + quarkus-rest-client-jackson + + + io.quarkus + quarkus-smallrye-health + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + + + org.eclipse.jkube + openshift-maven-plugin + ${compiler-plugin-openshift.version} + + + + + maven-volumen + /root/.m2 + + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + native-image-agent + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${compiler-plugin.version} + + true + + + + + + + + native + + + native + + + + native + true + + + + + src/main/resources + + application-dev.yaml + application-local.yaml + + + + + + + \ No newline at end of file diff --git a/scripts/native/Dockerfile b/scripts/native/Dockerfile new file mode 100644 index 0000000..c3b34be --- /dev/null +++ b/scripts/native/Dockerfile @@ -0,0 +1,31 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./mvnw package -Dnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/rec-legal-customer-product-directory . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/rec-legal-customer-product-directory +# +### +FROM quay.io/quarkus/quarkus-micro-image:2.0 +RUN mkdir -p /work +ENV TZ="America/Caracas" +ENV LANGUAGE='en_US:en' +VOLUME /tmp +COPY /file/*-runner /work/app +RUN chmod -R 775 /work +RUN ls -ltra /work/ +EXPOSE 8080 +WORKDIR /work/ + +ENTRYPOINT ["./app", "-Dquarkus.http.host=0.0.0.0"] \ No newline at end of file diff --git a/scripts/native/file/bus-create-security-notification-1.0-native-quarkus-jdk17-runner b/scripts/native/file/bus-create-security-notification-1.0-native-quarkus-jdk17-runner new file mode 100644 index 0000000..7089221 Binary files /dev/null and b/scripts/native/file/bus-create-security-notification-1.0-native-quarkus-jdk17-runner differ diff --git a/src/main/docker/Dockerfile.jvm b/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..49119f0 --- /dev/null +++ b/src/main/docker/Dockerfile.jvm @@ -0,0 +1,98 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./mvnw package +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/bus-create-security-notification-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/bus-create-security-notification-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. +# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 +# when running the container +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/bus-create-security-notification-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") - Be aware that this will override +# the default JVM options, use `JAVA_OPTS_APPEND` to append options +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi9/openjdk-17:1.23 + +ENV LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 target/quarkus-app/*.jar /deployments/ +COPY --chown=185 target/quarkus-app/app/ /deployments/app/ +COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + +ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] + diff --git a/src/main/docker/Dockerfile.legacy-jar b/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 0000000..c0461e0 --- /dev/null +++ b/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,94 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./mvnw package -Dquarkus.package.jar.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/bus-create-security-notification-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/bus-create-security-notification-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. +# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 +# when running the container +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/bus-create-security-notification-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") - Be aware that this will override +# the default JVM options, use `JAVA_OPTS_APPEND` to append options +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi9/openjdk-17:1.23 + +ENV LANGUAGE='en_US:en' + + +COPY target/lib/* /deployments/lib/ +COPY target/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + +ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] diff --git a/src/main/docker/Dockerfile.native b/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..831e9e7 --- /dev/null +++ b/src/main/docker/Dockerfile.native @@ -0,0 +1,29 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./mvnw package -Dnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/bus-create-security-notification . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/bus-create-security-notification +# +# The ` registry.access.redhat.com/ubi9/ubi-minimal:9.7` base image is based on UBI 9. +# To use UBI 8, switch to `quay.io/ubi8/ubi-minimal:8.10`. +### +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.7 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root --chmod=0755 target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/src/main/docker/Dockerfile.native-micro b/src/main/docker/Dockerfile.native-micro new file mode 100644 index 0000000..1f48fbe --- /dev/null +++ b/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,32 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./mvnw package -Dnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/bus-create-security-notification . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/bus-create-security-notification +# +# The `quay.io/quarkus/ubi9-quarkus-micro-image:2.0` base image is based on UBI 9. +# To use UBI 8, switch to `quay.io/quarkus/quarkus-micro-image:2.0`. +### +FROM quay.io/quarkus/ubi9-quarkus-micro-image:2.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root --chmod=0755 target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/src/main/java/com/banesco/common/application/helper/MessageHelper.java b/src/main/java/com/banesco/common/application/helper/MessageHelper.java new file mode 100644 index 0000000..223f0ff --- /dev/null +++ b/src/main/java/com/banesco/common/application/helper/MessageHelper.java @@ -0,0 +1,234 @@ +package com.banesco.common.application.helper; + +import com.banesco.common.domain.exception.HttpStatusCodeException; +import com.banesco.common.domain.model.ApiResponse; +import com.banesco.common.domain.model.ErrorMapping; +import com.banesco.common.domain.model.StatusResponse; +import com.banesco.common.infrastructure.config.MessagesConfig; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Response; +import lombok.extern.slf4j.Slf4j; + +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@ApplicationScoped +@RegisterForReflection +public class MessageHelper { + + private final MessagesConfig messagesConfig; + private final Map errorMappings; + private static final String ERROR_DEFAULT = "default"; + private static final String SUCCESS_DEFAULT = "200"; + private static final String ERROR_FILE_PATH = "errors-mapping/errors.json"; + private final ObjectMapper objectMapper; + + @Inject + public MessageHelper( + ObjectMapper objectMapper, + MessagesConfig messagesConfig + ) { + this.objectMapper = objectMapper; + this.messagesConfig = messagesConfig; + this.errorMappings = initializeErrorMappings(); + } + + public Response handleSuccess(Object data, String statusCode) { + log.info( + "Respuesta exitosa controlada: {}", + statusCode + ); + return buildResponse(data, statusCode); + } + + public Response handleException(HttpStatusCodeException exception) { + log.error( + "Error interno controlado: {} -> {}", + exception.getStatusCode(), + exception.getErrorCode() + ); + return buildErrorResponse(exception); + } + + public Response handleGenericException(Exception exception) { + log.error("Error interno no controlado: {}", exception.getMessage()); + return buildErrorResponse(HttpStatusCodeException.internalServer("500")); + } + + public StatusResponse createStatusResponse(String code) { + ErrorMapping successMapping = getError(code); + + return StatusResponse.builder() + .statusCode(successMapping.getStatusCode()) + .message(successMapping.getDescription()) + .build(); + } + + public StatusResponse createError( + ErrorMapping mapping, + String fieldPath + ) { + String message = mapping.getDescription(); + + if (fieldPath != null && message != null && message.contains("%s")) { + message = String.format(message, fieldPath); + } + + return StatusResponse.builder() + .statusCode(mapping.getStatusCode()) + .message(message) + .build(); + } + + public ApiResponse buildServiceUnavailableResponse() { + return new ApiResponse<>(createStatusResponse("503")); + } + + private ErrorMapping getError(String errorCode) { + return errorMappings.getOrDefault( + errorCode, errorMappings.getOrDefault(ERROR_DEFAULT, createDefaultMapping()) + ); + } + + private Response buildErrorResponse(HttpStatusCodeException exception) { + ErrorMapping mapping = errorMappings.getOrDefault( + exception.getErrorCode(), errorMappings.getOrDefault( + String.valueOf(exception.getStatusCode()), + createDefaultMapping() + ) + ); + StatusResponse status = createError(mapping, exception.getFieldPath()); + + log.error( + "[{}] Message {} -> {}", + exception.getExceptionType(), + exception.getErrorCode(), + status.getMessage() + ); + + return Response.status(mapping.getHttpCode()) + .entity(new ApiResponse<>(status)) + .build(); + } + + private Response buildResponse(Object data, String statusCode) { + ErrorMapping mapping = errorMappings.getOrDefault( + statusCode, errorMappings.getOrDefault( + statusCode, createDefaultMapping() + ) + ); + StatusResponse status = createError(mapping, null); + + log.error( + "[Success] Message {} -> {}", + statusCode, + status.getMessage() + ); + + return Response.status(mapping.getHttpCode()) + .entity(new ApiResponse<>(data, status)) + .build(); + } + + private Map initializeErrorMappings() { + try { + String json; + + if (isReadingFromProps()) { + json = messagesConfig.getErrorMessagesJson(); + log.info("Cargando mensajes de errores desde properties"); + } else { + json = loadFromJsonFile(); + log.info("Cargando mensajes de errores desdel cliente JSON"); + } + + if (json == null || json.isEmpty()) { + log.warn("No se encontro JSON de errores"); + return createDefaultMappings(); + } + + List mappings = objectMapper.readValue(json, new TypeReference>() {}); + Map result = new ConcurrentHashMap<>(); + + for (ErrorMapping mapping : mappings) { + if (mapping.getBackendCode() != null && !mapping.getBackendCode().trim().isEmpty()) { + if (result.containsKey(mapping.getBackendCode())) { + log.warn("Clave duplicada encontrada en mappings de errores: {}", mapping.getBackendCode()); + } else { + result.put(mapping.getBackendCode(), mapping); + } + } else { + log.warn("Ignorando mapping sin backendCode valido: {}", mapping.getDescription()); + } + } + + log.info("Mappings de errores cargados exitosamente, total validos: {}", result.size()); + return result; + } catch (Exception e) { + log.error("Error cargando mappings de errores: {}", e.getMessage()); + return createDefaultMappings(); + } + } + + private String loadFromJsonFile() { + try (InputStream is = MessageHelper.class.getClassLoader().getResourceAsStream(ERROR_FILE_PATH)) { + if (is == null) { + log.warn("No se encontro el archivo de errores: {}", ERROR_FILE_PATH); + return ""; + } + return new String(is.readAllBytes()); + } catch (Exception e) { + log.error("Error leyendo archivo de errores: {}", e.getMessage()); + return ""; + } + } + + private Map createDefaultMappings() { + Map defaults = new ConcurrentHashMap<>(); + defaults.put(ERROR_DEFAULT, createDefaultMapping()); + defaults.put(SUCCESS_DEFAULT, createSuccessMapping()); + return defaults; + } + + private ErrorMapping createDefaultMapping() { + ErrorMapping mapping = new ErrorMapping(); + mapping.setBackendCode(ERROR_DEFAULT); + mapping.setHttpCode(409); + mapping.setStatusCode("409"); + mapping.setDescription("Conflicto"); + return mapping; + } + + private ErrorMapping createSuccessMapping() { + ErrorMapping mapping = new ErrorMapping(); + mapping.setBackendCode(SUCCESS_DEFAULT); + mapping.setHttpCode(200); + mapping.setStatusCode("200"); + mapping.setDescription("Operacion exitosa"); + return mapping; + } + + public boolean isReadingFromProps() { + return messagesConfig.isReadFromProps(); + } + + public boolean isSuccessStatusCode(StatusResponse statusResponse) { + if (statusResponse == null || statusResponse.getStatusCode() == null) { + return false; + } + + try { + int statusCode = Integer.parseInt(statusResponse.getStatusCode()); + return statusCode >= 200 && statusCode < 300; + } catch (NumberFormatException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/common/application/service/HttpClientService.java b/src/main/java/com/banesco/common/application/service/HttpClientService.java new file mode 100644 index 0000000..fb42a5c --- /dev/null +++ b/src/main/java/com/banesco/common/application/service/HttpClientService.java @@ -0,0 +1,466 @@ +package com.banesco.common.application.service; + +import com.banesco.common.application.usecase.HttpClientUseCase; +import com.banesco.common.domain.exception.HttpApiResponseException; +import com.banesco.common.domain.exception.HttpStatusCodeException; +import com.banesco.common.domain.model.*; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.client.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Slf4j +@ApplicationScoped +public class HttpClientService implements HttpClientUseCase { + + private final ObjectMapper objectMapper; + + @Inject + public HttpClientService(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public T execute(HttpRequest request) { + return executeInternal(request); + } + + @Override + public ApiResponse executeApiResponse(HttpRequest request) { + return executeInternal(request); + } + + @Override + public ApiResponse> executeApiResponseList( + HttpRequest request + ) { + return executeInternal(request); + } + + @Override + public ApiPrivateResponse> executeApiPrivateResponse( + HttpRequest request + ) { + return executeInternal(request); + } + + @Override + public ApiPrivateResponse, ApiPrivateError>> executeApiPrivateResponseList( + HttpRequest request + ) { + return executeInternal(request); + } + + private T executeInternal(HttpRequest request) { + String finalUrl = buildFinalUrl(request); + + if (request.isLogRequestBody()) { + log.info("URL final: {}", finalUrl); + + if (request.getHeaders() != null && !request.getHeaders().isEmpty()) { + log.info("Headers: {}", request.getHeaders()); + } + + if (request.getQueryParams() != null && !request.getQueryParams().isEmpty()) { + log.info("Query params: {}", request.getQueryParams()); + } + + if (request.getBody() != null) { + log.info("Body: {}", request.getBody()); + } + } + + try (Client client = createClient(request.getConnectTimeout(), request.getReadTimeout())) { + WebTarget target = client.target(finalUrl); + Invocation.Builder builder = target.request(MediaType.APPLICATION_JSON); + + if (request.getHeaders() != null) { + request.getHeaders().forEach(builder::header); + } + + Response response = buildRequest(builder, request); + return handleResponse(request, response); + } catch (HttpStatusCodeException | HttpApiResponseException e) { + throw e; + } catch (Exception e) { + log.error("Error de conexion {}: {}", request.getMethod(), e.getMessage()); + throw HttpStatusCodeException.serviceUnavailable( + "503", + "Error de conexion con el servicio externo: " + e.getMessage() + ); + } + } + + private String buildFinalUrl(HttpRequest request) { + String finalUrl = request.getUrl(); + + if (request.getPathParams() != null && !request.getPathParams().isEmpty()) { + for (Map.Entry entry : request.getPathParams().entrySet()) { + String placeholder = "{" + entry.getKey() + "}"; + finalUrl = finalUrl.replace(placeholder, entry.getValue()); + } + } + + return appendQueryParams(finalUrl, request.getQueryParams()); + } + + private String appendQueryParams(String url, Map queryParams) { + if (queryParams == null || queryParams.isEmpty()) { + return url; + } + + StringBuilder urlBuilder = new StringBuilder(url); + boolean firstParam = !url.contains("?"); + + for (Map.Entry entry : queryParams.entrySet()) { + if (firstParam) { + urlBuilder.append("?"); + firstParam = false; + } else { + urlBuilder.append("&"); + } + + String encodedKey = URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8); + String encodedValue = entry.getValue() != null + ? URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8) + : ""; + + urlBuilder.append(encodedKey).append("=").append(encodedValue); + } + + return urlBuilder.toString(); + } + + private Response buildRequest( + Invocation.Builder builder, + HttpRequest request + ) { + log.info("Metodo HTTP: {}", request.getMethod().name()); + + return switch (request.getMethod()) { + case GET -> builder.get(); + case POST -> builder.post(Entity.entity(request.getBody(), MediaType.APPLICATION_JSON)); + case PUT -> builder.put(Entity.entity(request.getBody(), MediaType.APPLICATION_JSON)); + case DELETE -> builder.delete(); + case PATCH -> builder.method("PATCH", Entity.entity(request.getBody(), MediaType.APPLICATION_JSON)); + case HEAD -> builder.head(); + case OPTIONS -> builder.options(); + default -> throw new IllegalArgumentException("Metodo HTTP no soportado: " + request.getMethod()); + }; + } + + private Client createClient(int connectTimeout, int readTimeout) { + return ClientBuilder.newBuilder() + .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS) + .readTimeout(readTimeout, TimeUnit.MILLISECONDS) + .build(); + } + + private T handleResponse( + HttpRequest request, + Response response + ) { + int statusCode = response.getStatus(); + log.info("Respuesta {} - Status: {}", request.getMethod(), statusCode); + + try (response) { + String responseBody = response.readEntity(String.class); + + if (request.isLogResponseBody()) { + log.info("Respuesta Cuerpo: {}", responseBody); + } + + if (statusCode >= 200 && statusCode < 300) { + if (request.getResponseType() == Void.class || request.getResponseType() == void.class) { + return null; + } + + T result = responseResult(request, responseBody); + + log.debug("Respuesta exitosa {} {}: {}", request.getMethod(), request.getUrl(), result); + + return result; + } else { + log.error( + "Error HTTP {} {} - Status: {} - Body: {}", + request.getMethod(), + request.getUrl(), + statusCode, + responseBody + ); + + if (isApiResponseFormat(responseBody)) { + ApiResponse apiResponse = deserializeApiResponse(responseBody, request); + throw new HttpApiResponseException(statusCode, apiResponse); + } else { + throw mapHttpStatusToException(statusCode, responseBody); + } + } + } catch (HttpStatusCodeException | HttpApiResponseException e) { + throw e; + } catch (Exception e) { + log.error( + "Error procesando respuesta {} {}: {}", + request.getMethod(), + request.getUrl(), + e.getMessage() + ); + throw HttpStatusCodeException.internalServer( + "500", "Error procesando respuesta del servicio externo: " + e.getMessage() + ); + } + } + + private T responseResult( + HttpRequest request, + String responseBody + ) throws JsonProcessingException { + if (request.isApiPrivateResponse() && request.isEitherResponse()) { + return handleApiPrivateResponseWithEither(request, responseBody); + } + + T result; + + if (request.getResponseType() == ApiResponse.class) { + result = deserializeApiResponse(responseBody, request); + } else if (request.getComplexType() != null) { + JavaType javaType = objectMapper.getTypeFactory().constructParametricType( + request.getResponseType(), objectMapper.getTypeFactory().constructType(request.getComplexType()) + ); + result = objectMapper.readValue(responseBody, javaType); + } else if (request.getGenericType() != null) { + JavaType javaType = objectMapper.getTypeFactory().constructParametricType( + request.getResponseType(), objectMapper.getTypeFactory().constructType(request.getGenericType()) + ); + result = objectMapper.readValue(responseBody, javaType); + } else { + result = objectMapper.readValue( + responseBody, objectMapper.getTypeFactory().constructType(request.getResponseType()) + ); + } + + return result; + } + + private T handleApiPrivateResponseWithEither( + HttpRequest request, + String responseBody + ) throws JsonProcessingException { + JsonNode rootNode = objectMapper.readTree(responseBody); + String status = rootNode.has("estatus") ? rootNode.get("estatus").asText() : null; + String message = rootNode.has("mensaje") ? rootNode.get("mensaje").asText() : null; + JsonNode detailNode = rootNode.get("detalle"); + + if (request.getStatusSuccess().equals(status)) { + return handleSuccessResponse(request, status, message, detailNode); + } else { + return handleErrorResponse(status, message, detailNode); + } + } + + @SuppressWarnings("unchecked") + private T handleSuccessResponse( + HttpRequest request, + String status, + String message, + JsonNode detailNode + ) { + Object successData; + + if (request.isListResponse()) { + successData = handleListSuccess(request, detailNode); + ApiPrivateResponse, ApiPrivateError>> response = new ApiPrivateResponse<>(); + + response.setEstatus(status); + response.setMensaje(message); + response.setDetalle(Either.left((List) successData)); + return (T) response; + } else { + successData = handleObjectSuccess(request, detailNode); + ApiPrivateResponse> response = new ApiPrivateResponse<>(); + + response.setEstatus(status); + response.setMensaje(message); + response.setDetalle(Either.left(successData)); + return (T) response; + } + } + + private Object handleListSuccess( + HttpRequest request, + JsonNode detailNode + ) { + Class elementType = getElementTypeFromRequest(request); + JavaType listType = objectMapper.getTypeFactory().constructCollectionType(List.class, elementType); + + if (detailNode != null && !detailNode.isNull()) { + return objectMapper.convertValue(detailNode, listType); + } + + return List.of(); + } + + private Object handleObjectSuccess( + HttpRequest request, + JsonNode detailNode + ) { + Class elementType = getElementTypeFromRequest(request); + + if (detailNode != null && !detailNode.isNull()) { + return objectMapper.convertValue(detailNode, elementType); + } + + return null; + } + + @SuppressWarnings("unchecked") + private T handleErrorResponse( + String status, + String message, + JsonNode detailNode + ) { + ApiPrivateError error = buildApiPrivateError(detailNode, message); + ApiPrivateResponse> response = new ApiPrivateResponse<>(); + + response.setEstatus(status); + response.setMensaje(message); + response.setDetalle(Either.right(error)); + + return (T) response; + } + + private ApiPrivateError buildApiPrivateError( + JsonNode detailNode, + String message + ) { + if (detailNode != null && !detailNode.isNull()) { + try { + return objectMapper.convertValue(detailNode, ApiPrivateError.class); + } catch (Exception e) { + log.warn("Cannot map detail to ApiPrivateError, creating default error. Detail: {}", detailNode); + } + } + + return ApiPrivateError.builder() + .codError(null) + .mensajeError(message) + .constraintName(null) + .build(); + } + + private Class getElementTypeFromRequest(HttpRequest request) { + if (request.getGenericType() != null) { + return request.getGenericType(); + } + + if (request.getComplexType() instanceof ParameterizedType pt) { + Type[] typeArgs = pt.getActualTypeArguments(); + + if (typeArgs.length > 0) { + Type firstArg = typeArgs[0]; + + if (firstArg instanceof ParameterizedType innerPt) { + Type[] innerArgs = innerPt.getActualTypeArguments(); + + if (innerArgs.length > 0 && innerArgs[0] instanceof Class innerClass) { + return innerClass; + } + } else if (firstArg instanceof Class elementClass) { + return elementClass; + } + } + } + + log.warn("No se pudo determinar el elementType del request, usando Object.class"); + return Object.class; + } + + @SuppressWarnings("unchecked") + private T deserializeApiResponse( + String responseBody, + HttpRequest request + ) { + try { + if (request.getGenericType() != null) { + JavaType javaType = objectMapper.getTypeFactory().constructParametricType( + ApiResponse.class, + objectMapper.getTypeFactory().constructType(request.getGenericType()) + ); + return objectMapper.readValue(responseBody, javaType); + } else { + return (T) objectMapper.readValue(responseBody, ApiResponse.class); + } + } catch (JsonProcessingException e) { + log.error("Error deserializando respuesta JSON: {}", e.getMessage()); + throw HttpStatusCodeException.internalServer( + "500", "Error deserializando respuesta JSON: " + e.getMessage() + ); + } catch (Exception e) { + log.error("Error desconocido al deserializar respuesta: {}", e.getMessage()); + throw HttpStatusCodeException.internalServer( + "500", "Error desconocido al deserializar respuesta: " + e.getMessage() + ); + } + } + + private boolean isApiResponseFormat(String responseBody) { + try { + if (responseBody == null || responseBody.trim().isEmpty()) { + return false; + } + + return responseBody.contains("\"data\"") && + responseBody.contains("\"statusResponse\"") && + responseBody.contains("\"statusCode\"") && + responseBody.contains("\"message\""); + } catch (Exception e) { + return false; + } + } + + private HttpStatusCodeException mapHttpStatusToException( + int statusCode, + String errorBody + ) { + String errorCode = "HTTP_" + statusCode; + String defaultMessage = "Error en servicio externo: HTTP " + statusCode; + String message = errorBody != null && !errorBody.isEmpty() + ? errorBody + : defaultMessage; + + return switch (statusCode) { + case 400 -> HttpStatusCodeException.badRequest(errorCode, message); + case 401 -> HttpStatusCodeException.unauthorized(errorCode, message); + case 403 -> HttpStatusCodeException.forbidden(errorCode, message); + case 404 -> HttpStatusCodeException.notFound(errorCode, message); + case 405 -> HttpStatusCodeException.methodNotAllowed(errorCode, message); + case 408 -> HttpStatusCodeException.fromStatusCode(408, errorCode, message); + case 409 -> HttpStatusCodeException.conflict(errorCode, message); + case 410 -> HttpStatusCodeException.gone(errorCode, message); + case 412 -> HttpStatusCodeException.preconditionFailed(errorCode, message); + case 415 -> HttpStatusCodeException.unsupportedMediaType(errorCode, message); + case 422 -> HttpStatusCodeException.unprocessableEntity(errorCode, message); + case 429 -> HttpStatusCodeException.tooManyRequests(errorCode, message); + case 500 -> HttpStatusCodeException.internalServer(errorCode, message); + case 501 -> HttpStatusCodeException.notImplemented(errorCode, message); + case 502 -> HttpStatusCodeException.badGateway(errorCode, message); + case 503 -> HttpStatusCodeException.serviceUnavailable(errorCode, message); + case 504 -> HttpStatusCodeException.gatewayTimeout(errorCode, message); + default -> HttpStatusCodeException.fromStatusCode(statusCode, errorCode, message); + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/common/application/usecase/HttpClientUseCase.java b/src/main/java/com/banesco/common/application/usecase/HttpClientUseCase.java new file mode 100644 index 0000000..c7a0b7b --- /dev/null +++ b/src/main/java/com/banesco/common/application/usecase/HttpClientUseCase.java @@ -0,0 +1,18 @@ +package com.banesco.common.application.usecase; + +import com.banesco.common.domain.model.*; + +import java.util.List; + +public interface HttpClientUseCase { + + T execute(HttpRequest request); + + ApiResponse executeApiResponse(HttpRequest request); + + ApiResponse> executeApiResponseList(HttpRequest request); + + ApiPrivateResponse> executeApiPrivateResponse(HttpRequest request); + + ApiPrivateResponse, ApiPrivateError>> executeApiPrivateResponseList(HttpRequest request); +} \ No newline at end of file diff --git a/src/main/java/com/banesco/common/domain/exception/BaseApiException.java b/src/main/java/com/banesco/common/domain/exception/BaseApiException.java new file mode 100644 index 0000000..c44d4dd --- /dev/null +++ b/src/main/java/com/banesco/common/domain/exception/BaseApiException.java @@ -0,0 +1,21 @@ +package com.banesco.common.domain.exception; + +import lombok.Getter; + +@Getter +public abstract class BaseApiException extends RuntimeException { + private final String errorCode; + private final String fieldPath; + private final String exceptionType; + + protected BaseApiException(String errorCode, String message, String fieldPath, String exceptionType) { + super(message); + this.errorCode = errorCode; + this.fieldPath = fieldPath; + this.exceptionType = exceptionType; + } + + protected BaseApiException(String errorCode, String fieldPath, String exceptionType) { + this(errorCode, null, fieldPath, exceptionType); + } +} diff --git a/src/main/java/com/banesco/common/domain/exception/BusinessException.java b/src/main/java/com/banesco/common/domain/exception/BusinessException.java new file mode 100644 index 0000000..6d44727 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/exception/BusinessException.java @@ -0,0 +1,11 @@ +package com.banesco.common.domain.exception; + +public class BusinessException extends BaseApiException { + public BusinessException(String errorCode, String message, String fieldPath) { + super(errorCode, message, fieldPath, "business"); + } + + public BusinessException(String errorCode, String fieldPath) { + super(errorCode, fieldPath, "business"); + } +} diff --git a/src/main/java/com/banesco/common/domain/exception/HttpApiResponseException.java b/src/main/java/com/banesco/common/domain/exception/HttpApiResponseException.java new file mode 100644 index 0000000..399bdd9 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/exception/HttpApiResponseException.java @@ -0,0 +1,20 @@ +package com.banesco.common.domain.exception; + +import com.banesco.common.domain.model.ApiResponse; +import lombok.Getter; + +@Getter +public class HttpApiResponseException extends RuntimeException { + private final int statusCode; + private final transient ApiResponse apiResponse; + + public HttpApiResponseException(int statusCode, ApiResponse apiResponse) { + super(String.format( + "HTTP %d: %s", statusCode, + apiResponse.getStatusResponse() != null ? + apiResponse.getStatusResponse().getMessage() : "Error sin mensaje" + )); + this.statusCode = statusCode; + this.apiResponse = apiResponse; + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/common/domain/exception/HttpClientException.java b/src/main/java/com/banesco/common/domain/exception/HttpClientException.java new file mode 100644 index 0000000..04f4f03 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/exception/HttpClientException.java @@ -0,0 +1,11 @@ +package com.banesco.common.domain.exception; + +public class HttpClientException extends RuntimeException { + public HttpClientException(String message) { + super(message); + } + + public HttpClientException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/banesco/common/domain/exception/HttpStatusCodeException.java b/src/main/java/com/banesco/common/domain/exception/HttpStatusCodeException.java new file mode 100644 index 0000000..588bb6c --- /dev/null +++ b/src/main/java/com/banesco/common/domain/exception/HttpStatusCodeException.java @@ -0,0 +1,320 @@ +package com.banesco.common.domain.exception; + +import lombok.*; + +import java.util.Map; +import java.util.HashMap; + +@Getter +@ToString +@Builder +public class HttpStatusCodeException extends BaseApiException { + private final int statusCode; + + private static final Map STATUS_CATEGORIES = new HashMap<>(); + + static { + STATUS_CATEGORIES.put(100, "continue"); + STATUS_CATEGORIES.put(101, "switching-protocols"); + STATUS_CATEGORIES.put(102, "processing"); + STATUS_CATEGORIES.put(103, "early-hints"); + STATUS_CATEGORIES.put(200, "ok"); + STATUS_CATEGORIES.put(201, "created"); + STATUS_CATEGORIES.put(202, "accepted"); + STATUS_CATEGORIES.put(203, "non-authoritative-information"); + STATUS_CATEGORIES.put(204, "no-content"); + STATUS_CATEGORIES.put(205, "reset-content"); + STATUS_CATEGORIES.put(206, "partial-content"); + STATUS_CATEGORIES.put(207, "multi-status"); + STATUS_CATEGORIES.put(208, "already-reported"); + STATUS_CATEGORIES.put(226, "im-used"); + STATUS_CATEGORIES.put(300, "multiple-choices"); + STATUS_CATEGORIES.put(301, "moved-permanently"); + STATUS_CATEGORIES.put(302, "found"); + STATUS_CATEGORIES.put(303, "see-other"); + STATUS_CATEGORIES.put(304, "not-modified"); + STATUS_CATEGORIES.put(305, "use-proxy"); + STATUS_CATEGORIES.put(307, "temporary-redirect"); + STATUS_CATEGORIES.put(308, "permanent-redirect"); + STATUS_CATEGORIES.put(400, "bad-request"); + STATUS_CATEGORIES.put(401, "unauthorized"); + STATUS_CATEGORIES.put(402, "payment-required"); + STATUS_CATEGORIES.put(403, "forbidden"); + STATUS_CATEGORIES.put(404, "not-found"); + STATUS_CATEGORIES.put(405, "method-not-allowed"); + STATUS_CATEGORIES.put(406, "not-acceptable"); + STATUS_CATEGORIES.put(407, "proxy-authentication-required"); + STATUS_CATEGORIES.put(408, "request-timeout"); + STATUS_CATEGORIES.put(409, "conflict"); + STATUS_CATEGORIES.put(410, "gone"); + STATUS_CATEGORIES.put(411, "length-required"); + STATUS_CATEGORIES.put(412, "precondition-failed"); + STATUS_CATEGORIES.put(413, "payload-too-large"); + STATUS_CATEGORIES.put(414, "uri-too-long"); + STATUS_CATEGORIES.put(415, "unsupported-media-type"); + STATUS_CATEGORIES.put(416, "range-not-satisfiable"); + STATUS_CATEGORIES.put(417, "expectation-failed"); + STATUS_CATEGORIES.put(418, "im-a-teapot"); + STATUS_CATEGORIES.put(421, "misdirected-request"); + STATUS_CATEGORIES.put(422, "unprocessable-entity"); + STATUS_CATEGORIES.put(423, "locked"); + STATUS_CATEGORIES.put(424, "failed-dependency"); + STATUS_CATEGORIES.put(425, "too-early"); + STATUS_CATEGORIES.put(426, "upgrade-required"); + STATUS_CATEGORIES.put(428, "precondition-required"); + STATUS_CATEGORIES.put(429, "too-many-requests"); + STATUS_CATEGORIES.put(431, "request-header-fields-too-large"); + STATUS_CATEGORIES.put(451, "unavailable-for-legal-reasons"); + STATUS_CATEGORIES.put(500, "internal-server-error"); + STATUS_CATEGORIES.put(501, "not-implemented"); + STATUS_CATEGORIES.put(502, "bad-gateway"); + STATUS_CATEGORIES.put(503, "service-unavailable"); + STATUS_CATEGORIES.put(504, "gateway-timeout"); + STATUS_CATEGORIES.put(505, "http-version-not-supported"); + STATUS_CATEGORIES.put(506, "variant-also-negotiates"); + STATUS_CATEGORIES.put(507, "insufficient-storage"); + STATUS_CATEGORIES.put(508, "loop-detected"); + STATUS_CATEGORIES.put(510, "not-extended"); + STATUS_CATEGORIES.put(511, "network-authentication-required"); + } + + public HttpStatusCodeException(int statusCode, String errorCode, String message, String fieldPath) { + super(errorCode, message, fieldPath, getHttpStatusCategory(statusCode)); + this.statusCode = statusCode; + } + + public HttpStatusCodeException(int statusCode, String errorCode, String message) { + this(statusCode, errorCode, message, null); + } + + public HttpStatusCodeException(int statusCode, String errorCode) { + this(statusCode, errorCode, getDefaultMessage(statusCode), null); + } + + public HttpStatusCodeException(int statusCode) { + this(statusCode, "HTTP_" + statusCode); + } + + private static String getHttpStatusCategory(int statusCode) { + String category = STATUS_CATEGORIES.get(statusCode); + + if (category != null) { + return category; + } + + if (statusCode >= 100 && statusCode < 200) return "informational"; + if (statusCode >= 200 && statusCode < 300) return "success"; + if (statusCode >= 300 && statusCode < 400) return "redirection"; + if (statusCode >= 400 && statusCode < 500) return "client-error"; + if (statusCode >= 500 && statusCode < 600) return "server-error"; + return "unknown"; + } + + private static String getDefaultMessage(int statusCode) { + return switch (statusCode) { + case 200 -> "OK"; + case 201 -> "Created"; + case 202 -> "Accepted"; + case 204 -> "No Content"; + case 400 -> "Bad Request"; + case 401 -> "Unauthorized"; + case 403 -> "Forbidden"; + case 404 -> "Not Found"; + case 405 -> "Method Not Allowed"; + case 408 -> "Request Timeout"; + case 409 -> "Conflict"; + case 410 -> "Gone"; + case 422 -> "Unprocessable Entity"; + case 429 -> "Too Many Requests"; + case 500 -> "Internal Server Error"; + case 502 -> "Bad Gateway"; + case 503 -> "Service Unavailable"; + case 504 -> "Gateway Timeout"; + default -> "HTTP " + statusCode; + }; + } + + public static HttpStatusCodeException badRequest(String errorCode) { + return new HttpStatusCodeException(400, errorCode); + } + + public static HttpStatusCodeException badRequest(String errorCode, String fieldPath) { + return new HttpStatusCodeException(400, errorCode, getDefaultMessage(400), fieldPath); + } + + public static HttpStatusCodeException unauthorized(String errorCode) { + return new HttpStatusCodeException(401, errorCode); + } + + public static HttpStatusCodeException unauthorized(String errorCode, String fieldPath) { + return new HttpStatusCodeException(401, errorCode, getDefaultMessage(401), fieldPath); + } + + public static HttpStatusCodeException forbidden(String errorCode) { + return new HttpStatusCodeException(403, errorCode); + } + + public static HttpStatusCodeException forbidden(String errorCode, String fieldPath) { + return new HttpStatusCodeException(403, errorCode, getDefaultMessage(403), fieldPath); + } + + public static HttpStatusCodeException notFound(String errorCode) { + return new HttpStatusCodeException(404, errorCode); + } + + public static HttpStatusCodeException notFound(String errorCode, String fieldPath) { + return new HttpStatusCodeException(404, errorCode, getDefaultMessage(404), fieldPath); + } + + public static HttpStatusCodeException methodNotAllowed(String errorCode) { + return new HttpStatusCodeException(405, errorCode); + } + + public static HttpStatusCodeException methodNotAllowed(String errorCode, String fieldPath) { + return new HttpStatusCodeException(405, errorCode, getDefaultMessage(405), fieldPath); + } + + public static HttpStatusCodeException conflict(String errorCode) { + return new HttpStatusCodeException(409, errorCode); + } + + public static HttpStatusCodeException conflict(String errorCode, String fieldPath) { + return new HttpStatusCodeException(409, errorCode, getDefaultMessage(409), fieldPath); + } + + public static HttpStatusCodeException unprocessableEntity(String errorCode) { + return new HttpStatusCodeException(422, errorCode); + } + + public static HttpStatusCodeException unprocessableEntity(String errorCode, String fieldPath) { + return new HttpStatusCodeException(422, errorCode, getDefaultMessage(422), fieldPath); + } + + public static HttpStatusCodeException tooManyRequests(String errorCode) { + return new HttpStatusCodeException(429, errorCode); + } + + public static HttpStatusCodeException tooManyRequests(String errorCode, String fieldPath) { + return new HttpStatusCodeException(429, errorCode, getDefaultMessage(429), fieldPath); + } + + public static HttpStatusCodeException internalServer(String errorCode) { + return new HttpStatusCodeException(500, errorCode); + } + + public static HttpStatusCodeException internalServer(String errorCode, String fieldPath) { + return new HttpStatusCodeException(500, errorCode, getDefaultMessage(500), fieldPath); + } + + public static HttpStatusCodeException badGateway(String errorCode) { + return new HttpStatusCodeException(502, errorCode); + } + + public static HttpStatusCodeException badGateway(String errorCode, String fieldPath) { + return new HttpStatusCodeException(502, errorCode, getDefaultMessage(502), fieldPath); + } + + public static HttpStatusCodeException serviceUnavailable(String errorCode) { + return new HttpStatusCodeException(503, errorCode); + } + + public static HttpStatusCodeException serviceUnavailable(String errorCode, String fieldPath) { + return new HttpStatusCodeException(503, errorCode, getDefaultMessage(503), fieldPath); + } + + public static HttpStatusCodeException gatewayTimeout(String errorCode) { + return new HttpStatusCodeException(504, errorCode); + } + + public static HttpStatusCodeException gatewayTimeout(String errorCode, String fieldPath) { + return new HttpStatusCodeException(504, errorCode, getDefaultMessage(504), fieldPath); + } + + public static HttpStatusCodeException paymentRequired(String errorCode) { + return new HttpStatusCodeException(402, errorCode); + } + + public static HttpStatusCodeException paymentRequired(String errorCode, String fieldPath) { + return new HttpStatusCodeException(402, errorCode, getDefaultMessage(402), fieldPath); + } + + public static HttpStatusCodeException gone(String errorCode) { + return new HttpStatusCodeException(410, errorCode); + } + + public static HttpStatusCodeException gone(String errorCode, String fieldPath) { + return new HttpStatusCodeException(410, errorCode, getDefaultMessage(410), fieldPath); + } + + public static HttpStatusCodeException preconditionFailed(String errorCode) { + return new HttpStatusCodeException(412, errorCode); + } + + public static HttpStatusCodeException preconditionFailed(String errorCode, String fieldPath) { + return new HttpStatusCodeException(412, errorCode, getDefaultMessage(412), fieldPath); + } + + public static HttpStatusCodeException unsupportedMediaType(String errorCode) { + return new HttpStatusCodeException(415, errorCode); + } + + public static HttpStatusCodeException unsupportedMediaType(String errorCode, String fieldPath) { + return new HttpStatusCodeException(415, errorCode, getDefaultMessage(415), fieldPath); + } + + public static HttpStatusCodeException notImplemented(String errorCode) { + return new HttpStatusCodeException(501, errorCode); + } + + public static HttpStatusCodeException notImplemented(String errorCode, String fieldPath) { + return new HttpStatusCodeException(501, errorCode, getDefaultMessage(501), fieldPath); + } + + public static HttpStatusCodeException ok(String errorCode) { + return new HttpStatusCodeException(200, errorCode); + } + + public static HttpStatusCodeException ok(String errorCode, String fieldPath) { + return new HttpStatusCodeException(200, errorCode, getDefaultMessage(200), fieldPath); + } + + public static HttpStatusCodeException created(String errorCode) { + return new HttpStatusCodeException(201, errorCode); + } + + public static HttpStatusCodeException created(String errorCode, String fieldPath) { + return new HttpStatusCodeException(201, errorCode, getDefaultMessage(201), fieldPath); + } + + public static HttpStatusCodeException accepted(String errorCode) { + return new HttpStatusCodeException(202, errorCode); + } + + public static HttpStatusCodeException accepted(String errorCode, String fieldPath) { + return new HttpStatusCodeException(202, errorCode, getDefaultMessage(202), fieldPath); + } + + public static HttpStatusCodeException noContent(String errorCode) { + return new HttpStatusCodeException(204, errorCode); + } + + public static HttpStatusCodeException noContent(String errorCode, String fieldPath) { + return new HttpStatusCodeException(204, errorCode, getDefaultMessage(204), fieldPath); + } + + public static HttpStatusCodeException requestTimeout(String errorCode) { + return new HttpStatusCodeException(408, errorCode); + } + + public static HttpStatusCodeException requestTimeout(String errorCode, String fieldPath) { + return new HttpStatusCodeException(408, errorCode, getDefaultMessage(408), fieldPath); + } + + public static HttpStatusCodeException fromStatusCode(int statusCode, String errorCode) { + return new HttpStatusCodeException(statusCode, errorCode); + } + + public static HttpStatusCodeException fromStatusCode(int statusCode, String errorCode, String fieldPath) { + return new HttpStatusCodeException(statusCode, errorCode, getDefaultMessage(statusCode), fieldPath); + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/common/domain/model/Amount.java b/src/main/java/com/banesco/common/domain/model/Amount.java new file mode 100644 index 0000000..75fb5e5 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/model/Amount.java @@ -0,0 +1,17 @@ +package com.banesco.common.domain.model; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +import java.math.BigDecimal; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class Amount { + private BigDecimal value; + private String currencyCode; +} diff --git a/src/main/java/com/banesco/common/domain/model/ApiPrivateError.java b/src/main/java/com/banesco/common/domain/model/ApiPrivateError.java new file mode 100644 index 0000000..f333114 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/model/ApiPrivateError.java @@ -0,0 +1,16 @@ +package com.banesco.common.domain.model; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class ApiPrivateError { + private Long codError; + private String mensajeError; + private String constraintName; +} \ No newline at end of file diff --git a/src/main/java/com/banesco/common/domain/model/ApiPrivateResponse.java b/src/main/java/com/banesco/common/domain/model/ApiPrivateResponse.java new file mode 100644 index 0000000..b66edb5 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/model/ApiPrivateResponse.java @@ -0,0 +1,17 @@ +package com.banesco.common.domain.model; + +import lombok.*; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class ApiPrivateResponse { + private String estatus; + private String mensaje; + private T detalle; + private String codeline; + private String __equalsCalc; + private Boolean __hashCodeCalc; +} \ No newline at end of file diff --git a/src/main/java/com/banesco/common/domain/model/ApiResponse.java b/src/main/java/com/banesco/common/domain/model/ApiResponse.java new file mode 100644 index 0000000..a5e4845 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/model/ApiResponse.java @@ -0,0 +1,18 @@ +package com.banesco.common.domain.model; + +import lombok.*; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class ApiResponse { + private T data; + private StatusResponse statusResponse; + + public ApiResponse(StatusResponse statusResponse) { + this.statusResponse = statusResponse; + this.data = null; + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/common/domain/model/BankService.java b/src/main/java/com/banesco/common/domain/model/BankService.java new file mode 100644 index 0000000..210d1e5 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/model/BankService.java @@ -0,0 +1,16 @@ +package com.banesco.common.domain.model; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class BankService { + private String bankCode; + private String serviceCode; + private String eventCode; +} diff --git a/src/main/java/com/banesco/common/domain/model/Device.java b/src/main/java/com/banesco/common/domain/model/Device.java new file mode 100644 index 0000000..8299a06 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/model/Device.java @@ -0,0 +1,17 @@ +package com.banesco.common.domain.model; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class Device { + private String deviceType; + private String deviceDescription; + private String deviceIp; + private String deviceSessionReference; +} diff --git a/src/main/java/com/banesco/common/domain/model/DomainConfig.java b/src/main/java/com/banesco/common/domain/model/DomainConfig.java new file mode 100644 index 0000000..c7f4911 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/model/DomainConfig.java @@ -0,0 +1,13 @@ +package com.banesco.common.domain.model; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@RegisterForReflection +public class DomainConfig { + private String url; + private TimeoutConfig timeout; +} diff --git a/src/main/java/com/banesco/common/domain/model/Either.java b/src/main/java/com/banesco/common/domain/model/Either.java new file mode 100644 index 0000000..a8ff953 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/model/Either.java @@ -0,0 +1,33 @@ +package com.banesco.common.domain.model; + +import lombok.Getter; + +@Getter +public class Either { + private final L left; + private final R right; + + private final boolean leftFlag; + + private Either(L left, R right, boolean leftFlag) { + this.left = left; + this.right = right; + this.leftFlag = leftFlag; + } + + public static Either left(L left) { + return new Either<>(left, null, true); + } + + public static Either right(R right) { + return new Either<>(null, right, false); + } + + public boolean isLeft() { + return leftFlag; + } + + public boolean isRight() { + return !leftFlag; + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/common/domain/model/ErrorMapping.java b/src/main/java/com/banesco/common/domain/model/ErrorMapping.java new file mode 100644 index 0000000..56437fd --- /dev/null +++ b/src/main/java/com/banesco/common/domain/model/ErrorMapping.java @@ -0,0 +1,22 @@ +package com.banesco.common.domain.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Setter +@Getter +@ToString +@RegisterForReflection +public class ErrorMapping { + @JsonProperty("backendCode") + String backendCode; + @JsonProperty("httpCode") + int httpCode; + @JsonProperty("statusCode") + String statusCode; + @JsonProperty("description") + String description; +} diff --git a/src/main/java/com/banesco/common/domain/model/HttpRequest.java b/src/main/java/com/banesco/common/domain/model/HttpRequest.java new file mode 100644 index 0000000..af48ca7 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/model/HttpRequest.java @@ -0,0 +1,223 @@ +package com.banesco.common.domain.model; + +import lombok.*; + +import java.lang.reflect.Type; +import java.util.Map; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class HttpRequest { + private String url; + private HttpMethod method; + private Object body; + private Map headers; + private Map queryParams; + private Map pathParams; + + @Builder.Default + private Class responseType = Object.class; + + private Class genericType; + + private Type complexType; + + private Class errorType; + + private String statusSuccess; + + @Builder.Default + private boolean eitherResponse = false; + + @Builder.Default + private boolean apiPrivateResponse = false; + + @Builder.Default + private boolean apiResponse = false; + + @Builder.Default + private boolean listResponse = false; + + @Builder.Default + private int connectTimeout = 5000; + + @Builder.Default + private int readTimeout = 10000; + + @Builder.Default + private boolean returnFullResponse = false; + + @Builder.Default + private boolean logRequestBody = true; + + @Builder.Default + private boolean logResponseBody = true; + + public static HttpRequest forApiResponse( + String url, + HttpMethod method, + Class dataType + ) { + return HttpRequest.builder() + .url(url) + .method(method) + .responseType(ApiResponse.class) + .genericType(dataType) + .apiResponse(true) + .build(); + } + + public static HttpRequest forApiResponseList( + String url, + HttpMethod method, + Class elementType + ) { + return HttpRequest.builder() + .url(url) + .method(method) + .responseType(ApiResponse.class) + .complexType(TypeBuilder.listOf(elementType)) + .apiResponse(true) + .listResponse(true) + .build(); + } + + public static HttpRequest forApiPrivateResponse( + String url, + String statusSuccess, + HttpMethod method, + Class successType + ) { + return HttpRequest.builder() + .url(url) + .method(method) + .responseType(ApiPrivateResponse.class) + .complexType(TypeBuilder.parametricType( + Either.class, + successType, + ApiPrivateError.class + )) + .apiPrivateResponse(true) + .eitherResponse(true) + .errorType(ApiPrivateError.class) + .statusSuccess(statusSuccess) + .build(); + } + + public static HttpRequest forApiPrivateResponseList( + String url, + String statusSuccess, + HttpMethod method, + Class elementType + ) { + return HttpRequest.builder() + .url(url) + .method(method) + .responseType(ApiPrivateResponse.class) + .complexType(TypeBuilder.parametricType( + Either.class, + TypeBuilder.listOf(elementType), + ApiPrivateError.class + )) + .apiPrivateResponse(true) + .eitherResponse(true) + .listResponse(true) + .errorType(ApiPrivateError.class) + .statusSuccess(statusSuccess) + .build(); + } + + public static HttpRequest forDirectResponse( + String url, + HttpMethod method, + Class responseType + ) { + return HttpRequest.builder() + .url(url) + .method(method) + .responseType(responseType) + .build(); + } + + public static HttpRequest forGenericResponse( + String url, + HttpMethod method, + Class rawType, + Class genericType + ) { + return HttpRequest.builder() + .url(url) + .method(method) + .responseType(rawType) + .complexType(TypeBuilder.parametricType(rawType, genericType)) + .build(); + } + + public HttpRequest withHeaders(Map headers) { + this.headers = headers; + return this; + } + + public HttpRequest withQueryParams(Map queryParams) { + this.queryParams = queryParams; + return this; + } + + public HttpRequest withPathParams(Map pathParams) { + this.pathParams = pathParams; + return this; + } + + public HttpRequest withBody(Object body) { + this.body = body; + return this; + } + + public HttpRequest withTimeout(int connectTimeout, int readTimeout) { + this.connectTimeout = connectTimeout; + this.readTimeout = readTimeout; + return this; + } + + public HttpRequest disableRequestLogging() { + this.logRequestBody = false; + return this; + } + + public HttpRequest disableResponseLogging() { + this.logResponseBody = false; + return this; + } + + public ResponseType getExpectedResponseType() { + if (apiPrivateResponse && eitherResponse) { + return listResponse ? ResponseType.API_PRIVATE_WITH_EITHER_LIST : ResponseType.API_PRIVATE_WITH_EITHER; + } else if (apiResponse) { + return listResponse ? ResponseType.API_RESPONSE_LIST : ResponseType.API_RESPONSE; + } else if (complexType != null) { + return ResponseType.COMPLEX_TYPE; + } else if (genericType != null) { + return ResponseType.GENERIC_TYPE; + } else { + return ResponseType.DIRECT_TYPE; + } + } + + public enum ResponseType { + API_RESPONSE, + API_RESPONSE_LIST, + API_PRIVATE_WITH_EITHER, + API_PRIVATE_WITH_EITHER_LIST, + GENERIC_TYPE, + COMPLEX_TYPE, + DIRECT_TYPE + } + + public enum HttpMethod { + GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/common/domain/model/Identifier.java b/src/main/java/com/banesco/common/domain/model/Identifier.java new file mode 100644 index 0000000..5aeef67 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/model/Identifier.java @@ -0,0 +1,14 @@ +package com.banesco.common.domain.model; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class Identifier { + private String identifierValue; +} diff --git a/src/main/java/com/banesco/common/domain/model/PaymentInstruction.java b/src/main/java/com/banesco/common/domain/model/PaymentInstruction.java new file mode 100644 index 0000000..56243f6 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/model/PaymentInstruction.java @@ -0,0 +1,16 @@ +package com.banesco.common.domain.model; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +import java.util.List; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class PaymentInstruction { + private List amount; // amount, currency +} diff --git a/src/main/java/com/banesco/common/domain/model/StatusResponse.java b/src/main/java/com/banesco/common/domain/model/StatusResponse.java new file mode 100644 index 0000000..8bb4406 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/model/StatusResponse.java @@ -0,0 +1,15 @@ +package com.banesco.common.domain.model; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class StatusResponse { + private String statusCode; + private String message; +} \ No newline at end of file diff --git a/src/main/java/com/banesco/common/domain/model/TimeoutConfig.java b/src/main/java/com/banesco/common/domain/model/TimeoutConfig.java new file mode 100644 index 0000000..5db3277 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/model/TimeoutConfig.java @@ -0,0 +1,13 @@ +package com.banesco.common.domain.model; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@RegisterForReflection +public class TimeoutConfig { + private int connect; + private int response; +} diff --git a/src/main/java/com/banesco/common/domain/model/TypeBuilder.java b/src/main/java/com/banesco/common/domain/model/TypeBuilder.java new file mode 100644 index 0000000..3120444 --- /dev/null +++ b/src/main/java/com/banesco/common/domain/model/TypeBuilder.java @@ -0,0 +1,67 @@ +package com.banesco.common.domain.model; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public class TypeBuilder { + + private TypeBuilder() {} + + public static Type listOf(Class elementType) { + return new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return new Type[]{elementType}; + } + + @Override + public Type getRawType() { + return java.util.List.class; + } + + @Override + public Type getOwnerType() { + return null; + } + }; + } + + public static Type parametricType(Class rawType, Type... typeArguments) { + return new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return typeArguments; + } + + @Override + public Type getRawType() { + return rawType; + } + + @Override + public Type getOwnerType() { + return null; + } + }; + } + + public static Type apiPrivateResponseWithEither(Class successType, Class errorType) { + Type eitherType = parametricType(Either.class, successType, errorType); + return parametricType(ApiPrivateResponse.class, eitherType); + } + + public static Type apiPrivateResponseWithListEither(Class successType, Class errorType) { + Type listType = listOf(successType); + Type eitherType = parametricType(Either.class, listType, errorType); + return parametricType(ApiPrivateResponse.class, eitherType); + } + + public static Type apiResponseType(Class dataType) { + return parametricType(ApiResponse.class, dataType); + } + + public static Type apiResponseListType(Class elementType) { + Type listType = listOf(elementType); + return parametricType(ApiResponse.class, listType); + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/common/infrastructure/config/MessagesConfig.java b/src/main/java/com/banesco/common/infrastructure/config/MessagesConfig.java new file mode 100644 index 0000000..d639e76 --- /dev/null +++ b/src/main/java/com/banesco/common/infrastructure/config/MessagesConfig.java @@ -0,0 +1,32 @@ +package com.banesco.common.infrastructure.config; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import lombok.Getter; +import org.eclipse.microprofile.config.Config; + +@ApplicationScoped +@Getter +public class MessagesConfig { + + private final boolean readFromProps; + private final String errorMessagesJson; + private final String messagesKey; + + private static final String KEY = "bus-create-security-notification"; + + @Inject + public MessagesConfig(Config config) { + this.readFromProps = config.getValue("api.read-messages.from-props", Boolean.class); + this.errorMessagesJson = config.getValue("api." + KEY + ".messages.content", String.class); + this.messagesKey = config.getValue("api." + KEY + ".messages.key", String.class); + } + + public String getErrorMessagesJson() { + if (readFromProps) { + return errorMessagesJson != null ? errorMessagesJson : ""; + } else { + return ""; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/common/infrastructure/config/RestClientConfig.java b/src/main/java/com/banesco/common/infrastructure/config/RestClientConfig.java new file mode 100644 index 0000000..c0e2b81 --- /dev/null +++ b/src/main/java/com/banesco/common/infrastructure/config/RestClientConfig.java @@ -0,0 +1,81 @@ +package com.banesco.common.infrastructure.config; + +import com.banesco.common.domain.model.DomainConfig; +import com.banesco.module.security_trace.domain.model.SecurityTraceConfig; +import com.banesco.module.service_status.domain.model.ServiceStatusConfig; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.config.Config; + +import java.util.Map; + +@Slf4j +@ApplicationScoped +@RegisterForReflection +public class RestClientConfig { + + private final Config config; + private final ObjectMapper objectMapper; + + private static final String API_BASE = "api.rest-client."; + private static final String API_DOMAIN_NAME = "dom-create-security-notification"; + private static final String API_SECURITY_TRACE_NAME = "security-trace"; + private static final String API_SERVICE_STATUS_NAME = "service-status"; + + @Inject + public RestClientConfig( + Config config, + ObjectMapper objectMapper + ) { + this.config = config; + this.objectMapper = objectMapper; + } + + public DomainConfig getDomDocserviceFileConfig() { + return getConfig(API_DOMAIN_NAME, DomainConfig.class); + } + + public SecurityTraceConfig getSecurityTraceConfig() { + return getConfig(API_SECURITY_TRACE_NAME, SecurityTraceConfig.class); + } + + public ServiceStatusConfig getServiceStatusConfig() { + return getConfig(API_SERVICE_STATUS_NAME, ServiceStatusConfig.class); + } + + private T getConfig( + String configName, + Class configType + ) { + try { + String fullConfigName = API_BASE + configName; + String json = config.getValue(fullConfigName, String.class); + log.info("Configurando {}: {}", fullConfigName, json); + + if (json == null || json.trim().isEmpty()) { + throw new IllegalStateException("Configuracion no encontrada para: " + fullConfigName); + } + + Map configMap = objectMapper.readValue(json, new TypeReference<>() {}); + return objectMapper.convertValue(configMap, configType); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Formato JSON invalido para " + configName + ": " + e.getMessage(), e); + } catch (Exception e) { + throw new IllegalStateException("Error cargando configuracion del servicio " + configName + ": " + e.getMessage(), e); + } + } + + public String toJsonString(Object object) { + try { + return objectMapper.writeValueAsString(object); + } catch (Exception e) { + log.error("Error al convertir a json string: {}", e.getMessage()); + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/common/infrastructure/context/RequestContext.java b/src/main/java/com/banesco/common/infrastructure/context/RequestContext.java new file mode 100644 index 0000000..bf29549 --- /dev/null +++ b/src/main/java/com/banesco/common/infrastructure/context/RequestContext.java @@ -0,0 +1,24 @@ +package com.banesco.common.infrastructure.context; + +import org.slf4j.MDC; + +public class RequestContext { + + private RequestContext() {} + + public static final String REQUEST_ID = "requestId"; + public static final String DEVICE = "device"; + public static final String DEVICE_SESSION_REFERENCE = "deviceSessionReference"; + + public static String getRequestId() { + return MDC.get(REQUEST_ID); + } + + public static void setRequestId(String requestId) { + MDC.put(REQUEST_ID, requestId); + } + + public static void clear() { + MDC.remove(REQUEST_ID); + } +} diff --git a/src/main/java/com/banesco/common/infrastructure/filter/RequestIdFilter.java b/src/main/java/com/banesco/common/infrastructure/filter/RequestIdFilter.java new file mode 100644 index 0000000..e14b02b --- /dev/null +++ b/src/main/java/com/banesco/common/infrastructure/filter/RequestIdFilter.java @@ -0,0 +1,104 @@ +package com.banesco.common.infrastructure.filter; + +import com.banesco.common.infrastructure.context.RequestContext; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.ext.Provider; +import lombok.extern.slf4j.Slf4j; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +@Slf4j +@Provider +public class RequestIdFilter implements ContainerRequestFilter, ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext) { + String requestId = requestContext.getHeaderString(RequestContext.DEVICE_SESSION_REFERENCE); + + if (isEmpty(requestId)) { + requestId = requestContext.getUriInfo() + .getQueryParameters() + .getFirst(RequestContext.DEVICE_SESSION_REFERENCE); + } + + if (isEmpty(requestId) && hasJsonBody(requestContext)) { + requestId = extractRequestIdFromBody(requestContext); + } + + if (isEmpty(requestId)) { + requestId = UUID.randomUUID().toString().substring(0, 13); + } + + RequestContext.setRequestId(requestId); + } + + private boolean isEmpty(String value) { + return value == null || value.trim().isEmpty(); + } + + private boolean hasJsonBody(ContainerRequestContext context) { + try { + String method = context.getMethod(); + String contentType = context.getHeaderString("Content-Type"); + + return ("POST".equals(method) || "PUT".equals(method)) + && contentType != null + && contentType.contains("application/json"); + } catch (Exception e) { + log.warn("La peticion no es un POST o PUT: {}", e.getMessage()); + return false; + } + } + + private String extractRequestIdFromBody(ContainerRequestContext context) { + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + context.getEntityStream().transferTo(buffer); + byte[] bodyBytes = buffer.toByteArray(); + + context.setEntityStream(new ByteArrayInputStream(bodyBytes)); + + String bodyString = new String(bodyBytes, StandardCharsets.UTF_8); + io.vertx.core.json.JsonObject jsonObject = new io.vertx.core.json.JsonObject(bodyString); + + if (jsonObject.containsKey(RequestContext.DEVICE)) { + io.vertx.core.json.JsonObject device = jsonObject.getJsonObject(RequestContext.DEVICE); + + if (device.containsKey(RequestContext.DEVICE_SESSION_REFERENCE)) { + return device.getString(RequestContext.DEVICE_SESSION_REFERENCE); + } + } + + if (jsonObject.containsKey(RequestContext.REQUEST_ID)) { + return jsonObject.getString(RequestContext.REQUEST_ID); + } + + if (jsonObject.containsKey(RequestContext.DEVICE_SESSION_REFERENCE)) { + return jsonObject.getString(RequestContext.DEVICE_SESSION_REFERENCE); + } + + return null; + } catch (Exception e) { + log.error("Error extrayendo el requestId del cuerpo de la peticion: {}", e.getMessage()); + return null; + } + } + + @Override + public void filter( + ContainerRequestContext requestContext, + ContainerResponseContext responseContext + ) { + try { + RequestContext.clear(); + } catch (Exception e) { + log.error("Error limpiando el filtro: {}", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/module/correspondence/application/service/CorrespondenceService.java b/src/main/java/com/banesco/module/correspondence/application/service/CorrespondenceService.java new file mode 100644 index 0000000..fdfee57 --- /dev/null +++ b/src/main/java/com/banesco/module/correspondence/application/service/CorrespondenceService.java @@ -0,0 +1,144 @@ +package com.banesco.module.correspondence.application.service; + +import com.banesco.common.application.helper.MessageHelper; +import com.banesco.common.domain.exception.HttpStatusCodeException; +import com.banesco.common.domain.model.ApiResponse; +import com.banesco.common.infrastructure.context.RequestContext; +import com.banesco.module.correspondence.application.usecase.DomainUseCase; +import com.banesco.module.correspondence.application.usecase.CorrespondenceUseCase; +import com.banesco.module.correspondence.domain.dto.request.CorrespondenceRequest; +import com.banesco.module.correspondence.domain.dto.response.CorrespondenceResponse; +import com.banesco.module.security_trace.application.usecase.SecurityTraceUseCase; +import com.banesco.module.security_trace.domain.dto.response.SecurityTraceResponse; +import com.banesco.module.service_status.application.usecase.ServiceStatusUseCase; +import com.banesco.module.service_status.domain.dto.request.ServiceStatusRequest; +import com.banesco.module.service_status.domain.dto.response.ServiceStatusResponse; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Response; +import lombok.extern.slf4j.Slf4j; + +import java.util.Objects; + +@Slf4j +@ApplicationScoped +public class CorrespondenceService implements CorrespondenceUseCase { + + private final MessageHelper messageHelper; + private final ServiceStatusUseCase serviceStatusUseCase; + private final SecurityTraceUseCase securityTraceUseCase; + private final DomainUseCase domainUseCase; + + @Inject + public CorrespondenceService( + MessageHelper messageHelper, + ServiceStatusUseCase serviceStatusUseCase, + SecurityTraceUseCase securityTraceUseCase, + DomainUseCase domainUseCase + ) { + this.messageHelper = messageHelper; + this.serviceStatusUseCase = serviceStatusUseCase; + this.securityTraceUseCase = securityTraceUseCase; + this.domainUseCase = domainUseCase; + } + + @Override + public Response execute( + CorrespondenceRequest request + ) { + log.info("Iniciando ejecucion para el archivo: {}", request.getRecipientId()); + + Response response = null; + long startTime = System.currentTimeMillis(); + + try { + if(!isServiceStatusActive(request)) { + log.info("Estatus del servicio no disponible: {}", request.getRecipientId()); + throw HttpStatusCodeException.serviceUnavailable("VRN04"); + } + + ApiResponse apiResponse = domain(request); + + response = messageHelper.handleSuccess( + apiResponse.getData(), + apiResponse.getStatusResponse().getStatusCode() + ); + } catch (HttpStatusCodeException e) { + log.error("Excepcion HTTP del api de dominio: {} - {}", e.getStatusCode(), e.getErrorCode()); + response = messageHelper.handleException(e); + } catch (Exception e) { + log.error("Excepcion generica del api de dominio: {}", e.getMessage()); + response = messageHelper.handleGenericException(e); + } finally { + long endTime = System.currentTimeMillis(); + securityTrace(request, response, startTime, endTime); + } + + return response; + } + + private ApiResponse domain( + CorrespondenceRequest request + ) { + log.info("Ejecutando llamada al api de dominio: {}", request.getRecipientId()); + return domainUseCase.execute(request, CorrespondenceResponse.class); + } + + private boolean isServiceStatusActive( + CorrespondenceRequest request + ) { + log.info("Ejecutando llamada al api de la consulta del estatus del servicio"); + + boolean isServiceActive = false; + + try { + ServiceStatusResponse response = serviceStatusUseCase.execute( + ServiceStatusRequest.builder() + .applicationId(request.getAppId()) + .transactionId(RequestContext.getRequestId()) + .build(), + ServiceStatusResponse.class + ); + + isServiceActive = Objects.equals(response.getStatus(), "A"); + } catch (HttpStatusCodeException e) { + log.info( + "Error HTTP al ejecutar la consulta del estatus del servicio: {} -> {}", + e.getStatusCode(), + e.getMessage() + ); + } catch (Exception e) { + log.info( + "Error al ejecutar la consulta del estatus del servicio: {}", + e.getMessage() + ); + } + + return isServiceActive; + } + + private void securityTrace( + CorrespondenceRequest request, + Response response, + long startTime, + long endTime + ) { + log.info("Ejecutando llamada al api de la traza de seguridad"); + + try { + securityTraceUseCase.execute( + request, response, + startTime, endTime, + SecurityTraceResponse.class + ); + } catch (HttpStatusCodeException e) { + log.info( + "Error HTTP al ejecutar la traza de seguridad: {} -> {}", + e.getStatusCode(), + e.getMessage() + ); + } catch (Exception e) { + log.info("Error al ejecutar la traza de seguridad: {}", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/module/correspondence/application/usecase/CorrespondenceUseCase.java b/src/main/java/com/banesco/module/correspondence/application/usecase/CorrespondenceUseCase.java new file mode 100644 index 0000000..33eda0d --- /dev/null +++ b/src/main/java/com/banesco/module/correspondence/application/usecase/CorrespondenceUseCase.java @@ -0,0 +1,10 @@ +package com.banesco.module.correspondence.application.usecase; + +import com.banesco.module.correspondence.domain.dto.request.CorrespondenceRequest; +import jakarta.ws.rs.core.Response; + +public interface CorrespondenceUseCase { + Response execute( + CorrespondenceRequest request + ); +} diff --git a/src/main/java/com/banesco/module/correspondence/application/usecase/DomainUseCase.java b/src/main/java/com/banesco/module/correspondence/application/usecase/DomainUseCase.java new file mode 100644 index 0000000..18f6b41 --- /dev/null +++ b/src/main/java/com/banesco/module/correspondence/application/usecase/DomainUseCase.java @@ -0,0 +1,11 @@ +package com.banesco.module.correspondence.application.usecase; + +import com.banesco.common.domain.model.ApiResponse; +import com.banesco.module.correspondence.domain.dto.request.CorrespondenceRequest; + +public interface DomainUseCase { + ApiResponse execute( + CorrespondenceRequest params, + Class responseType + ); +} diff --git a/src/main/java/com/banesco/module/correspondence/domain/dto/request/CorrespondenceRequest.java b/src/main/java/com/banesco/module/correspondence/domain/dto/request/CorrespondenceRequest.java new file mode 100644 index 0000000..5798dfa --- /dev/null +++ b/src/main/java/com/banesco/module/correspondence/domain/dto/request/CorrespondenceRequest.java @@ -0,0 +1,128 @@ +package com.banesco.module.correspondence.domain.dto.request; + +import com.banesco.common.domain.model.Device; +import com.banesco.common.domain.model.PaymentInstruction; +import com.banesco.common.infrastructure.context.RequestContext; +import com.banesco.module.correspondence.domain.model.CorrespondenceOutbound; +import com.banesco.module.instruction.domain.model.Instruction; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.netty.util.internal.StringUtil; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +import java.math.BigDecimal; +import java.util.Objects; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class CorrespondenceRequest { + @NonNull + private String customerReferenceFintechId; + @NonNull + private String appId; + @NonNull + private CorrespondenceOutbound correspondenceOutbound; + private PaymentInstruction paymentInstruction; + @NonNull + private Instruction procedureRequest; + @NonNull + private Device device; + + @JsonIgnore + public String getCodeType() { + return correspondenceOutbound + .getSourceReference(); + } + + @JsonIgnore + public String getRecipientId() { + return correspondenceOutbound + .getCustomerReference() + .getPartyIdentification() + .get(0) + .getPartyIdentification() + .getIdentifierValue(); + } + + @JsonIgnore + public BigDecimal getAmount() { + if( + Objects.isNull(paymentInstruction) || + Objects.isNull(paymentInstruction.getAmount()) || + paymentInstruction.getAmount().isEmpty() + ) { + return null; + } + + return paymentInstruction + .getAmount() + .get(0) + .getValue(); + } + + @JsonIgnore + public String getCurrency() { + if( + Objects.isNull(paymentInstruction) || + Objects.isNull(paymentInstruction.getAmount()) || + paymentInstruction.getAmount().isEmpty() + ) { + return null; + } + + return paymentInstruction + .getAmount() + .get(0) + .getCurrencyCode(); + } + + @JsonIgnore + public String getExpirationDate() { + return correspondenceOutbound + .getDate(); + } + + @JsonIgnore + public Long getExpirationTime() { + return correspondenceOutbound + .getTime(); + } + + @JsonIgnore + public String getChannelCode() { + return procedureRequest + .getInstructionPurposeType() + .name(); + } + + @JsonIgnore + public static CorrespondenceRequest fromResource( + String customerReferenceFintechId, + String appId, + CorrespondenceRequest request + ) { + return CorrespondenceRequest.builder() + .customerReferenceFintechId(customerReferenceFintechId) + .appId(appId) + .correspondenceOutbound(request.getCorrespondenceOutbound()) + .paymentInstruction(request.getPaymentInstruction()) + .procedureRequest(request.getProcedureRequest()) + .device( + Device.builder() + .deviceType(request.getDevice().getDeviceType()) + .deviceDescription(request.getDevice().getDeviceDescription()) + .deviceIp(request.getDevice().getDeviceIp()) + .deviceSessionReference( + (!StringUtil.isNullOrEmpty(request.getDevice().getDeviceSessionReference())) + ? request.getDevice().getDeviceSessionReference() + : RequestContext.getRequestId() + ) + .build() + ) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/module/correspondence/domain/dto/response/CorrespondenceResponse.java b/src/main/java/com/banesco/module/correspondence/domain/dto/response/CorrespondenceResponse.java new file mode 100644 index 0000000..9330f18 --- /dev/null +++ b/src/main/java/com/banesco/module/correspondence/domain/dto/response/CorrespondenceResponse.java @@ -0,0 +1,18 @@ +package com.banesco.module.correspondence.domain.dto.response; + +import com.banesco.module.correspondence.domain.model.CorrespondenceTransaction; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +@Schema(description = "Respuesta del envio de la notificación al cliente") +public class CorrespondenceResponse { + @Schema(description = "Instancia de la informacion de la notificacion") + private CorrespondenceTransaction correspondenceTransaction; +} diff --git a/src/main/java/com/banesco/module/correspondence/domain/model/CorrespondenceOutbound.java b/src/main/java/com/banesco/module/correspondence/domain/model/CorrespondenceOutbound.java new file mode 100644 index 0000000..340fad7 --- /dev/null +++ b/src/main/java/com/banesco/module/correspondence/domain/model/CorrespondenceOutbound.java @@ -0,0 +1,18 @@ +package com.banesco.module.correspondence.domain.model; + +import com.banesco.module.party.domain.model.Party; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class CorrespondenceOutbound { + private String sourceReference; // codeAlert + private Party customerReference; // recipientIdentification + private String date; // expirationDate + private Long time; // expirationTime +} diff --git a/src/main/java/com/banesco/module/correspondence/domain/model/CorrespondenceTransaction.java b/src/main/java/com/banesco/module/correspondence/domain/model/CorrespondenceTransaction.java new file mode 100644 index 0000000..d59c12b --- /dev/null +++ b/src/main/java/com/banesco/module/correspondence/domain/model/CorrespondenceTransaction.java @@ -0,0 +1,15 @@ +package com.banesco.module.correspondence.domain.model; + +import com.banesco.module.transaction.domain.model.Transaction; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class CorrespondenceTransaction { + private Transaction transaction; // id, dateCreate +} diff --git a/src/main/java/com/banesco/module/correspondence/infrastructure/client/DomCorrespondenceClient.java b/src/main/java/com/banesco/module/correspondence/infrastructure/client/DomCorrespondenceClient.java new file mode 100644 index 0000000..858c842 --- /dev/null +++ b/src/main/java/com/banesco/module/correspondence/infrastructure/client/DomCorrespondenceClient.java @@ -0,0 +1,85 @@ +package com.banesco.module.correspondence.infrastructure.client; + +import com.banesco.common.application.usecase.HttpClientUseCase; +import com.banesco.common.domain.exception.HttpApiResponseException; +import com.banesco.common.domain.exception.HttpStatusCodeException; +import com.banesco.common.domain.model.ApiResponse; +import com.banesco.common.domain.model.DomainConfig; +import com.banesco.common.domain.model.HttpRequest; +import com.banesco.common.infrastructure.config.RestClientConfig; +import com.banesco.module.correspondence.application.usecase.DomainUseCase; +import com.banesco.module.correspondence.domain.dto.request.CorrespondenceRequest; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@ApplicationScoped +public class DomCorrespondenceClient implements DomainUseCase { + private final HttpClientUseCase httpClientUseCase; + private final DomainConfig domainConfig; + + @Inject + public DomCorrespondenceClient( + HttpClientUseCase httpClientUseCase, + RestClientConfig restClientConfig + ) { + this.httpClientUseCase = httpClientUseCase; + this.domainConfig = restClientConfig.getDomDocserviceFileConfig(); + log.info("Configuracion cargada para bus-create-security-notification: {}", domainConfig); + } + + @Override + public ApiResponse execute( + CorrespondenceRequest params, + Class responseType + ) { + String recipientId = params.getRecipientId(); + HttpRequest request = HttpRequest.forApiResponse( + domainConfig.getUrl(), + HttpRequest.HttpMethod.POST, + responseType + ) + .withBody(params) + .withTimeout( + domainConfig.getTimeout().getConnect(), + domainConfig.getTimeout().getResponse() + ); + + log.debug("Request configurado: {}", request); + + try { + ApiResponse response = httpClientUseCase.execute(request); + + log.info( + "Solicitud del api de dominio exitoso: {}", + response.getStatusResponse() + ); + + return response; + } catch (HttpApiResponseException e) { + log.error( + "Error HTTP con ApiResponse consultando transaccion {}: {}", + recipientId, + e.getApiResponse() + ); + + throw new HttpStatusCodeException( + e.getStatusCode(), + e.getApiResponse().getStatusResponse().getStatusCode() + ); + } catch (HttpStatusCodeException e) { + log.error( + "Error HTTP consultando transaccion {}: {} - {}", + recipientId, + e.getStatusCode(), + e.getMessage() + ); + + throw e; + } catch (Exception e) { + log.error("Error consultando transaccion {}: {}", recipientId, e.getMessage()); + throw HttpStatusCodeException.serviceUnavailable("503"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/module/correspondence/infrastructure/resource/CorrespondenceResource.java b/src/main/java/com/banesco/module/correspondence/infrastructure/resource/CorrespondenceResource.java new file mode 100644 index 0000000..c231482 --- /dev/null +++ b/src/main/java/com/banesco/module/correspondence/infrastructure/resource/CorrespondenceResource.java @@ -0,0 +1,271 @@ +package com.banesco.module.correspondence.infrastructure.resource; + +import com.banesco.common.domain.model.ApiResponse; +import com.banesco.common.domain.model.StatusResponse; +import com.banesco.module.correspondence.application.usecase.CorrespondenceUseCase; +import com.banesco.module.correspondence.domain.dto.request.CorrespondenceRequest; +import com.banesco.module.correspondence.domain.dto.response.CorrespondenceResponse; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.ExampleObject; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.media.SchemaProperty; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; + +@Slf4j +@Path("/correspondence") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class CorrespondenceResource { + + private final CorrespondenceUseCase useCase; + + @Inject + public CorrespondenceResource( + CorrespondenceUseCase useCase + ) { + this.useCase = useCase; + } + + @POST + @Path("/initiate") + @Operation( + summary = "Genera una alerta de seguridad", + description = "Envía una notificación de seguridad al cliente" + ) + @APIResponses(value = { + @APIResponse( + responseCode = "200", + description = "Operacion exitosa", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ApiResponse.class, + properties = { + @SchemaProperty( + name = "data", + oneOf = {CorrespondenceResponse.class} + ), + @SchemaProperty( + name = "statusResponse", + implementation = StatusResponse.class + ) + } + ), + examples = @ExampleObject( + name = "Ejemplo exitoso", + value = """ + { + "data": { + "correspondenceTransaction": { + "transaction": { + "transactionIdentification": { + "identifierValue": "1" + }, + "transactionDate": [ + { + "dateType": "INITIATED_DATE", + "date": "16/01/2026 11:32:52" + } + ] + } + } + }, + "statusResponse": { + "statusCode": "200", + "message": "Operacion exitosa" + } + } + """ + ) + ) + ), + @APIResponse( + responseCode = "400", + description = "Error en formato o campo requerido", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ApiResponse.class, + properties = { + @SchemaProperty( + name = "data", + oneOf = {CorrespondenceResponse.class} + ), + @SchemaProperty( + name = "statusResponse", + implementation = StatusResponse.class + ) + } + ), + examples = { + @ExampleObject( + name = "Error VDE01 - Campo obligatorio", + value = """ + { + "data": null, + "statusResponse": { + "statusCode": "VDE01", + "message": "El campo document.documentName es obligatorio" + } + } + """ + ), + @ExampleObject( + name = "Error VDE02 - Valor no permitido", + value = """ + { + "data": null, + "statusResponse": { + "statusCode": "VDE02", + "message": "El valor 'XYZ' no es permitido para el campo channelCode. Valores permitidos: BOL" + } + } + """ + ) + } + ) + ), + @APIResponse( + responseCode = "401", + description = "No autorizado", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ApiResponse.class, + properties = { + @SchemaProperty( + name = "data", + oneOf = {CorrespondenceResponse.class} + ), + @SchemaProperty( + name = "statusResponse", + implementation = StatusResponse.class + ) + } + ), + examples = { + @ExampleObject( + name = "Error VRN08 - Identificacion del fintechId no coincide", + value = """ + { + "data": null, + "statusResponse": { + "statusCode": "VRN08", + "message": "El fintechId proporcionado no coincide con el registrado para este cliente" + } + } + """ + ) + } + ) + ), + @APIResponse( + responseCode = "503", + description = "Servicio no disponible", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ApiResponse.class, + properties = { + @SchemaProperty( + name = "data", + oneOf = {CorrespondenceResponse.class} + ), + @SchemaProperty( + name = "statusResponse", + implementation = StatusResponse.class + ) + } + ), + examples = { + @ExampleObject( + name = "Error VRN04 - Fuera de horario", + value = """ + { + "data": null, + "statusResponse": { + "statusCode": "VRN04", + "message": "El servicio no esta disponible fuera del horario operativo" + } + } + """ + ), + @ExampleObject( + name = "Error VDR13 - OSB no disponible", + value = """ + { + "data": null, + "statusResponse": { + "statusCode": "VDR13", + "message": "El servicio OSB no esta disponible en este momento" + } + } + """ + ) + } + ) + ) + }) + public Response initiate( + @RequestBody( + description = "Request de la solicitud del pago del cliente", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = CorrespondenceRequest.class), + examples = @ExampleObject( + name = "Ejemplo de request", + value = """ + { + "customerReferenceFintechId": "provider-test", + "appId": "DANIAPP", + "correspondenceOutbound": { + "sourceReference": "TRANSATIP", + "customerReference": { + "partyIdentification": [ + { + "partyIdentification": { + "identifierValue": "V20132859" + } + } + ] + }, + "date": "2025-12-31", + "time": 0 + }, + "paymentInstruction": { + "amount": [ + { + "value": 1000.00, + "currencyCode": "VES" + } + ] + }, + "procedureRequest": { + "instructionPurposeType": "consolaPredictiva" + }, + "device": { + "deviceType": "Mobile", + "deviceDescription": "Xiaomi Note 11 PRO", + "deviceIp": "127.0.0.1", + "deviceSessionReference": "12345678901304" + } + } + """ + ) + ) + ) + CorrespondenceRequest request + ) { + log.info("Iniciando consulta para instruccion del cliente: {}", request.getRecipientId()); + + return useCase.execute(request); + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/module/instruction/domain/model/Instruction.java b/src/main/java/com/banesco/module/instruction/domain/model/Instruction.java new file mode 100644 index 0000000..d28b7ed --- /dev/null +++ b/src/main/java/com/banesco/module/instruction/domain/model/Instruction.java @@ -0,0 +1,16 @@ +package com.banesco.module.instruction.domain.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Instruction { + private InstructionPurposeType instructionPurposeType; // Request JSON: "channelOrigin" (BOL) +} diff --git a/src/main/java/com/banesco/module/instruction/domain/model/InstructionPurposeType.java b/src/main/java/com/banesco/module/instruction/domain/model/InstructionPurposeType.java new file mode 100644 index 0000000..fce3ad3 --- /dev/null +++ b/src/main/java/com/banesco/module/instruction/domain/model/InstructionPurposeType.java @@ -0,0 +1,5 @@ +package com.banesco.module.instruction.domain.model; + +public enum InstructionPurposeType { + PREDICTIVE_CONSOLE, +} diff --git a/src/main/java/com/banesco/module/party/domain/model/Party.java b/src/main/java/com/banesco/module/party/domain/model/Party.java new file mode 100644 index 0000000..b413901 --- /dev/null +++ b/src/main/java/com/banesco/module/party/domain/model/Party.java @@ -0,0 +1,16 @@ +package com.banesco.module.party.domain.model; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +import java.util.List; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class Party { + private List partyIdentification; +} diff --git a/src/main/java/com/banesco/module/party/domain/model/PartyIdentification.java b/src/main/java/com/banesco/module/party/domain/model/PartyIdentification.java new file mode 100644 index 0000000..567169b --- /dev/null +++ b/src/main/java/com/banesco/module/party/domain/model/PartyIdentification.java @@ -0,0 +1,15 @@ +package com.banesco.module.party.domain.model; + +import com.banesco.common.domain.model.Identifier; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class PartyIdentification { + private Identifier partyIdentification; +} diff --git a/src/main/java/com/banesco/module/security_trace/application/usecase/SecurityTraceUseCase.java b/src/main/java/com/banesco/module/security_trace/application/usecase/SecurityTraceUseCase.java new file mode 100644 index 0000000..4a3912f --- /dev/null +++ b/src/main/java/com/banesco/module/security_trace/application/usecase/SecurityTraceUseCase.java @@ -0,0 +1,14 @@ +package com.banesco.module.security_trace.application.usecase; + +import com.banesco.module.correspondence.domain.dto.request.CorrespondenceRequest; +import jakarta.ws.rs.core.Response; + +public interface SecurityTraceUseCase { + T execute( + CorrespondenceRequest apiRequest, + Response apiResponse, + long startTime, + long endTime, + Class responseType + ); +} diff --git a/src/main/java/com/banesco/module/security_trace/domain/dto/request/SecurityTraceRequest.java b/src/main/java/com/banesco/module/security_trace/domain/dto/request/SecurityTraceRequest.java new file mode 100644 index 0000000..355d7ab --- /dev/null +++ b/src/main/java/com/banesco/module/security_trace/domain/dto/request/SecurityTraceRequest.java @@ -0,0 +1,134 @@ +package com.banesco.module.security_trace.domain.dto.request; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +import java.math.BigDecimal; +import java.util.Date; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SecurityTraceRequest { + private String codBan; + private String codMon; + private String codEve; + private String codEve2; + private String login; + private Date fecHor; + private String nacCli; + private Integer cedRifCli; + private String tipoProductoCli; + private String tipoProductoBen; + private String productoCli; + private String codEmpresa; + private String nacBen; + private Integer cedBen; + private String nombreBen; + private String productoBen; + private BigDecimal monto; + private String referencia; + private String nroDePago; + private String desPago; + private String objeto; + private Integer tipoRespuesta; + private String msgRespuesta; + private Long tiempoRespuesta; + private String codFintech; + private String cedRifFintech; + private String nombreFintech; + private String tipoDispositivo; + private String desDispositivo; + private String ipCli; + private String refInternacional; + private BigDecimal montoReceptor; + private String refBanco; + private String stsTransaccion; + private String tipoMonedaEmi; + private String tipoMonedaRec; + private String paisEmisor; + private String paisReceptor; + private BigDecimal montoEmisor; + private String numeroCuentaEmi; + private String numeroCuentaRec; + private String numeroTelefonoRec; + private String propositoTransaccion; + private BigDecimal montoComision; + private String tipoIdEmisor; + private String idEmisor; + private BigDecimal numeroTarjeta; + private Integer codigoAutoriza; + private String refUniversal; + private String fechaAprobacion; + private String refBanesco; + private String fecHorCarga; + private String tipoProductoEmisor; + private String ctaEmisor; + private String tipoProductoReceptor; + private String ctaReceptor; + private String idReceptor; + private String nombreReceptor; + private String refExterna; + private String origenFondos; + private String destinoFondos; + private String fecHorTransaccion; + private String metodoPagoEmisor; + private String valorMetodoPagoEmisor; + private String entFinancieraEmisor; + private String nombreEmisor; + private String monedaEmisor; + private String metodoPagoReceptor; + private String valorMetodoPagoReceptor; + private String entFinancieraReceptor; + private String monedaReceptor; + private String descPago; + private String descTransaccion; + private BigDecimal tasaComision; + private BigDecimal numTarjeta; + private String codAutorizacion; + private String fecHorAprobacion; + private BigDecimal montoIgtf; + private BigDecimal montoLbtr; + private BigDecimal tasaCambio; + private BigDecimal montoTotalBs; + private String sp; + private String cn; + private String OU; + private String distinguishedName; + private String userPassword; + private Integer Enabled; + private String givenName; + private String sn; + private String mail; + private String employeeNumber; + private String employeeID; + private String mobile; + private String homePhone; + private String company; + private String personalTitle; + private String title; + private String description; + private String departament; + private String otherMobile; + private String otherHomePhone; + private String telephoneNumber; + private String facsimileTelephoneNumber; + private String postalAddress; + private String postalCode; + private String st; + private String owner; + private String serialNumber; + private String usuarioMaster; + private Integer lockoutTime; + private Integer lockoutDuration; + private Integer lockoutThreshold; + private Integer badPwdCount; + private String badPasswordTime; + private String oU; + private Integer enabled; +} \ No newline at end of file diff --git a/src/main/java/com/banesco/module/security_trace/domain/dto/response/SecurityTraceResponse.java b/src/main/java/com/banesco/module/security_trace/domain/dto/response/SecurityTraceResponse.java new file mode 100644 index 0000000..1adb9d3 --- /dev/null +++ b/src/main/java/com/banesco/module/security_trace/domain/dto/response/SecurityTraceResponse.java @@ -0,0 +1,14 @@ +package com.banesco.module.security_trace.domain.dto.response; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class SecurityTraceResponse { + private String msg; +} diff --git a/src/main/java/com/banesco/module/security_trace/domain/model/SecurityTraceConfig.java b/src/main/java/com/banesco/module/security_trace/domain/model/SecurityTraceConfig.java new file mode 100644 index 0000000..42a1660 --- /dev/null +++ b/src/main/java/com/banesco/module/security_trace/domain/model/SecurityTraceConfig.java @@ -0,0 +1,16 @@ +package com.banesco.module.security_trace.domain.model; + +import com.banesco.common.domain.model.TimeoutConfig; +import com.banesco.module.security_trace.domain.dto.request.SecurityTraceRequest; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@RegisterForReflection +public class SecurityTraceConfig { + private String url; + private TimeoutConfig timeout; + private SecurityTraceRequest request; +} \ No newline at end of file diff --git a/src/main/java/com/banesco/module/security_trace/domain/model/SecurityTraceObject.java b/src/main/java/com/banesco/module/security_trace/domain/model/SecurityTraceObject.java new file mode 100644 index 0000000..22e2e7a --- /dev/null +++ b/src/main/java/com/banesco/module/security_trace/domain/model/SecurityTraceObject.java @@ -0,0 +1,20 @@ +package com.banesco.module.security_trace.domain.model; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +@JsonPropertyOrder({"RQ", "RS"}) +@JsonNaming(PropertyNamingStrategies.UpperSnakeCaseStrategy.class) +public class SecurityTraceObject { + private Object RQ; + private Object RS; +} diff --git a/src/main/java/com/banesco/module/security_trace/infrastructure/client/SecurityTraceClient.java b/src/main/java/com/banesco/module/security_trace/infrastructure/client/SecurityTraceClient.java new file mode 100644 index 0000000..205298c --- /dev/null +++ b/src/main/java/com/banesco/module/security_trace/infrastructure/client/SecurityTraceClient.java @@ -0,0 +1,161 @@ +package com.banesco.module.security_trace.infrastructure.client; + +import com.banesco.common.application.usecase.HttpClientUseCase; +import com.banesco.common.domain.exception.HttpStatusCodeException; +import com.banesco.common.domain.model.ApiResponse; +import com.banesco.common.domain.model.HttpRequest; +import com.banesco.common.infrastructure.config.RestClientConfig; +import com.banesco.module.correspondence.domain.dto.request.CorrespondenceRequest; +import com.banesco.module.security_trace.application.usecase.SecurityTraceUseCase; +import com.banesco.module.security_trace.domain.dto.request.SecurityTraceRequest; +import com.banesco.module.security_trace.domain.model.SecurityTraceConfig; +import com.banesco.module.security_trace.domain.model.SecurityTraceObject; +import io.netty.util.internal.StringUtil; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Response; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; + +@Slf4j +@ApplicationScoped +public class SecurityTraceClient implements SecurityTraceUseCase { + private final HttpClientUseCase httpClientUseCase; + private final RestClientConfig restClientConfig; + private final SecurityTraceConfig securityTraceConfig; + + @Inject + public SecurityTraceClient( + HttpClientUseCase httpClientUseCase, + RestClientConfig restClientConfig + ) { + this.httpClientUseCase = httpClientUseCase; + this.restClientConfig = restClientConfig; + this.securityTraceConfig = restClientConfig.getSecurityTraceConfig(); + log.info("Configuracion cargada para security-trace: {}", securityTraceConfig); + } + + @Override + public T execute( + CorrespondenceRequest apiRequest, + Response apiResponse, + long startTime, + long endTime, + Class responseType + ) { + SecurityTraceRequest body = getRequest( + apiRequest, apiResponse, + startTime, endTime + ); + HttpRequest request = HttpRequest.forDirectResponse( + securityTraceConfig.getUrl(), + HttpRequest.HttpMethod.POST, + responseType + ) + .withBody(body) + .withTimeout( + securityTraceConfig.getTimeout().getConnect(), + securityTraceConfig.getTimeout().getResponse() + ); + + log.debug("Request configurado: {}", request); + + try { + T response = httpClientUseCase.execute(request); + + log.info( + "Solicitud de traza de seguridad exitosa: {}", + response.toString().contains("msg=Message sende") + ); + + return response; + } catch (HttpStatusCodeException e) { + log.error( + "Error HTTP ejecutando traza de seguridad: {} - {}", + e.getStatusCode(), + e.getMessage() + ); + + throw e; + } catch (Exception e) { + log.error("Error ejecutando traza de seguridad: {}", e.getMessage()); + throw HttpStatusCodeException.serviceUnavailable("503"); + } + } + + private SecurityTraceRequest getRequest( + CorrespondenceRequest apiRequest, + Response apiResponse, + long startTime, + long endTime + ) { + long executedTime = (endTime - startTime); + int statusHttp = apiResponse.getStatus(); + String recipientId = apiRequest.getRecipientId(); + ApiResponse apiResponseData = (ApiResponse) apiResponse.getEntity(); + + return SecurityTraceRequest.builder() + .sp(securityTraceConfig.getRequest().getSp()) + .codBan(securityTraceConfig.getRequest().getCodBan()) + .codMon(securityTraceConfig.getRequest().getCodMon()) + .codEve(securityTraceConfig.getRequest().getCodEve()) + .codEve2(securityTraceConfig.getRequest().getCodEve2()) + .login(apiRequest.getChannelCode()) + .fecHor(new Date()) + .nacCli(recipientId.substring(0, 1).toUpperCase()) + .cedRifCli(getPartyIdNumber(recipientId.substring(1))) + .monto(apiRequest.getAmount()) + .objeto( + restClientConfig.toJsonString(SecurityTraceObject.builder() + .RQ(apiRequest) + .RS(apiResponseData) + .build()) + ) + .tipoRespuesta(statusHttp) + .msgRespuesta(getStatusMessage(apiResponseData, statusHttp)) + .tiempoRespuesta(executedTime) + .codFintech(apiRequest.getCustomerReferenceFintechId()) + .nombreFintech(apiRequest.getChannelCode()) + .tipoDispositivo(apiRequest.getDevice().getDeviceType()) + .desDispositivo(apiRequest.getDevice().getDeviceDescription()) + .ipCli(apiRequest.getDevice().getDeviceIp()) + .build(); + } + + private String getStatusMessage( + ApiResponse apiResponseData, + int statusHttp + ) { + if (apiResponseData != null && apiResponseData.getStatusResponse() != null) { + String statusCode = apiResponseData.getStatusResponse().getStatusCode(); + String message = apiResponseData.getStatusResponse().getMessage(); + + if(StringUtil.isNullOrEmpty(statusCode)) { + statusCode = String.valueOf(statusHttp); + } + + return statusCode + "-" + message; + } + + return null; + } + + private Integer getPartyIdNumber(String partyId) { + if (partyId == null || partyId.trim().isEmpty()) { + return null; + } + + String digits = partyId.replaceAll("\\D", ""); + + if (digits.isEmpty()) { + return null; + } + + try { + return Integer.parseInt(digits); + } catch (NumberFormatException e) { + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/module/service_status/application/usecase/ServiceStatusUseCase.java b/src/main/java/com/banesco/module/service_status/application/usecase/ServiceStatusUseCase.java new file mode 100644 index 0000000..4e6b537 --- /dev/null +++ b/src/main/java/com/banesco/module/service_status/application/usecase/ServiceStatusUseCase.java @@ -0,0 +1,10 @@ +package com.banesco.module.service_status.application.usecase; + +import com.banesco.module.service_status.domain.dto.request.ServiceStatusRequest; + +public interface ServiceStatusUseCase { + T execute( + ServiceStatusRequest params, + Class responseType + ); +} diff --git a/src/main/java/com/banesco/module/service_status/domain/dto/request/ServiceStatusRequest.java b/src/main/java/com/banesco/module/service_status/domain/dto/request/ServiceStatusRequest.java new file mode 100644 index 0000000..db697dd --- /dev/null +++ b/src/main/java/com/banesco/module/service_status/domain/dto/request/ServiceStatusRequest.java @@ -0,0 +1,19 @@ +package com.banesco.module.service_status.domain.dto.request; + +import com.banesco.common.domain.model.BankService; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ServiceStatusRequest { + private String applicationId; + private String transactionId; + private BankService bankService; +} diff --git a/src/main/java/com/banesco/module/service_status/domain/dto/response/ServiceStatusResponse.java b/src/main/java/com/banesco/module/service_status/domain/dto/response/ServiceStatusResponse.java new file mode 100644 index 0000000..624b220 --- /dev/null +++ b/src/main/java/com/banesco/module/service_status/domain/dto/response/ServiceStatusResponse.java @@ -0,0 +1,14 @@ +package com.banesco.module.service_status.domain.dto.response; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class ServiceStatusResponse { + private String status; +} diff --git a/src/main/java/com/banesco/module/service_status/domain/model/ServiceStatusConfig.java b/src/main/java/com/banesco/module/service_status/domain/model/ServiceStatusConfig.java new file mode 100644 index 0000000..b9cef33 --- /dev/null +++ b/src/main/java/com/banesco/module/service_status/domain/model/ServiceStatusConfig.java @@ -0,0 +1,16 @@ +package com.banesco.module.service_status.domain.model; + +import com.banesco.common.domain.model.TimeoutConfig; +import com.banesco.module.service_status.domain.dto.request.ServiceStatusRequest; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@RegisterForReflection +public class ServiceStatusConfig { + private String url; + private TimeoutConfig timeout; + private ServiceStatusRequest request; +} \ No newline at end of file diff --git a/src/main/java/com/banesco/module/service_status/infrastructure/client/ServiceStatusClient.java b/src/main/java/com/banesco/module/service_status/infrastructure/client/ServiceStatusClient.java new file mode 100644 index 0000000..f362237 --- /dev/null +++ b/src/main/java/com/banesco/module/service_status/infrastructure/client/ServiceStatusClient.java @@ -0,0 +1,75 @@ +package com.banesco.module.service_status.infrastructure.client; + +import com.banesco.common.application.usecase.HttpClientUseCase; +import com.banesco.common.domain.exception.HttpStatusCodeException; +import com.banesco.common.domain.model.HttpRequest; +import com.banesco.common.infrastructure.config.RestClientConfig; +import com.banesco.module.service_status.application.usecase.ServiceStatusUseCase; +import com.banesco.module.service_status.domain.dto.request.ServiceStatusRequest; +import com.banesco.module.service_status.domain.model.ServiceStatusConfig; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@ApplicationScoped +public class ServiceStatusClient implements ServiceStatusUseCase { + private final HttpClientUseCase httpClientUseCase; + private final ServiceStatusConfig serviceStatusConfig; + + @Inject + public ServiceStatusClient( + HttpClientUseCase httpClientUseCase, + RestClientConfig restClientConfig + ) { + this.httpClientUseCase = httpClientUseCase; + this.serviceStatusConfig = restClientConfig.getServiceStatusConfig(); + log.info("Configuracion cargada para service-status: {}", serviceStatusConfig); + } + + @Override + public T execute( + ServiceStatusRequest params, + Class responseType + ) { + ServiceStatusRequest body = ServiceStatusRequest.builder() + .applicationId(params.getApplicationId()) + .transactionId(params.getTransactionId()) + .bankService(serviceStatusConfig.getRequest().getBankService()) + .build(); + HttpRequest request = HttpRequest.forDirectResponse( + serviceStatusConfig.getUrl(), + HttpRequest.HttpMethod.POST, + responseType + ) + .withBody(body) + .withTimeout( + serviceStatusConfig.getTimeout().getConnect(), + serviceStatusConfig.getTimeout().getResponse() + ); + + log.debug("Request configurado: {}", request); + + try { + T response = httpClientUseCase.execute(request); + + log.info( + "Solicitud de estado del servicio exitosa: {}", + response.toString().contains("status=") + ); + + return response; + } catch (HttpStatusCodeException e) { + log.error( + "Error HTTP ejecutando verificacion de estado del servicio: {} - {}", + e.getStatusCode(), + e.getMessage() + ); + + throw e; + } catch (Exception e) { + log.error("Error ejecutando verificacion de estado del servicio: {}", e.getMessage()); + throw HttpStatusCodeException.serviceUnavailable("503"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/banesco/module/transaction/domain/model/Transaction.java b/src/main/java/com/banesco/module/transaction/domain/model/Transaction.java new file mode 100644 index 0000000..05f0c7f --- /dev/null +++ b/src/main/java/com/banesco/module/transaction/domain/model/Transaction.java @@ -0,0 +1,18 @@ +package com.banesco.module.transaction.domain.model; + +import com.banesco.common.domain.model.Identifier; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +import java.util.List; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class Transaction { + private Identifier transactionIdentification; // id + private List transactionDate; // dateCreate +} diff --git a/src/main/java/com/banesco/module/transaction/domain/model/TransactionDateTime.java b/src/main/java/com/banesco/module/transaction/domain/model/TransactionDateTime.java new file mode 100644 index 0000000..3381fef --- /dev/null +++ b/src/main/java/com/banesco/module/transaction/domain/model/TransactionDateTime.java @@ -0,0 +1,15 @@ +package com.banesco.module.transaction.domain.model; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.*; + +@Getter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class TransactionDateTime { + private TransactionDateTimeType dateType; + private String date; +} diff --git a/src/main/java/com/banesco/module/transaction/domain/model/TransactionDateTimeType.java b/src/main/java/com/banesco/module/transaction/domain/model/TransactionDateTimeType.java new file mode 100644 index 0000000..dfc93ff --- /dev/null +++ b/src/main/java/com/banesco/module/transaction/domain/model/TransactionDateTimeType.java @@ -0,0 +1,14 @@ +package com.banesco.module.transaction.domain.model; + +public enum TransactionDateTimeType { + EXECUTED_DATE, + FULFILLMENT_DATE, + INITIATED_DATE, + CANCELLED_DATE, + APPROVED_DATE, + VALUE_DATE, + BOOKING_DATE, + AVAILABLE_DATE, + EXPIRY_DATE, + EXPIRY_TIME, +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..f7a2dfc --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,20 @@ +quarkus: + http: + port: 8082 + idle-timeout: 30s + thread-pool: + max-threads: 100 + core-threads: 1 + +api: + source-id: BCSN + read-messages: + from-props: true + bus-create-security-notification: + messages: + key: 'bus-create-security-notification' + content: '[{"backendCode":"200","httpCode":200,"statusCode":"200","description":"Operacion exitosa"},{"backendCode":"R404","httpCode":404,"statusCode":"404","description":"Datos de validacion no encontrado."},{"backendCode":"503","httpCode":503,"statusCode":"503","description":"Uso interno"},{"backendCode":"422","httpCode":422,"statusCode":"422","description":"Uso interno"},{"backendCode":"500","httpCode":500,"statusCode":"500","description":"Uso interno"},{"backendCode":"100","httpCode":503,"statusCode":"503","description":"VDR13 - OSB Disponible"},{"backendCode":"OSB-382505","httpCode":503,"statusCode":"503","description":"VDR13 - OSB Disponible"},{"backendCode":"OSB-380002","httpCode":503,"statusCode":"503","description":"VDR13 - OSB Disponible"},{"backendCode":"ERROR","httpCode":400,"statusCode":"400","description":"Uso interno"},{"backendCode":"400","httpCode":400,"statusCode":"400","description":"Uso interno"},{"backendCode":"401","httpCode":401,"statusCode":"401","description":"Uso interno"},{"backendCode":"403","httpCode":403,"statusCode":"403","description":"Uso interno"},{"backendCode":"404","httpCode":404,"statusCode":"404","description":"Uso interno"},{"backendCode":"default","httpCode":409,"statusCode":"409","description":"Conflicto"},{"backendCode":"424","httpCode":424,"statusCode":"424","description":"Error de dependencia"},{"backendCode":"VDE01","httpCode":400,"statusCode":"VDE01","description":"VDE01 - Error en dato de entrada obligatorio: %s"},{"backendCode":"VDE02","httpCode":400,"statusCode":"VDE02","description":"VDE02 - Error en valor permitido para campo: %s"},{"backendCode":"VRN04","httpCode":"503","statusCode":"VRN04","description":"Servicio en horario de mantenimiento","status":"error"},{"backendCode":"VRN02","httpCode":"204","statusCode":"VRN02","description":"Cliente sin productos"}]' + rest-client: + dom-create-security-notification: '{"url":"http://localhost:8083/correspondence/initiate","timeout":{"connect":10000,"response":10000}}' + security-trace: '{"url":"http://api-register-security-route-apis-banesco-dev.apps.desplakur3.desintra.banesco.com/register-security/save","timeout":{"connect":10000,"response":10000},"request":{"sp":"spAPI_Traza","codEve":"P2PVUEL","codEve2":"codeAlert","codBan":"01","codMon":"BS"}}' + service-status: '{"url":"http://api-get-service-status-route-apis-banesco-dev.apps.desplakur3.desintra.banesco.com/service/status","timeout":{"connect":10000,"response":10000},"request":{"applicationId": "","transactionId": "","bankService": {"bankCode": "01","serviceCode": "APIFI","eventCode": "P2PVUEL"}}}' \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..86ce834 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,41 @@ +quarkus: + application: + name: bus-create-security-notification + version: 1.0.0 + ## Profile + profile: dev + http: + non-application-root-path: actuator + native: + file-encoding: UTF-8 + container-build: true + builder-image: quay.io/quarkus/ubi-quarkus-mandrel-builder-image:23.0.5.0-Final-java17 + container-runtime: docker + additional-build-args: + - -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy\$BySpaceAndTime + native-image-xmx: 6G + resources: + excludes: resources/*.yaml + ## OpenApi + smallrye-openapi: + path: /openapi + enable: 'true' + ## Swagger IU + swagger-ui: + path: /swagger-ui + always-include: 'true' + ## Logs + log: + level: INFO + console: + enable: true + format: "%d{HH:mm:ss.SSS} %-5p [%t] [%X{requestId}] %c{1} - %s%e%n" + smallrye-health: + root-path: /actuator/health + liveness-path: /actuator/health/live + readiness-path: /actuator/health/ready + ui: + enable: false + debug: + print-startup-times: true + reflection: false \ No newline at end of file diff --git a/src/main/resources/configmap.yaml b/src/main/resources/configmap.yaml new file mode 100644 index 0000000..e4598ed --- /dev/null +++ b/src/main/resources/configmap.yaml @@ -0,0 +1,80 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: configmap-bus-create-security-notification + namespace: proyecto-prueba-ja + uid: 999fa179-5fce-447d-90d2-b969543c6be8 + resourceVersion: '3493542477' + creationTimestamp: '2026-01-19T23:35:27Z' + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"v1","data":{"api.allowed.app-id":"[{\"appId\":\"DANIAPP\",\"request\":{\"serviceType\":\"P2P\",\"limitType\":\"REC\",\"casheaIndicator\":\"NO\"}}]","api.bus-create-security-notification.messages.content":"[{\"backendCode\":\"200\",\"httpCode\":200,\"statusCode\":\"200\",\"description\":\"Operacion exitosa\"},{\"backendCode\":\"R404\",\"httpCode\":404,\"statusCode\":\"404\",\"description\":\"Datos de validacion no encontrado.\"},{\"backendCode\":\"503\",\"httpCode\":503,\"statusCode\":\"503\",\"description\":\"Uso interno\"},{\"backendCode\":\"422\",\"httpCode\":422,\"statusCode\":\"422\",\"description\":\"Uso interno\"},{\"backendCode\":\"500\",\"httpCode\":500,\"statusCode\":\"500\",\"description\":\"Uso interno\"},{\"backendCode\":\"100\",\"httpCode\":503,\"statusCode\":\"503\",\"description\":\"VDR13 - OSB Disponible\"},{\"backendCode\":\"OSB-382505\",\"httpCode\":503,\"statusCode\":\"503\",\"description\":\"VDR13 - OSB Disponible\"},{\"backendCode\":\"OSB-380002\",\"httpCode\":503,\"statusCode\":\"503\",\"description\":\"VDR13 - OSB Disponible\"},{\"backendCode\":\"ERROR\",\"httpCode\":400,\"statusCode\":\"400\",\"description\":\"Uso interno\"},{\"backendCode\":\"400\",\"httpCode\":400,\"statusCode\":\"400\",\"description\":\"Uso interno\"},{\"backendCode\":\"401\",\"httpCode\":401,\"statusCode\":\"401\",\"description\":\"Uso interno\"},{\"backendCode\":\"403\",\"httpCode\":403,\"statusCode\":\"403\",\"description\":\"Uso interno\"},{\"backendCode\":\"404\",\"httpCode\":404,\"statusCode\":\"404\",\"description\":\"Uso interno\"},{\"backendCode\":\"default\",\"httpCode\":409,\"statusCode\":\"409\",\"description\":\"Conflicto\"},{\"backendCode\":\"424\",\"httpCode\":424,\"statusCode\":\"424\",\"description\":\"Error de dependencia\"},{\"backendCode\":\"VDE01\",\"httpCode\":400,\"statusCode\":\"VDE01\",\"description\":\"VDE01 - Error en dato de entrada obligatorio: %s\"},{\"backendCode\":\"VDE02\",\"httpCode\":400,\"statusCode\":\"VDE02\",\"description\":\"VDE02 - Error en valor permitido para campo: %s\"},{\"backendCode\":\"VRN04\",\"httpCode\":\"503\",\"statusCode\":\"VRN04\",\"description\":\"Servicio en horario de mantenimiento\",\"status\":\"error\"},{\"backendCode\":\"204\",\"httpCode\":\"200\",\"statusCode\":\"200\",\"description\":\"Cliente sin productos\",\"status\":\"ok\"}]","api.bus-create-security-notification.messages.key":"bus-create-security-notification","api.read-messages.from-props":"true","api.rest-client.dom-create-security-notification":"{\"url\":\"http://dom-create-security-notification-route-proyecto-prueba-ja.apps.desplakur3.desintra.banesco.com/create-security-notification/provide\",\"timeout\":{\"connect\":10000,\"response\":10000}}","api.rest-client.security-trace":"{\"url\":\"http://api-register-security-route-apis-banesco-dev.apps.desplakur3.desintra.banesco.com/register-security/save\",\"timeout\":{\"connect\":10000,\"response\":10000},\"request\":{\"sp\":\"spAPI_Traza\",\"codEve\":\"P2PVUEL\",\"codEve2\":\"P2PVUEL\",\"codBan\":\"01\",\"codMon\":\"BS\"}}","api.rest-client.service-status":"{\"url\":\"http://api-get-service-status-route-apis-banesco-dev.apps.desplakur3.desintra.banesco.com/service/status\",\"timeout\":{\"connect\":10000,\"response\":10000},\"request\":{\"applicationId\": \"\",\"transactionId\": \"\",\"bankService\": {\"bankCode\": \"01\",\"serviceCode\": \"APIFI\",\"eventCode\": \"P2PVUEL\"}}}","api.source-id":"BCSN","quarkus.application.name":"bus-create-security-notification","quarkus.application.version":"1.0.0","quarkus.debug.print-startup-times":"true","quarkus.debug.reflection":"false","quarkus.http.non-application-root-path":"actuator","quarkus.http.port":"8080","quarkus.log.consol.enable":"true","quarkus.log.consol.format":"%d{HH:mm:ss.SSS} %-5p [%t] [%X{requestId}] %c{1} - %s%e%n","quarkus.log.console.enable":"true","quarkus.log.console.format":"%d{HH:mm:ss.SSS} %-5p [%t] [%X{requestId}] %c{1} - %s%e%n","quarkus.log.level":"INFO","quarkus.profile":"dev","quarkus.smallrye-health.liveness-path":"/actuator/health/live","quarkus.smallrye-health.readiness-path":"/actuator/health/ready","quarkus.smallrye-health.root-path":"/actuator/health","quarkus.smallrye-health.ui.enable":"false","quarkus.smallrye-openapi.enable":"true","quarkus.smallrye-openapi.path":"/openapi","quarkus.swagger-ui.always-include":"true","quarkus.swagger-ui.path":"/swagger-ui"},"kind":"ConfigMap","metadata":{"annotations":{},"app":"bus-create-security-notification","labels":null,"name":"configmap-bus-create-security-notification","namespace":"proyecto-prueba-ja"}} + managedFields: + - manager: kubectl-client-side-apply + operation: Update + apiVersion: v1 + time: '2026-01-20T00:18:58Z' + fieldsType: FieldsV1 + fieldsV1: + 'f:data': + 'f:api.read-messages.from-props': {} + 'f:quarkus.smallrye-health.liveness-path': {} + 'f:api.source-id': {} + 'f:quarkus.log.console.format': {} + 'f:quarkus.smallrye-health.root-path': {} + 'f:api.bus-create-security-notification.messages.content': {} + 'f:quarkus.smallrye-openapi.enable': {} + 'f:api.rest-client.security-trace': {} + 'f:quarkus.smallrye-openapi.path': {} + 'f:quarkus.log.consol.enable': {} + 'f:quarkus.application.version': {} + 'f:quarkus.log.level': {} + 'f:quarkus.debug.print-startup-times': {} + .: {} + 'f:api.bus-create-security-notification.messages.key': {} + 'f:quarkus.swagger-ui.path': {} + 'f:quarkus.smallrye-health.readiness-path': {} + 'f:quarkus.log.console.enable': {} + 'f:quarkus.smallrye-health.ui.enable': {} + 'f:quarkus.swagger-ui.always-include': {} + 'f:quarkus.debug.reflection': {} + 'f:api.allowed.app-id': {} + 'f:api.rest-client.dom-create-security-notification': {} + 'f:quarkus.application.name': {} + 'f:quarkus.http.port': {} + 'f:quarkus.http.non-application-root-path': {} + 'f:quarkus.profile': {} + 'f:api.rest-client.service-status': {} + 'f:quarkus.log.consol.format': {} + 'f:metadata': + 'f:annotations': + .: {} + 'f:kubectl.kubernetes.io/last-applied-configuration': {} +data: + quarkus.application.version: 1.0.0 + quarkus.log.level: INFO + quarkus.debug.reflection: 'false' + quarkus.debug.print-startup-times: 'true' + quarkus.swagger-ui.path: /swagger-ui + quarkus.application.name: bus-create-security-notification + quarkus.smallrye-health.ui.enable: 'false' + quarkus.swagger-ui.always-include: 'true' + quarkus.http.non-application-root-path: actuator + api.rest-client.service-status: '{"url":"http://api-get-service-status-route-apis-banesco-dev.apps.desplakur3.desintra.banesco.com/service/status","timeout":{"connect":10000,"response":10000},"request":{"applicationId": "","transactionId": "","bankService": {"bankCode": "01","serviceCode": "APIFI","eventCode": "P2PVUEL"}}}' + api.rest-client.dom-create-security-notification: '{"url":"http://dom-create-security-notification-route-proyecto-prueba-ja.apps.desplakur3.desintra.banesco.com/correspondence/initiate","timeout":{"connect":10000,"response":10000}}' + api.read-messages.from-props: 'true' + quarkus.http.port: '8080' + quarkus.profile: dev + quarkus.log.console.format: '%d{HH:mm:ss.SSS} %-5p [%t] [%X{requestId}] %c{1} - %s%e%n' + quarkus.log.consol.format: '%d{HH:mm:ss.SSS} %-5p [%t] [%X{requestId}] %c{1} - %s%e%n' + api.bus-create-security-notification.messages.content: '[{"backendCode":"200","httpCode":200,"statusCode":"200","description":"Operacion exitosa"},{"backendCode":"R404","httpCode":404,"statusCode":"404","description":"Datos de validacion no encontrado."},{"backendCode":"503","httpCode":503,"statusCode":"503","description":"Uso interno"},{"backendCode":"422","httpCode":422,"statusCode":"422","description":"Uso interno"},{"backendCode":"500","httpCode":500,"statusCode":"500","description":"Uso interno"},{"backendCode":"100","httpCode":503,"statusCode":"503","description":"VDR13 - OSB Disponible"},{"backendCode":"OSB-382505","httpCode":503,"statusCode":"503","description":"VDR13 - OSB Disponible"},{"backendCode":"OSB-380002","httpCode":503,"statusCode":"503","description":"VDR13 - OSB Disponible"},{"backendCode":"ERROR","httpCode":400,"statusCode":"400","description":"Uso interno"},{"backendCode":"400","httpCode":400,"statusCode":"400","description":"Uso interno"},{"backendCode":"401","httpCode":401,"statusCode":"401","description":"Uso interno"},{"backendCode":"403","httpCode":403,"statusCode":"403","description":"Uso interno"},{"backendCode":"404","httpCode":404,"statusCode":"404","description":"Uso interno"},{"backendCode":"default","httpCode":409,"statusCode":"409","description":"Conflicto"},{"backendCode":"424","httpCode":424,"statusCode":"424","description":"Error de dependencia"},{"backendCode":"VDE01","httpCode":400,"statusCode":"VDE01","description":"VDE01 - Error en dato de entrada obligatorio: %s"},{"backendCode":"VDE02","httpCode":400,"statusCode":"VDE02","description":"VDE02 - Error en valor permitido para campo: %s"},{"backendCode":"VRN04","httpCode":"503","statusCode":"VRN04","description":"Servicio en horario de mantenimiento","status":"error"},{"backendCode":"204","httpCode":"200","statusCode":"200","description":"Cliente sin productos","status":"ok"}]' + api.rest-client.security-trace: '{"url":"http://api-register-security-route-apis-banesco-dev.apps.desplakur3.desintra.banesco.com/register-security/save","timeout":{"connect":10000,"response":10000},"request":{"sp":"spAPI_Traza","codEve":"P2PVUEL","codEve2":"codeAlert","codBan":"01","codMon":"BS"}}' + quarkus.smallrye-health.liveness-path: /actuator/health/live + api.source-id: BCSN + quarkus.smallrye-health.root-path: /actuator/health + api.bus-create-security-notification.messages.key: bus-create-security-notification + quarkus.smallrye-openapi.enable: 'true' + quarkus.smallrye-health.readiness-path: /actuator/health/ready + quarkus.log.console.enable: 'true' + quarkus.log.consol.enable: 'true' + quarkus.smallrye-openapi.path: /openapi