diff --git a/README.md b/README.md
index 637f474b..2bf2df47 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,58 @@
-# Midas
+# MidasFinCore
Project repo for the JPMC Advanced Software Engineering Forage program
+
+# Midas Core System
+
+## Overview
+
+**Midas Core** is a key component of the larger **Midas System**, a high-profile fintech platform designed to handle financial transactions with speed, reliability, and integrity. This project is part of a distributed system that integrates real-time transaction processing, data validation, and external API incentivization.
+
+The goal of Midas Core is to **receive**, **validate**, and **record** incoming financial transactions. It leverages modern backend technologies such as **Spring Boot**, **Apache Kafka**, **SQL databases**, and **REST APIs** to ensure secure, scalable, and efficient processing.
+
+---
+
+## Tech Stack
+
+- **Java 17**
+- **Spring Boot**
+- **Apache Kafka** – for receiving streaming financial transaction data
+- **PostgreSQL / MySQL** – for transaction validation and persistence
+- **External REST APIs** – for handling transaction incentivization
+- **Maven / Gradle** – for build and dependency management
+
+---
+
+## Architecture
+
+Midas Core operates as a service within a microservices architecture:
+
+1. **Receives transactions** via Kafka topics.
+2. **Validates** each transaction against business rules and database records.
+3. **Persists** the transaction data to a SQL database.
+4. **Communicates** with an external REST API to trigger incentive mechanisms.
+
+All components are wired using **Spring Boot’s dependency injection**, allowing clean modular development and easier testing.
+
+---
+
+## Getting Started
+
+### Prerequisites
+
+- Java 17+
+- Maven or Gradle
+- Docker (optional, for local Kafka or DB setup)
+- PostgreSQL or MySQL running locally or via Docker
+
+### Setup
+
+```bash
+# Clone the repository
+git clone https://github.com/YOUR-USERNAME/midas-core.git
+cd midas-core
+
+# Build the project
+./mvnw clean install
+
+# Run the application
+./mvnw spring-boot:run
diff --git a/application.yml b/application.yml
index e69de29b..3609de66 100644
--- a/application.yml
+++ b/application.yml
@@ -0,0 +1,51 @@
+general:
+ kafka-topic: midas-transactions
+
+incentive:
+ api:
+ url: http://localhost:8080/transaction
+
+server:
+ port: 33400
+
+spring:
+ datasource:
+ url: jdbc:h2:mem:midasdb;DB_CLOSE_DELAY=-1
+ driver-class-name: org.h2.Driver
+ username: sa
+ password:
+
+ kafka:
+ consumer:
+ bootstrap-servers: ${spring.embedded.kafka.brokers:localhost:9092}
+ group-id: midas-consumer-group
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
+ properties:
+ spring.json.trusted.packages: "com.jpmc.midascore.entity,com.jpmc.midascore.foundation"
+ spring.deserializer.value.delegate.class: org.springframework.kafka.support.serializer.JsonDeserializer
+ auto-offset-reset: earliest
+ producer:
+ bootstrap-servers: ${spring.embedded.kafka.brokers:localhost:9092}
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
+
+ jpa:
+ hibernate:
+ ddl-auto: update
+ show-sql: true
+ properties:
+ hibernate:
+ format_sql: true
+
+ h2:
+ console:
+ enabled: true
+ path: /h2-console
+
+logging:
+ level:
+ org.springframework: INFO
+ com.jpmc.midascore: DEBUG
+ org.hibernate.SQL: DEBUG
+ org.hibernate.type: TRACE
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index d1dedfec..3a6afbd7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,7 +16,52 @@
17
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.kafka
+ spring-kafka
+ 3.1.4
+
+
+ com.h2database
+ h2
+ runtime
+
+
+ org.springframework.kafka
+ spring-kafka-test
+ test
+
+
+ org.testcontainers
+ kafka
+ 1.19.1
+ test
+
diff --git a/src/main/java/com/jpmc/midascore/MidasCoreApplication.java b/src/main/java/com/jpmc/midascore/MidasCoreApplication.java
index 9222581f..26b9ef2e 100644
--- a/src/main/java/com/jpmc/midascore/MidasCoreApplication.java
+++ b/src/main/java/com/jpmc/midascore/MidasCoreApplication.java
@@ -2,8 +2,12 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
+@EntityScan("com.jpmc.midascore.entity") // Explicitly scan entity package
+@EnableJpaRepositories("com.jpmc.midascore.repository") // Explicitly scan repository package
public class MidasCoreApplication {
public static void main(String[] args) {
diff --git a/src/main/java/com/jpmc/midascore/component/IncentiveService.java b/src/main/java/com/jpmc/midascore/component/IncentiveService.java
new file mode 100644
index 00000000..0941fbe8
--- /dev/null
+++ b/src/main/java/com/jpmc/midascore/component/IncentiveService.java
@@ -0,0 +1,28 @@
+package com.jpmc.midascore.component;
+
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.jpmc.midascore.exception.DependencyException;
+import com.jpmc.midascore.foundation.Transaction;
+
+@Component
+public class IncentiveService {
+
+ private final RestTemplate restTemplate = new RestTemplate();
+
+ private final static String INCENTIVE_URL = "http://localhost:8080/incentive";
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ public float getIncentive(Transaction transaction) throws DependencyException {
+ try {
+ String responseEntityStr = restTemplate.postForObject(INCENTIVE_URL, transaction, String.class);
+ return Float.parseFloat(objectMapper.readTree(responseEntityStr).get("amount").asText());
+ } catch (Exception e) {
+ throw new DependencyException(e.getMessage());
+ }
+ }
+
+}
diff --git a/src/main/java/com/jpmc/midascore/component/TransactionConsumer.java b/src/main/java/com/jpmc/midascore/component/TransactionConsumer.java
new file mode 100644
index 00000000..51e677fb
--- /dev/null
+++ b/src/main/java/com/jpmc/midascore/component/TransactionConsumer.java
@@ -0,0 +1,90 @@
+package com.jpmc.midascore.component;
+
+import com.jpmc.midascore.entity.TransactionRecord;
+import com.jpmc.midascore.entity.UserRecord;
+import com.jpmc.midascore.exception.DependencyException;
+import com.jpmc.midascore.exception.ValidationException;
+import com.jpmc.midascore.foundation.Transaction;
+import com.jpmc.midascore.repository.TransactionRecordRepository;
+import com.jpmc.midascore.repository.UserRepository;
+
+import java.util.Optional;
+
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+public class TransactionConsumer {
+
+ private final UserRepository userRepository;
+
+ private final TransactionRecordRepository transactionRecordRepository;
+
+ private final IncentiveService incentiveService;
+
+ public TransactionConsumer(UserRepository userRepository,
+ TransactionRecordRepository transactionRecordRepository,
+ IncentiveService incentiveService) {
+ this.userRepository = userRepository;
+ this.transactionRecordRepository = transactionRecordRepository;
+ this.incentiveService = incentiveService;
+ }
+
+ @KafkaListener(topics = "${general.kafka-topic}", groupId = "${spring.kafka.consumer.group-id}")
+ public void listen(Transaction transaction) {
+ try {
+ final UserRecord receiver = validateAndGetReceiver(transaction);
+ final UserRecord sender = validateAndGetSender(transaction);
+ final TransactionRecord transactionRecord = new TransactionRecord(sender, receiver,
+ transaction.getAmount());
+ final float incentive = getIncentive(transaction);
+ receiver.setBalance(receiver.getBalance() + transaction.getAmount() + incentive);
+ sender.setBalance(sender.getBalance() - transaction.getAmount());
+ updateDatabaseInOneTransaction(transactionRecord, sender, receiver);
+ System.out.println(sender.getName() + " - balance: " + sender.getBalance());
+ System.out.println(receiver.getName() + " - balance: " + receiver.getBalance());
+ } catch (ValidationException ve) {
+ System.out.println("no modification to db because of error: " + ve.getMessage());
+ } catch (DependencyException de) {
+ System.out.println("no modification to db because of error: " + de.getMessage());
+ }
+ }
+
+ private float getIncentive(Transaction transaction) throws DependencyException {
+ return incentiveService.getIncentive(transaction);
+ }
+
+ @Transactional
+ private void updateDatabaseInOneTransaction(final TransactionRecord transactionRecord,
+ final UserRecord sender, final UserRecord receiver) {
+ transactionRecordRepository.save(transactionRecord);
+ userRepository.save(sender);
+ userRepository.save(receiver);
+ }
+
+ private UserRecord validateAndGetReceiver(final Transaction transaction) throws ValidationException {
+ final Long recipientId = transaction.getRecipientId();
+ Optional userRecord = userRepository.findById(recipientId);
+ if (!userRecord.isPresent()) {
+ final String errorMsg = "Recipient not found with id " + recipientId;
+ throw new ValidationException(errorMsg);
+ }
+ return userRecord.get();
+ }
+
+ private UserRecord validateAndGetSender(final Transaction transaction) throws ValidationException {
+ final Long senderId = transaction.getSenderId();
+ Optional userRecord = userRepository.findById(senderId);
+ if (!userRecord.isPresent()) {
+ final String errorMsg = "Sender not found with id " + senderId;
+ throw new ValidationException(errorMsg);
+ }
+ final UserRecord sender = userRecord.get();
+ if (sender.getBalance() < transaction.getAmount()) {
+ final String errorMsg = "Sender does not have enough balance to complete the transaction";
+ throw new ValidationException(errorMsg);
+ }
+ return sender;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/jpmc/midascore/entity/TransactionRecord.java b/src/main/java/com/jpmc/midascore/entity/TransactionRecord.java
new file mode 100644
index 00000000..762f5c96
--- /dev/null
+++ b/src/main/java/com/jpmc/midascore/entity/TransactionRecord.java
@@ -0,0 +1,75 @@
+package com.jpmc.midascore.entity;
+
+import jakarta.persistence.*;
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.fasterxml.jackson.annotation.ObjectIdGenerators;
+
+@Entity
+@JsonIdentityInfo(
+ generator = ObjectIdGenerators.PropertyGenerator.class,
+ property = "id")
+public class TransactionRecord {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "senderId", nullable = false)
+ private UserRecord sender;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "recipientId", nullable = false)
+ private UserRecord recipient;
+
+ @Column(nullable = false)
+ private float amount;
+
+ // Default constructor required by JPA
+ public TransactionRecord() {}
+
+ // All-args constructor for convenience
+ public TransactionRecord(UserRecord sender, UserRecord recipient, float amount) {
+ this.sender = sender;
+ this.recipient = recipient;
+ this.amount = amount;
+ }
+
+ // Getters and Setters
+ public Long getId() {
+ return id;
+ }
+
+ public UserRecord getSender() {
+ return sender;
+ }
+
+ public void setSender(UserRecord sender) {
+ this.sender = sender;
+ }
+
+ public UserRecord getRecipient() {
+ return recipient;
+ }
+
+ public void setRecipient(UserRecord recipient) {
+ this.recipient = recipient;
+ }
+
+ public float getAmount() {
+ return amount;
+ }
+
+ public void setAmount(float amount) {
+ this.amount = amount;
+ }
+
+ @Override
+ public String toString() {
+ return "Transaction{" +
+ "id=" + id +
+ ", senderId=" + (sender != null ? sender.getId() : "null") +
+ ", recipientId=" + (recipient != null ? recipient.getId() : "null") +
+ ", amount=" + amount +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/jpmc/midascore/entity/UserRecord.java b/src/main/java/com/jpmc/midascore/entity/UserRecord.java
index f9606fff..1417d8cc 100644
--- a/src/main/java/com/jpmc/midascore/entity/UserRecord.java
+++ b/src/main/java/com/jpmc/midascore/entity/UserRecord.java
@@ -1,6 +1,7 @@
package com.jpmc.midascore.entity;
import jakarta.persistence.*;
+import java.util.List;
@Entity
public class UserRecord {
@@ -15,6 +16,12 @@ public class UserRecord {
@Column(nullable = false)
private float balance;
+ @OneToMany(mappedBy = "sender")
+ private List sentTransactions;
+
+ @OneToMany(mappedBy = "recipient")
+ private List receivedTransactions;
+
protected UserRecord() {
}
@@ -25,7 +32,7 @@ public UserRecord(String name, float balance) {
@Override
public String toString() {
- return String.format("User[id=%d, name='%s', balance='%f'", id, name, balance);
+ return String.format("User[id=%d, name='%s', balance='%f']", id, name, balance);
}
public Long getId() {
diff --git a/src/main/java/com/jpmc/midascore/exception/DependencyException.java b/src/main/java/com/jpmc/midascore/exception/DependencyException.java
new file mode 100644
index 00000000..f9fa7e29
--- /dev/null
+++ b/src/main/java/com/jpmc/midascore/exception/DependencyException.java
@@ -0,0 +1,9 @@
+package com.jpmc.midascore.exception;
+
+public class DependencyException extends Exception {
+
+ public DependencyException(String message) {
+ super(message);
+ }
+
+}
diff --git a/src/main/java/com/jpmc/midascore/exception/ValidationException.java b/src/main/java/com/jpmc/midascore/exception/ValidationException.java
new file mode 100644
index 00000000..deb59710
--- /dev/null
+++ b/src/main/java/com/jpmc/midascore/exception/ValidationException.java
@@ -0,0 +1,7 @@
+package com.jpmc.midascore.exception;
+
+public class ValidationException extends Exception {
+ public ValidationException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/jpmc/midascore/repository/TransactionRecordRepository.java b/src/main/java/com/jpmc/midascore/repository/TransactionRecordRepository.java
new file mode 100644
index 00000000..c62cfc71
--- /dev/null
+++ b/src/main/java/com/jpmc/midascore/repository/TransactionRecordRepository.java
@@ -0,0 +1,8 @@
+package com.jpmc.midascore.repository;
+
+import com.jpmc.midascore.entity.TransactionRecord;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface TransactionRecordRepository extends JpaRepository {
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/jpmc/midascore/repository/UserRepository.java b/src/main/java/com/jpmc/midascore/repository/UserRepository.java
index 937275b6..48267a7f 100644
--- a/src/main/java/com/jpmc/midascore/repository/UserRepository.java
+++ b/src/main/java/com/jpmc/midascore/repository/UserRepository.java
@@ -1,8 +1,11 @@
package com.jpmc.midascore.repository;
import com.jpmc.midascore.entity.UserRecord;
-import org.springframework.data.repository.CrudRepository;
+import org.springframework.data.jpa.repository.JpaRepository;
+import java.util.Optional;
-public interface UserRepository extends CrudRepository {
- UserRecord findById(long id);
-}
+public interface UserRepository extends JpaRepository {
+ Optional findByName(String name);
+
+ Optional findById(Long id);
+}
\ No newline at end of file
diff --git a/src/main/java/com/jpmc/midascore/rest/BalanceController.java b/src/main/java/com/jpmc/midascore/rest/BalanceController.java
new file mode 100644
index 00000000..8250501b
--- /dev/null
+++ b/src/main/java/com/jpmc/midascore/rest/BalanceController.java
@@ -0,0 +1,32 @@
+package com.jpmc.midascore.rest;
+
+import java.util.Optional;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.jpmc.midascore.entity.UserRecord;
+import com.jpmc.midascore.foundation.Balance;
+import com.jpmc.midascore.repository.UserRepository;
+
+@RestController
+public class BalanceController {
+
+ private final UserRepository userRepository;
+
+ private static final Balance ZERO_BALANCE = new Balance(0);
+
+ public BalanceController(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ }
+
+ @GetMapping("/balance")
+ public Balance greeting(final Long userId) {
+ Optional userRecord = userRepository.findById(userId);
+ if (userRecord.isPresent()) {
+ return new Balance(userRecord.get().getBalance());
+ } else {
+ return ZERO_BALANCE;
+ }
+ }
+ ,
+}
diff --git a/src/test/java/com/jpmc/midascore/FileLoader.java b/src/test/java/com/jpmc/midascore/FileLoader.java
index 69992eb4..3432bd3f 100644
--- a/src/test/java/com/jpmc/midascore/FileLoader.java
+++ b/src/test/java/com/jpmc/midascore/FileLoader.java
@@ -1,7 +1,7 @@
package com.jpmc.midascore;
import org.springframework.stereotype.Component;
-import org.testcontainers.shaded.org.apache.commons.io.IOUtils;
+import org.apache.commons.io.IOUtils;
import java.io.InputStream;