Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,57 @@
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Starter Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.2.5</version>
</dependency>

<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.5</version>
</dependency>

<!-- Spring Kafka -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>3.1.4</version>
</dependency>

<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>

<!-- Spring Boot Starter Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>3.2.5</version>
<scope>test</scope>
</dependency>

<!-- Spring Kafka Test -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<version>3.1.4</version>
<scope>test</scope>
</dependency>

<!-- Testcontainers Kafka -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>kafka</artifactId>
<version>1.19.1</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
41 changes: 41 additions & 0 deletions src/main/java/com/jpmc/midascore/component/BalanceController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.jpmc.midascore.component;

import com.jpmc.midascore.entity.Balance;
import com.jpmc.midascore.entity.UserRecord;
import com.jpmc.midascore.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Optional;

@RestController
public class BalanceController {

private static final Logger logger = LoggerFactory.getLogger(BalanceController.class);

private final UserRepository userRepository;

public BalanceController(UserRepository userRepository) {
this.userRepository = userRepository;
}

@GetMapping("/balance")
public Balance getBalance(@RequestParam("userId") Long userId) {
logger.info("Received balance request for userId: {}", userId);

Optional<UserRecord> userOpt = userRepository.findById(userId);

if (userOpt.isPresent()) {
UserRecord user = userOpt.get();
float userBalance = user.getBalance();
logger.info("Found user {} with balance: {}", user.getName(), userBalance);
return new Balance(userBalance);
} else {
logger.info("User with id {} not found, returning balance 0", userId);
return new Balance(0);
}
}
}
56 changes: 56 additions & 0 deletions src/main/java/com/jpmc/midascore/component/IncentiveService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.jpmc.midascore.component;

import com.jpmc.midascore.entity.Incentive;
import com.jpmc.midascore.entity.Transaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class IncentiveService {

private static final Logger logger = LoggerFactory.getLogger(IncentiveService.class);

private final RestTemplate restTemplate;
private final String incentiveApiUrl;

public IncentiveService(RestTemplate restTemplate,
@Value("${incentive.api.url:http://localhost:8080/incentive}") String incentiveApiUrl) {
this.restTemplate = restTemplate;
this.incentiveApiUrl = incentiveApiUrl;
}

public float getIncentiveAmount(Transaction transaction) {
try {
logger.info("Calling incentive API for transaction: {}", transaction);

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<Transaction> request = new HttpEntity<>(transaction, headers);

Incentive incentive = restTemplate.postForObject(
incentiveApiUrl,
request,
Incentive.class
);

if (incentive != null) {
logger.info("Received incentive amount: {}", incentive.getAmount());
return incentive.getAmount();
} else {
logger.warn("Received null incentive response");
return 0;
}

} catch (Exception e) {
logger.error("Error calling incentive API: {}", e.getMessage());
return 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.jpmc.midascore.component;

import com.jpmc.midascore.entity.Transaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

@Component
public class TransactionKafkaListener {

private static final Logger logger = LoggerFactory.getLogger(TransactionKafkaListener.class);
private final TransactionService transactionService;

public TransactionKafkaListener(TransactionService transactionService) {
this.transactionService = transactionService;
}

@KafkaListener(topics = "${general.kafka-topic}", groupId = "midas-consumer-group")
public void listen(Transaction transaction) {
logger.info("Received transaction: {}", transaction);

// Process the transaction
boolean processed = transactionService.processTransaction(transaction);

if (processed) {
logger.info("Transaction processed successfully");
} else {
logger.info("Transaction was invalid and discarded");
}

// Log Waldorf's balance for debugging
float waldorfBalance = transactionService.getUserBalance("waldorf");
if (waldorfBalance >= 0) {
logger.info("WALDORF'S BALANCE: {} (rounded down: {})", waldorfBalance, (int) Math.floor(waldorfBalance));
}

// Log Wilbur's balance for Task 4
float wilburBalance = transactionService.getUserBalance("wilbur");
if (wilburBalance >= 0) {
logger.info("WILBUR'S BALANCE: {} (rounded down: {})", wilburBalance, (int) Math.floor(wilburBalance));
}
}
}
109 changes: 109 additions & 0 deletions src/main/java/com/jpmc/midascore/component/TransactionService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.jpmc.midascore.component;

import com.jpmc.midascore.entity.Transaction;
import com.jpmc.midascore.entity.TransactionRecord;
import com.jpmc.midascore.entity.UserRecord;
import com.jpmc.midascore.repository.TransactionRecordRepository;
import com.jpmc.midascore.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
public class TransactionService {

private static final Logger logger = LoggerFactory.getLogger(TransactionService.class);

private final UserRepository userRepository;
private final TransactionRecordRepository transactionRecordRepository;
private final IncentiveService incentiveService;

public TransactionService(UserRepository userRepository,
TransactionRecordRepository transactionRecordRepository,
IncentiveService incentiveService) {
this.userRepository = userRepository;
this.transactionRecordRepository = transactionRecordRepository;
this.incentiveService = incentiveService;
}

@Transactional
public boolean processTransaction(Transaction transaction) {
logger.info("Processing transaction: senderId={}, recipientId={}, amount={}",
transaction.getSenderId(), transaction.getRecipientId(), transaction.getAmount());

// Validate transaction
if (!isValidTransaction(transaction)) {
logger.warn("Invalid transaction, discarding: {}", transaction);
return false;
}

// Get sender and recipient
Optional<UserRecord> senderOpt = userRepository.findById(transaction.getSenderId());
Optional<UserRecord> recipientOpt = userRepository.findById(transaction.getRecipientId());

if (senderOpt.isEmpty() || recipientOpt.isEmpty()) {
logger.warn("Sender or recipient not found, discarding transaction");
return false;
}

UserRecord sender = senderOpt.get();
UserRecord recipient = recipientOpt.get();

// Check if sender has sufficient balance
if (sender.getBalance() < transaction.getAmount()) {
logger.warn("Insufficient balance. Sender {} has {}, transaction amount is {}",
sender.getName(), sender.getBalance(), transaction.getAmount());
return false;
}

// Get incentive amount from API
float incentiveAmount = incentiveService.getIncentiveAmount(transaction);

// Update balances
sender.setBalance(sender.getBalance() - transaction.getAmount());
recipient.setBalance(recipient.getBalance() + transaction.getAmount() + incentiveAmount);

// Save updated user records
userRepository.save(sender);
userRepository.save(recipient);

// Create and save transaction record with incentive
TransactionRecord record = new TransactionRecord(sender, recipient, transaction.getAmount(), incentiveAmount);
transactionRecordRepository.save(record);

logger.info("Transaction processed successfully. Sender {} new balance: {}, Recipient {} new balance: {} (includes incentive: {})",
sender.getName(), sender.getBalance(), recipient.getName(), recipient.getBalance(), incentiveAmount);

return true;
}

public float getUserBalance(String userName) {
Optional<UserRecord> userOpt = userRepository.findByName(userName);
if (userOpt.isPresent()) {
return userOpt.get().getBalance();
}
return -1;
}

private boolean isValidTransaction(Transaction transaction) {
// Check if senderId and recipientId are valid (greater than 0)
if (transaction.getSenderId() <= 0 || transaction.getRecipientId() <= 0) {
return false;
}

// Check if amount is positive
if (transaction.getAmount() <= 0) {
return false;
}

// Check if sender and recipient are different
if (transaction.getSenderId() == transaction.getRecipientId()) {
return false;
}

return true;
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/jpmc/midascore/entity/Balance.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.jpmc.midascore.entity;

public class Balance {
private float balance;

public Balance() {
this.balance = 0;
}

public Balance(float balance) {
this.balance = balance;
}

public float getBalance() {
return balance;
}

public void setBalance(float balance) {
this.balance = balance;
}

@Override
public String toString() {
return "Balance{" +
"balance=" + balance +
'}';
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/jpmc/midascore/entity/Incentive.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.jpmc.midascore.entity;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class Incentive {
private float amount;

public Incentive() {
}

public Incentive(float amount) {
this.amount = amount;
}

public float getAmount() {
return amount;
}

public void setAmount(float amount) {
this.amount = amount;
}

@Override
public String toString() {
return "Incentive{amount=" + amount + "}";
}
}
Loading