diff --git a/.gitignore b/.gitignore
index 67045665db..e60b05b888 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,13 +1,7 @@
# Logs
logs
*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-lerna-debug.log*
-# Diagnostic reports (https://nodejs.org/api/report.html)
-report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
@@ -15,90 +9,8 @@ pids
*.seed
*.pid.lock
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
+# Compilation
+target
-# Coverage directory used by tools like istanbul
-coverage
-*.lcov
-
-# nyc test coverage
-.nyc_output
-
-# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-bower_components
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (https://nodejs.org/api/addons.html)
-build/Release
-
-# Dependency directories
-node_modules/
-jspm_packages/
-
-# TypeScript v1 declaration files
-typings/
-
-# TypeScript cache
-*.tsbuildinfo
-
-# Optional npm cache directory
-.npm
-
-# Optional eslint cache
-.eslintcache
-
-# Microbundle cache
-.rpt2_cache/
-.rts2_cache_cjs/
-.rts2_cache_es/
-.rts2_cache_umd/
-
-# Optional REPL history
-.node_repl_history
-
-# Output of 'npm pack'
-*.tgz
-
-# Yarn Integrity file
-.yarn-integrity
-
-# dotenv environment variables file
-.env
-.env.test
-
-# parcel-bundler cache (https://parceljs.org/)
-.cache
-
-# Next.js build output
-.next
-
-# Nuxt.js build / generate output
-.nuxt
-dist
-
-# Gatsby files
-.cache/
-# Comment in the public line in if your project uses Gatsby and *not* Next.js
-# https://nextjs.org/blog/next-9-1#public-directory-support
-# public
-
-# vuepress build output
-.vuepress/dist
-
-# Serverless directories
-.serverless/
-
-# FuseBox cache
-.fusebox/
-
-# DynamoDB Local files
-.dynamodb/
-
-# TernJS port file
-.tern-port
+# VSCode
+.vscode
\ No newline at end of file
diff --git a/TESTING.md b/TESTING.md
new file mode 100644
index 0000000000..0e83a29a8d
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,36 @@
+# How to Test the Project (Windows CMD)
+
+## Prerequisites
+- Docker & Docker Compose
+- Java 17+
+- Maven
+
+## 1. Start Infrastructure
+Start using Docker Compose:
+
+docker compose up -d
+
+## 2. Test Transaction Creation
+Send a POST request to create a transaction.
+
+**Scenario A: Approved Transaction (Value <= 1000)**
+
+curl -X POST http://localhost:8081/transactions ^
+ -H "Content-Type: application/json" ^
+ -d "{\"accountExternalIdDebit\":\"26066299-4467-4d70-a8af-2415dd537af5\",\"accountExternalIdCredit\":\"d26e431c-b72e-436f-8898-7509e5399580\",\"transferTypeId\":1,\"value\":500}"
+
+**Scenario B: Rejected Transaction (Value > 1000)**
+
+curl -X POST http://localhost:8081/transactions ^
+ -H "Content-Type: application/json" ^
+ -d "{\"accountExternalIdDebit\":\"26066299-4467-4d70-a8af-2415dd537af5\",\"accountExternalIdCredit\":\"d26e431c-b72e-436f-8898-7509e5399580\",\"transferTypeId\":1,\"value\":1500}"
+
+## 3. Verify Status
+The response from the initial POST will be PENDING.
+To check the final status (updated by Anti-Fraud service via Kafka), copy the transactionExternalId from the response and run:
+
+curl -X GET "http://localhost:8081/transactions/{transactionExternalId}"
+
+Replace {transactionExternalId} with the UUID returned from the POST.
+- Scenario A should be APPROVED.
+- Scenario B should be REJECTED.
diff --git a/anti-fraud-service/Dockerfile b/anti-fraud-service/Dockerfile
new file mode 100644
index 0000000000..84cfeed4ba
--- /dev/null
+++ b/anti-fraud-service/Dockerfile
@@ -0,0 +1,20 @@
+# ---------- Build stage ----------
+FROM maven:3.9-eclipse-temurin-17 AS build
+WORKDIR /build
+COPY pom.xml .
+RUN mvn -B dependency:go-offline
+
+COPY src ./src
+RUN mvn -B package
+
+# ---------- Runtime stage ----------
+FROM eclipse-temurin:17-jre
+WORKDIR /work
+
+COPY --from=build /build/target/quarkus-app/lib/ lib/
+COPY --from=build /build/target/quarkus-app/app/ app/
+COPY --from=build /build/target/quarkus-app/quarkus/ quarkus/
+COPY --from=build /build/target/quarkus-app/quarkus-run.jar app.jar
+
+EXPOSE 8080
+ENTRYPOINT ["java","-jar","app.jar"]
diff --git a/anti-fraud-service/pom.xml b/anti-fraud-service/pom.xml
new file mode 100644
index 0000000000..a9ba1a4ba0
--- /dev/null
+++ b/anti-fraud-service/pom.xml
@@ -0,0 +1,128 @@
+
+
+ 4.0.0
+ com.yape.codechallenge
+ anti-fraud-service
+ 1.0.0-SNAPSHOT
+
+ 3.13.0
+ 17
+ UTF-8
+ UTF-8
+ quarkus-bom
+ io.quarkus.platform
+ 3.15.1
+ true
+ 3.5.0
+
+
+
+
+ io.quarkus.platform
+ quarkus-bom
+ ${quarkus.platform.version}
+ pom
+ import
+
+
+
+
+
+ io.quarkus
+ quarkus-resteasy-reactive-jackson
+
+
+
+ io.quarkus
+ quarkus-smallrye-reactive-messaging-kafka
+
+
+
+ io.quarkus
+ quarkus-rest-jsonb
+
+
+
+ io.quarkus
+ quarkus-arc
+
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
+
+
+
+
+ ${quarkus.platform.group-id}
+ quarkus-maven-plugin
+ ${quarkus.platform.version}
+ true
+
+
+
+ build
+ generate-code
+ generate-code-tests
+
+
+
+
+
+ maven-compiler-plugin
+ ${compiler-plugin.version}
+
+
+ -parameters
+
+
+
+
+ maven-surefire-plugin
+ ${surefire-plugin.version}
+
+
+ org.jboss.logmanager.LogManager
+ ${maven.home}
+
+
+
+
+ maven-failsafe-plugin
+ ${surefire-plugin.version}
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}-runner
+ org.jboss.logmanager.LogManager
+ ${maven.home}
+
+
+
+
+
+
+
+ native
+
+
+ native
+
+
+
+ false
+ native
+
+
+
+
diff --git a/anti-fraud-service/src/main/java/com/yape/codechallenge/event/TransactionEvent.java b/anti-fraud-service/src/main/java/com/yape/codechallenge/event/TransactionEvent.java
new file mode 100644
index 0000000000..d003fe4f93
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/codechallenge/event/TransactionEvent.java
@@ -0,0 +1,9 @@
+package com.yape.codechallenge.event;
+
+import java.util.UUID;
+
+public record TransactionEvent(
+ UUID transactionExternalId,
+ Double value,
+ String status) {
+}
diff --git a/anti-fraud-service/src/main/java/com/yape/codechallenge/service/AntiFraudService.java b/anti-fraud-service/src/main/java/com/yape/codechallenge/service/AntiFraudService.java
new file mode 100644
index 0000000000..ef1b66b68d
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/codechallenge/service/AntiFraudService.java
@@ -0,0 +1,28 @@
+package com.yape.codechallenge.service;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import org.eclipse.microprofile.reactive.messaging.Incoming;
+import org.eclipse.microprofile.reactive.messaging.Outgoing;
+
+import com.yape.codechallenge.event.TransactionEvent;
+
+import io.smallrye.reactive.messaging.annotations.Blocking;
+
+@ApplicationScoped
+public class AntiFraudService {
+
+ @Incoming("transaction-created-in")
+ @Outgoing("transaction-status-out")
+ @Blocking
+ public TransactionEvent validateTransaction(TransactionEvent event) {
+
+ String newStatus = event.value() > 1000
+ ? "rejected"
+ : "approved";
+
+ return new TransactionEvent(
+ event.transactionExternalId(),
+ event.value(),
+ newStatus);
+ }
+}
diff --git a/anti-fraud-service/src/main/resources/application.properties b/anti-fraud-service/src/main/resources/application.properties
new file mode 100644
index 0000000000..999915e91d
--- /dev/null
+++ b/anti-fraud-service/src/main/resources/application.properties
@@ -0,0 +1,15 @@
+# Kafka
+mp.messaging.connector.smallrye-kafka.bootstrap.servers=kafka:29092
+
+# Incoming
+mp.messaging.incoming.transaction-created-in.connector=smallrye-kafka
+mp.messaging.incoming.transaction-created-in.topic=transactions
+mp.messaging.incoming.transaction-created-in.value.deserializer.value.type=com.yape.codechallenge.TransactionEvent
+mp.messaging.incoming.transaction-created-in.group.id=anti-fraud-group
+
+# Outgoing
+mp.messaging.outgoing.transaction-status-out.connector=smallrye-kafka
+mp.messaging.outgoing.transaction-status-out.topic=transaction-status
+mp.messaging.outgoing.transaction-status-out.value.serializer=io.quarkus.kafka.client.serialization.ObjectMapperSerializer
+
+quarkus.http.port=8998
diff --git a/anti-fraud-service/src/test/java/com/yape/codechallenge/service/AntiFraudServiceTest.java b/anti-fraud-service/src/test/java/com/yape/codechallenge/service/AntiFraudServiceTest.java
new file mode 100644
index 0000000000..4fa0756fed
--- /dev/null
+++ b/anti-fraud-service/src/test/java/com/yape/codechallenge/service/AntiFraudServiceTest.java
@@ -0,0 +1,40 @@
+package com.yape.codechallenge.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+
+import com.yape.codechallenge.event.TransactionEvent;
+
+class AntiFraudServiceTest {
+
+ private final AntiFraudService antiFraudService = new AntiFraudService();
+
+ @Test
+ void shouldApproveTransactionWhenValueIsLessOrEqualThan1000() {
+ UUID transactionId = UUID.randomUUID();
+ TransactionEvent event = new TransactionEvent(transactionId, 1000.0, "pending");
+ TransactionEvent result = antiFraudService.validateTransaction(event);
+
+ assertNotNull(result);
+ assertEquals(transactionId, result.transactionExternalId());
+ assertEquals(1000.0, result.value());
+ assertEquals("approved", result.status());
+ }
+
+ @Test
+ void shouldRejectTransactionWhenValueIsGreaterThan1000() {
+ UUID transactionId = UUID.randomUUID();
+ TransactionEvent event = new TransactionEvent(transactionId, 1000.01, "pending");
+
+ TransactionEvent result = antiFraudService.validateTransaction(event);
+
+ assertNotNull(result);
+ assertEquals(transactionId, result.transactionExternalId());
+ assertEquals(1000.01, result.value());
+ assertEquals("rejected", result.status());
+ }
+}
diff --git a/anti-fraud-service/src/test/resources/application.properties b/anti-fraud-service/src/test/resources/application.properties
new file mode 100644
index 0000000000..b5301a7acb
--- /dev/null
+++ b/anti-fraud-service/src/test/resources/application.properties
@@ -0,0 +1,6 @@
+# Kafka Configuration
+%test.mp.messaging.incoming.transaction-created-in.connector=smallrye-in-memory
+%test.mp.messaging.incoming.transaction-created-in.topic=transactions
+
+%test.mp.messaging.outgoing.transaction-status-out.connector=smallrye-in-memory
+%test.mp.messaging.outgoing.transaction-status-out.topic=transaction-status
diff --git a/db/init/001_init.sql b/db/init/001_init.sql
new file mode 100644
index 0000000000..63d45d5e2b
--- /dev/null
+++ b/db/init/001_init.sql
@@ -0,0 +1,20 @@
+-- =========================================
+-- Init DB for transaction services
+-- =========================================
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+
+CREATE TABLE IF NOT EXISTS transactions (
+ transactionExternalId UUID PRIMARY KEY,
+ accountExternalIdDebit UUID NOT NULL,
+ accountExternalIdCredit UUID NOT NULL,
+ transferTypeId INTEGER NOT NULL,
+ value NUMERIC(12,2) NOT NULL,
+ status VARCHAR(20) NOT NULL,
+ createdAt TIMESTAMP NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS idx_transactions_status
+ ON transactions(status);
+
+CREATE INDEX IF NOT EXISTS idx_transactions_created_at
+ ON transactions(createdAt);
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 0e8807f21c..0bd28e6dfd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,25 +1,75 @@
-version: "3.7"
services:
postgres:
image: postgres:14
+ networks:
+ - kafka-net
ports:
- "5432:5432"
environment:
- - POSTGRES_USER=postgres
- - POSTGRES_PASSWORD=postgres
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_DB: postgres
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+ - ./db/init:/docker-entrypoint-initdb.d
+
zookeeper:
image: confluentinc/cp-zookeeper:5.5.3
+ networks:
+ - kafka-net
environment:
ZOOKEEPER_CLIENT_PORT: 2181
+
kafka:
image: confluentinc/cp-enterprise-kafka:5.5.3
- depends_on: [zookeeper]
+ networks:
+ - kafka-net
+ depends_on:
+ - zookeeper
+ ports:
+ - "9092:9092"
environment:
- KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
+ KAFKA_BROKER_ID: 1
+ KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
+
+ KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092,PLAINTEXT_HOST://0.0.0.0:9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
+
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
- KAFKA_BROKER_ID: 1
+ KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
- KAFKA_JMX_PORT: 9991
+ healthcheck:
+ test: [ "CMD", "bash", "-c", "kafka-topics --bootstrap-server localhost:9092 --list" ]
+ interval: 10s
+ timeout: 5s
+ retries: 10
+ transaction-service:
+ build: ./transaction-service
+ networks:
+ - kafka-net
+ depends_on:
+ kafka:
+ condition: service_healthy
+ postgres:
+ condition: service_started
+ ports:
+ - "8081:8998"
+
+ anti-fraud-service:
+ networks:
+ - kafka-net
+ build: ./anti-fraud-service
+ depends_on:
+ postgres:
+ condition: service_started
+ kafka:
+ condition: service_healthy
ports:
- - 9092:9092
+ - "8082:8998"
+
+volumes:
+ pgdata:
+networks:
+ kafka-net:
+ driver: bridge
diff --git a/transaction-service/Dockerfile b/transaction-service/Dockerfile
new file mode 100644
index 0000000000..a57c4d9292
--- /dev/null
+++ b/transaction-service/Dockerfile
@@ -0,0 +1,23 @@
+# ---------- Build stage ----------
+FROM maven:3.9-eclipse-temurin-17 AS build
+WORKDIR /build
+
+COPY pom.xml .
+RUN mvn -B dependency:go-offline
+
+COPY src ./src
+RUN mvn -B package
+
+
+# ---------- Runtime stage ----------
+FROM eclipse-temurin:17-jre
+WORKDIR /work
+
+COPY --from=build /build/target/quarkus-app/lib/ lib/
+COPY --from=build /build/target/quarkus-app/app/ app/
+COPY --from=build /build/target/quarkus-app/quarkus/ quarkus/
+COPY --from=build /build/target/quarkus-app/quarkus-run.jar app.jar
+
+EXPOSE 8080
+
+ENTRYPOINT ["java","-Dquarkus.profile=prod","-jar","app.jar"]
diff --git a/transaction-service/pom.xml b/transaction-service/pom.xml
new file mode 100644
index 0000000000..2c31b02087
--- /dev/null
+++ b/transaction-service/pom.xml
@@ -0,0 +1,155 @@
+
+
+ 4.0.0
+ com.yape.codechallenge
+ transaction-service
+ 1.0.0-SNAPSHOT
+
+ 3.13.0
+ 17
+ UTF-8
+ UTF-8
+ quarkus-bom
+ io.quarkus.platform
+ 3.15.1
+ true
+ 3.5.0
+
+
+
+
+ ${quarkus.platform.group-id}
+ ${quarkus.platform.artifact-id}
+ ${quarkus.platform.version}
+ pom
+ import
+
+
+
+
+
+ io.quarkus
+ quarkus-resteasy-reactive-jackson
+
+
+ io.quarkus
+ quarkus-hibernate-orm-panache
+
+
+ io.quarkus
+ quarkus-jdbc-postgresql
+
+
+ io.quarkus
+ quarkus-smallrye-reactive-messaging-kafka
+
+
+ io.quarkus
+ quarkus-arc
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ io.quarkus
+ quarkus-jdbc-h2
+ test
+
+
+ io.smallrye.reactive
+ smallrye-reactive-messaging-in-memory
+ test
+
+
+ org.mockito
+ mockito-core
+ 5.5.0
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.5.0
+ test
+
+
+
+
+
+ ${quarkus.platform.group-id}
+ quarkus-maven-plugin
+ ${quarkus.platform.version}
+ true
+
+
+
+ build
+ generate-code
+ generate-code-tests
+
+
+
+
+
+ maven-compiler-plugin
+ ${compiler-plugin.version}
+
+
+ -parameters
+
+
+
+
+ maven-surefire-plugin
+ ${surefire-plugin.version}
+
+
+ org.jboss.logmanager.LogManager
+ ${maven.home}
+
+
+
+
+ maven-failsafe-plugin
+ ${surefire-plugin.version}
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}-runner
+ org.jboss.logmanager.LogManager
+ ${maven.home}
+
+
+
+
+
+
+
+ native
+
+
+ native
+
+
+
+ false
+ native
+
+
+
+
diff --git a/transaction-service/src/main/java/com/yape/codechallenge/enums/TransactionStatus.java b/transaction-service/src/main/java/com/yape/codechallenge/enums/TransactionStatus.java
new file mode 100644
index 0000000000..0dca45f1c4
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/codechallenge/enums/TransactionStatus.java
@@ -0,0 +1,7 @@
+package com.yape.codechallenge.enums;
+
+public enum TransactionStatus {
+ APPROVED,
+ REJECTED,
+ PENDING
+}
\ No newline at end of file
diff --git a/transaction-service/src/main/java/com/yape/codechallenge/enums/TransactionType.java b/transaction-service/src/main/java/com/yape/codechallenge/enums/TransactionType.java
new file mode 100644
index 0000000000..248701e3ca
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/codechallenge/enums/TransactionType.java
@@ -0,0 +1,7 @@
+package com.yape.codechallenge.enums;
+
+public enum TransactionType {
+ PAYMENT,
+ TRANSFER,
+ REFUND
+}
\ No newline at end of file
diff --git a/transaction-service/src/main/java/com/yape/codechallenge/event/TransactionEvent.java b/transaction-service/src/main/java/com/yape/codechallenge/event/TransactionEvent.java
new file mode 100644
index 0000000000..a5a89c4aaa
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/codechallenge/event/TransactionEvent.java
@@ -0,0 +1,10 @@
+package com.yape.codechallenge.event;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+public record TransactionEvent(
+ UUID transactionExternalId,
+ BigDecimal value,
+ String status) {
+}
diff --git a/transaction-service/src/main/java/com/yape/codechallenge/model/Transaction.java b/transaction-service/src/main/java/com/yape/codechallenge/model/Transaction.java
new file mode 100644
index 0000000000..4f626e6597
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/codechallenge/model/Transaction.java
@@ -0,0 +1,40 @@
+package com.yape.codechallenge.model;
+
+import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import java.math.BigDecimal;
+import jakarta.persistence.Id;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Entity
+@jakarta.persistence.Table(name = "transactions")
+public class Transaction extends PanacheEntityBase {
+
+ @Id
+ @GeneratedValue
+ @Column(columnDefinition = "uuid")
+ public UUID transactionExternalId;
+
+ public UUID accountExternalIdDebit;
+ public UUID accountExternalIdCredit;
+ public Integer transferTypeId;
+ @Column(precision = 12, scale = 2)
+ public BigDecimal value;
+ public String status;
+ public LocalDateTime createdAt;
+
+ public Transaction() {
+ }
+
+ public Transaction(UUID accountDebit, UUID accountCredit, Integer typeId, BigDecimal val) {
+ this.accountExternalIdDebit = accountDebit;
+ this.accountExternalIdCredit = accountCredit;
+ this.transferTypeId = typeId;
+ this.value = val;
+ this.status = "pending";
+ this.createdAt = LocalDateTime.now();
+ }
+}
diff --git a/transaction-service/src/main/java/com/yape/codechallenge/repository/TransactionRepository.java b/transaction-service/src/main/java/com/yape/codechallenge/repository/TransactionRepository.java
new file mode 100644
index 0000000000..865ca878fa
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/codechallenge/repository/TransactionRepository.java
@@ -0,0 +1,11 @@
+package com.yape.codechallenge.repository;
+
+import com.yape.codechallenge.model.Transaction;
+
+import java.util.UUID;
+
+public interface TransactionRepository {
+ Transaction save(Transaction transaction);
+
+ Transaction findById(UUID id);
+}
diff --git a/transaction-service/src/main/java/com/yape/codechallenge/repository/TransactionRepositoryImpl.java b/transaction-service/src/main/java/com/yape/codechallenge/repository/TransactionRepositoryImpl.java
new file mode 100644
index 0000000000..b6c8f740bf
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/codechallenge/repository/TransactionRepositoryImpl.java
@@ -0,0 +1,21 @@
+package com.yape.codechallenge.repository;
+
+import com.yape.codechallenge.model.Transaction;
+import jakarta.enterprise.context.ApplicationScoped;
+
+import java.util.UUID;
+
+@ApplicationScoped
+public class TransactionRepositoryImpl implements TransactionRepository {
+
+ @Override
+ public Transaction save(Transaction transaction) {
+ transaction.persist();
+ return transaction;
+ }
+
+ @Override
+ public Transaction findById(UUID id) {
+ return Transaction.findById(id);
+ }
+}
diff --git a/transaction-service/src/main/java/com/yape/codechallenge/request/CreateTransactionRequest.java b/transaction-service/src/main/java/com/yape/codechallenge/request/CreateTransactionRequest.java
new file mode 100644
index 0000000000..70a3a3437b
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/codechallenge/request/CreateTransactionRequest.java
@@ -0,0 +1,11 @@
+package com.yape.codechallenge.request;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+public class CreateTransactionRequest {
+ public UUID accountExternalIdDebit;
+ public UUID accountExternalIdCredit;
+ public Integer transferTypeId;
+ public BigDecimal value;
+}
diff --git a/transaction-service/src/main/java/com/yape/codechallenge/resource/TransactionResource.java b/transaction-service/src/main/java/com/yape/codechallenge/resource/TransactionResource.java
new file mode 100644
index 0000000000..cfc7f17a96
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/codechallenge/resource/TransactionResource.java
@@ -0,0 +1,48 @@
+package com.yape.codechallenge.resource;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.*;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.util.UUID;
+
+import com.yape.codechallenge.enums.TransactionStatus;
+import com.yape.codechallenge.enums.TransactionType;
+import com.yape.codechallenge.model.Transaction;
+import com.yape.codechallenge.request.CreateTransactionRequest;
+import com.yape.codechallenge.response.TransactionResponse;
+import com.yape.codechallenge.service.TransactionService;
+
+@Path("/transactions")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class TransactionResource {
+
+ @Inject
+ TransactionService transactionService;
+
+ @POST
+ public Response create(CreateTransactionRequest request) {
+ Transaction transaction = transactionService.createTransaction(request);
+ return Response.status(201).entity(toResponse(transaction)).build();
+ }
+
+ @GET
+ @Path("/{id}")
+ public Response get(@PathParam("id") UUID id) {
+ Transaction transaction = transactionService.getTransaction(id);
+ if (transaction == null) {
+ return Response.status(404).build();
+ }
+ return Response.ok(toResponse(transaction)).build();
+ }
+
+ private TransactionResponse toResponse(Transaction t) {
+ return new TransactionResponse(
+ t.transactionExternalId,
+ TransactionType.TRANSFER,
+ TransactionStatus.valueOf(t.status.toUpperCase()),
+ t.value,
+ t.createdAt);
+ }
+}
diff --git a/transaction-service/src/main/java/com/yape/codechallenge/response/TransactionResponse.java b/transaction-service/src/main/java/com/yape/codechallenge/response/TransactionResponse.java
new file mode 100644
index 0000000000..b98dea8664
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/codechallenge/response/TransactionResponse.java
@@ -0,0 +1,16 @@
+package com.yape.codechallenge.response;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+import com.yape.codechallenge.enums.TransactionStatus;
+import com.yape.codechallenge.enums.TransactionType;
+
+public record TransactionResponse(
+ UUID transactionExternalId,
+ TransactionType transactionType,
+ TransactionStatus transactionStatus,
+ BigDecimal value,
+ LocalDateTime createdAt) {
+}
diff --git a/transaction-service/src/main/java/com/yape/codechallenge/service/TransactionService.java b/transaction-service/src/main/java/com/yape/codechallenge/service/TransactionService.java
new file mode 100644
index 0000000000..e31a12bc54
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/codechallenge/service/TransactionService.java
@@ -0,0 +1,65 @@
+package com.yape.codechallenge.service;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.transaction.Transactional;
+import org.eclipse.microprofile.reactive.messaging.Channel;
+import org.eclipse.microprofile.reactive.messaging.Emitter;
+import org.eclipse.microprofile.reactive.messaging.Incoming;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yape.codechallenge.event.TransactionEvent;
+import com.yape.codechallenge.model.Transaction;
+import com.yape.codechallenge.repository.TransactionRepository;
+import com.yape.codechallenge.request.CreateTransactionRequest;
+
+import java.io.IOException;
+import java.util.UUID;
+
+@ApplicationScoped
+public class TransactionService {
+
+ @Inject
+ @Channel("transaction-created-out")
+ Emitter transactionEmitter;
+
+ @Inject
+ ObjectMapper objectMapper;
+
+ @Inject
+ TransactionRepository transactionRepository;
+
+ @Transactional
+ public Transaction createTransaction(CreateTransactionRequest request) {
+ Transaction transaction = new Transaction(
+ request.accountExternalIdDebit,
+ request.accountExternalIdCredit,
+ request.transferTypeId,
+ request.value);
+ transactionRepository.save(transaction);
+
+ TransactionEvent event = new TransactionEvent(transaction.transactionExternalId, transaction.value,
+ transaction.status);
+ transactionEmitter.send(event);
+
+ return transaction;
+ }
+
+ public Transaction getTransaction(UUID id) {
+ return transactionRepository.findById(id);
+ }
+
+ @Incoming("transaction-status-in")
+ @Transactional
+ public void consumeStatusUpdate(String eventJson) {
+ try {
+ TransactionEvent event = objectMapper.readValue(eventJson, TransactionEvent.class);
+
+ Transaction transaction = transactionRepository.findById(event.transactionExternalId());
+ if (transaction != null) {
+ transaction.status = event.status();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/transaction-service/src/main/resources/application.properties b/transaction-service/src/main/resources/application.properties
new file mode 100644
index 0000000000..767010debf
--- /dev/null
+++ b/transaction-service/src/main/resources/application.properties
@@ -0,0 +1,38 @@
+#Quarkus
+quarkus.datasource.db-kind=postgresql
+quarkus.datasource.username=postgres
+quarkus.datasource.password=postgres
+
+quarkus.kafka.devservices.enabled=false
+quarkus.datasource.devservices.enabled=true
+
+quarkus.http.port=8998
+
+#Hibernate
+%dev.quarkus.hibernate-orm.database.generation=drop-and-create
+%prod.quarkus.hibernate-orm.database.generation=validate
+
+#Datasource
+%dev.quarkus.datasource.jdbc.url=jdbc:postgresql://postgres:5432/postgres
+%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://postgres:5432/postgres
+
+#Kafka - Bootstrap servers
+%prod.mp.messaging.connector.smallrye-kafka.bootstrap.servers=kafka:29092
+%dev.mp.messaging.connector.smallrye-kafka.bootstrap.servers=localhost:9092
+
+#Kafka - Outgoing
+mp.messaging.outgoing.transaction-created-out.connector=smallrye-kafka
+mp.messaging.outgoing.transaction-created-out.topic=transactions
+mp.messaging.outgoing.transaction-created-out.value.serializer=io.quarkus.kafka.client.serialization.ObjectMapperSerializer
+
+#Kafka - Incoming
+mp.messaging.incoming.transaction-status-in.connector=smallrye-kafka
+mp.messaging.incoming.transaction-status-in.topic=transaction-status
+mp.messaging.incoming.transaction-status-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
+
+# Test Configuration
+%test.quarkus.datasource.db-kind=h2
+%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb
+%test.quarkus.datasource.username=sa
+%test.quarkus.datasource.password=password
+%test.quarkus.hibernate-orm.database.generation=drop-and-create
diff --git a/transaction-service/src/test/java/com/yape/codechallenge/service/TransactionServiceUnitTest.java b/transaction-service/src/test/java/com/yape/codechallenge/service/TransactionServiceUnitTest.java
new file mode 100644
index 0000000000..f410adedb4
--- /dev/null
+++ b/transaction-service/src/test/java/com/yape/codechallenge/service/TransactionServiceUnitTest.java
@@ -0,0 +1,67 @@
+package com.yape.codechallenge.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yape.codechallenge.event.TransactionEvent;
+import com.yape.codechallenge.model.Transaction;
+import com.yape.codechallenge.repository.TransactionRepository;
+import com.yape.codechallenge.request.CreateTransactionRequest;
+
+import org.eclipse.microprofile.reactive.messaging.Emitter;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class TransactionServiceUnitTest {
+
+ @Mock
+ TransactionRepository transactionRepository;
+
+ @Mock
+ Emitter transactionEmitter;
+
+ @Mock
+ ObjectMapper objectMapper;
+
+ @InjectMocks
+ TransactionService transactionService;
+
+ @Test
+ public void testCreateTransaction() {
+ CreateTransactionRequest request = new CreateTransactionRequest();
+ request.accountExternalIdDebit = UUID.randomUUID();
+ request.accountExternalIdCredit = UUID.randomUUID();
+ request.transferTypeId = 1;
+ request.value = new BigDecimal("100");
+
+ Transaction fakeTransaction = new Transaction(
+ request.accountExternalIdDebit,
+ request.accountExternalIdCredit,
+ request.transferTypeId,
+ request.value);
+ fakeTransaction.transactionExternalId = UUID.randomUUID();
+ fakeTransaction.status = "pending";
+
+ when(transactionRepository.save(any())).thenReturn(fakeTransaction);
+ when(transactionEmitter.send(any(TransactionEvent.class))).thenReturn(CompletableFuture.completedFuture(null));
+
+ Transaction result = transactionService.createTransaction(request);
+
+ assertNotNull(result);
+ assertEquals("pending", result.status);
+
+ verify(transactionRepository).save(any());
+ verify(transactionEmitter).send(any(TransactionEvent.class));
+ }
+
+}
diff --git a/transaction-service/src/test/resources/application.properties b/transaction-service/src/test/resources/application.properties
new file mode 100644
index 0000000000..a918bb6629
--- /dev/null
+++ b/transaction-service/src/test/resources/application.properties
@@ -0,0 +1,12 @@
+quarkus.datasource.db-kind=h2
+quarkus.datasource.username=sa
+quarkus.datasource.password=sa
+quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
+quarkus.hibernate-orm.database.generation=update
+
+# Kafka Configuration
+%test.mp.messaging.outgoing.transaction-created-out.connector=smallrye-in-memory
+%test.mp.messaging.outgoing.transaction-created-out.topic=transactions
+
+%test.mp.messaging.incoming.transaction-status-in.connector=smallrye-in-memory
+%test.mp.messaging.incoming.transaction-status-in.topic=transaction-status