diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..1c2fda5 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..2a00f5d --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..9448234 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..9661ac7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b9b4d36 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 devArm + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 668a6a2..bd9dd96 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # Reto Técnico: Procesamiento de Transacciones Bancarias (CLI) ## Objetivo: @@ -64,3 +65,35 @@ Desarrolla una aplicación de línea de comandos (CLI) que procese un archivo CS 6. **Documentación y Calidad del Código:** - Código bien documentado y fácil de leer. - Comentarios explicando pasos clave y lógica del programa. + +# retotecnico-cobol + +## Introducción: + +El procesamiento de archivos en plataformas windows y linux puede ser un desafio, inclusive el paradigma de programación y la cantidad de datos a procesar. +En esta oportunidad procesamos archivos csv con ayuda de un complemento univocity y el uso de streams. Aunque sin un enfoque reactivo de momento. + +## Instrucciones de Ejecución: +- **Dependencias**: Agregar com.univocity:univocity-parsers:2.9.1 en el archivo build.gradle +- **Ejeción**: +Genera un jar en la carpeta build +```bash +gradle shadowJar +java -jar .\build\libs\retotecnico-cobol-1.0-SNAPSHOT-all.jar .\src\NotFound +java -jar .\build\libs\retotecnico-cobol-1.0-SNAPSHOT-all.jar +java -jar .\build\libs\retotecnico-cobol-1.0-SNAPSHOT-all.jar .\external.csv +``` + +## Enfoque y Solución: + +- **Lógica**: Leer el archivo y procesarlo por medio del uso de streams. +- **Diseño**: Maximo de filas en el archivo 2^31, por el uso de List. Programación bloqueante, en caso decidir reactiva +precisaria que todo el ecosistema sea reactivo. Sin emabrgo en JAVA 24, se pueden usar los virtual threats. +En caso no se le proporcione una ruta o el destino no existe, usara un documento que se encuentre en los recursos del jar. + +## Estructura del Proyecto: + +- **Arquitectura**: Hexagonal, reference Get Your Hands Dirty on Clean Architecture +- **Package**: Adapters, Ports and Domain + +# cd20e34 (Initial commit) diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..49f2171 --- /dev/null +++ b/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'java' + id 'com.github.johnrengelman.shadow' version '7.0.0' +} + +group = 'org.example' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + // https://mvnrepository.com/artifact/com.univocity/univocity-parsers + implementation 'com.univocity:univocity-parsers:2.8.4' + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' +} + +jar { + manifest { + attributes( + 'Main-Class': 'org.cli.Main' + ) + } + from('src/main/resources') { + into('resources') + } +} + +shadowJar { + archiveBaseName.set('retotecnico-cobol') + archiveVersion.set('1.0-SNAPSHOT') + mergeServiceFiles() +} + +build.dependsOn shadowJar + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..5b54dc2 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'retotecnico-cobol' + diff --git a/src/main/java/org/cli/Main.java b/src/main/java/org/cli/Main.java new file mode 100644 index 0000000..5d98de0 --- /dev/null +++ b/src/main/java/org/cli/Main.java @@ -0,0 +1,54 @@ +package org.cli; + +import org.cli.adapter.out.FileAdapter; +import org.cli.application.domain.model.Transaccion; +import org.cli.application.domain.service.LoadTransactionService; + +import java.io.*; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +public class Main { + public static void main(String[] args) { + + LoadTransactionService service = new LoadTransactionService(new FileAdapter()); + Optional filePath = getFilePath(args); + + List transactions = service.loadCsv(filePath.orElse(null)); + + Transaccion biggest = transactions.stream().max(Transaccion::compareTo).orElse(null); + Map kindCount = transactions.stream().collect( + Collectors.groupingBy(t -> t.tipo, Collectors.counting())); + BigDecimal balance = getBalance(transactions); + + print(balance, kindCount, biggest); + + } + + private static void print(BigDecimal balance, Map kindCount, Transaccion biggest) { + System.out.println("Reporte de Transacciones"); + System.out.println("---------------------------------------------"); + System.out.println("Balance Final: " + balance); + System.out.println("Transacción de Mayor Monto: " + biggest); + System.out.println("Conteo de Transacciones: " + kindCount.toString().replaceAll("[^\\p{L}\\p{N}\\s=,]","")); + + } + + private static BigDecimal getBalance(List transactions) { + return transactions.stream() + .map(t -> + switch (t.tipo){ + case "Débito" -> t.monto.negate(); + case "Crédito" -> t.monto; + default -> throw new IllegalStateException("Unexpected value: " + t.tipo); + }) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private static Optional getFilePath(String[] args) { + return args.length > 0 ? Optional.of(args[0]) : Optional.empty(); + } +} \ No newline at end of file diff --git a/src/main/java/org/cli/adapter/out/FileAdapter.java b/src/main/java/org/cli/adapter/out/FileAdapter.java new file mode 100644 index 0000000..b461501 --- /dev/null +++ b/src/main/java/org/cli/adapter/out/FileAdapter.java @@ -0,0 +1,33 @@ +package org.cli.adapter.out; + +import com.univocity.parsers.csv.CsvParserSettings; +import com.univocity.parsers.csv.CsvRoutines; +import org.cli.application.domain.model.Transaccion; +import org.cli.application.port.out.LoadTransaccionPort; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +public class FileAdapter implements LoadTransaccionPort { + @Override + public List loadFromLocal(String filename) { + + CsvParserSettings settings = new CsvParserSettings(); + settings.setHeaderExtractionEnabled(true); + CsvRoutines routines = new CsvRoutines(settings); + + return routines.parseAll(Transaccion.class, loadFromFileSystem(filename)); + } + private InputStream loadFromFileSystem(String filename) { + try { + return new FileInputStream(filename); + } catch (IOException | NullPointerException e) { + System.err.println("File not found in the filesystem: " + filename); + System.out.println("Using a internal file input.csv of own resources "); + System.out.println(); + return getClass().getClassLoader().getResourceAsStream("input.csv"); + } + } +} diff --git a/src/main/java/org/cli/application/domain/model/ComparableTransaccion.java b/src/main/java/org/cli/application/domain/model/ComparableTransaccion.java new file mode 100644 index 0000000..91fe41e --- /dev/null +++ b/src/main/java/org/cli/application/domain/model/ComparableTransaccion.java @@ -0,0 +1,6 @@ +package org.cli.application.domain.model; + +public interface ComparableTransaccion extends Comparable { + @Override + int compareTo(Transaccion t); +} diff --git a/src/main/java/org/cli/application/domain/model/Transaccion.java b/src/main/java/org/cli/application/domain/model/Transaccion.java new file mode 100644 index 0000000..21c2e7a --- /dev/null +++ b/src/main/java/org/cli/application/domain/model/Transaccion.java @@ -0,0 +1,27 @@ +package org.cli.application.domain.model; + +import com.univocity.parsers.annotations.Parsed; +import com.univocity.parsers.annotations.Trim; + +import java.math.BigDecimal; + +public class Transaccion implements ComparableTransaccion{ + @Parsed(field = "id") + private BigDecimal id; + @Trim + @Parsed(field = "tipo") + public String tipo; + @Parsed(field = "monto") + public BigDecimal monto; + + @Override + public String toString() { + return "ID " + id +" - " + monto; + } + + @Override + public int compareTo(Transaccion t) { + return monto.compareTo(t.monto); + } +} + diff --git a/src/main/java/org/cli/application/domain/service/LoadTransactionService.java b/src/main/java/org/cli/application/domain/service/LoadTransactionService.java new file mode 100644 index 0000000..4bf66ab --- /dev/null +++ b/src/main/java/org/cli/application/domain/service/LoadTransactionService.java @@ -0,0 +1,18 @@ +package org.cli.application.domain.service; + +import org.cli.application.domain.model.Transaccion; +import org.cli.application.port.out.LoadTransaccionPort; + +import java.io.FileNotFoundException; +import java.util.List; + +public class LoadTransactionService { + private final LoadTransaccionPort loadTransaccionPort; + + public LoadTransactionService(LoadTransaccionPort loadTransaccionPort) { + this.loadTransaccionPort = loadTransaccionPort; + } + public List loadCsv(String filename) { + return loadTransaccionPort.loadFromLocal(filename); + } +} diff --git a/src/main/java/org/cli/application/port/out/LoadTransaccionPort.java b/src/main/java/org/cli/application/port/out/LoadTransaccionPort.java new file mode 100644 index 0000000..36cd15b --- /dev/null +++ b/src/main/java/org/cli/application/port/out/LoadTransaccionPort.java @@ -0,0 +1,10 @@ +package org.cli.application.port.out; + +import org.cli.application.domain.model.Transaccion; + +import java.io.FileNotFoundException; +import java.util.List; + +public interface LoadTransaccionPort { + List loadFromLocal(String filename); +} diff --git a/src/main/resources/input.csv b/src/main/resources/input.csv new file mode 100644 index 0000000..ac0c3bd --- /dev/null +++ b/src/main/resources/input.csv @@ -0,0 +1,6 @@ +id,tipo,monto +1,Crédito,100.00 +2,Débito,50.00 +3,Crédito,200.00 +4,Débito,75.00 +5,Crédito,150.00 \ No newline at end of file