From 2bec89ed3fff089a35d3ab4f057b2fda6a3e6775 Mon Sep 17 00:00:00 2001 From: Dhawal Agarwal Date: Fri, 1 Aug 2025 19:29:16 +0530 Subject: [PATCH 1/7] all required dependencies --- pom.xml | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/pom.xml b/pom.xml index d1dedfec..ec081c5a 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,57 @@ 17 + + + org.springframework.boot + spring-boot-starter-data-jpa + 3.2.5 + + + + + org.springframework.boot + spring-boot-starter-web + 3.2.5 + + + + + org.springframework.kafka + spring-kafka + 3.1.4 + + + + + com.h2database + h2 + 2.2.224 + + + + + org.springframework.boot + spring-boot-starter-test + 3.2.5 + test + + + + + org.springframework.kafka + spring-kafka-test + 3.1.4 + test + + + + + org.testcontainers + kafka + 1.19.1 + test + From c9e057cafa015b0aba0f6f59e37e29c8b3ac72ef Mon Sep 17 00:00:00 2001 From: Dhawal Agarwal Date: Fri, 1 Aug 2025 19:32:56 +0530 Subject: [PATCH 2/7] kafka listener file --- .../component/TransactionKafkaListener.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/main/java/com/jpmc/midascore/component/TransactionKafkaListener.java diff --git a/src/main/java/com/jpmc/midascore/component/TransactionKafkaListener.java b/src/main/java/com/jpmc/midascore/component/TransactionKafkaListener.java new file mode 100644 index 00000000..5b1f7529 --- /dev/null +++ b/src/main/java/com/jpmc/midascore/component/TransactionKafkaListener.java @@ -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)); + } + } +} \ No newline at end of file From 8b31e725b7469591c8782be90d51c1621ed6b150 Mon Sep 17 00:00:00 2001 From: Dhawal Agarwal Date: Fri, 1 Aug 2025 19:33:48 +0530 Subject: [PATCH 3/7] kafka producer file (updated) --- src/test/java/com/jpmc/midascore/KafkaProducer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/jpmc/midascore/KafkaProducer.java b/src/test/java/com/jpmc/midascore/KafkaProducer.java index fa22e2eb..181b3c3c 100644 --- a/src/test/java/com/jpmc/midascore/KafkaProducer.java +++ b/src/test/java/com/jpmc/midascore/KafkaProducer.java @@ -1,6 +1,6 @@ package com.jpmc.midascore; -import com.jpmc.midascore.foundation.Transaction; +import com.jpmc.midascore.entity.Transaction; import org.springframework.beans.factory.annotation.Value; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Component; From 436f906e8947205dde19ecec53316ba159c17dca Mon Sep 17 00:00:00 2001 From: Dhawal Agarwal Date: Fri, 1 Aug 2025 19:34:44 +0530 Subject: [PATCH 4/7] application.yml file for kafka configuration --- src/main/resources/application.yml | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/resources/application.yml diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 00000000..39da1860 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,38 @@ +general: + kafka-topic: midas-transactions + +incentive: + api: + url: http://localhost:8080/incentive + +server: + port: 33400 + +spring: + 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: "*" + spring.json.type.mapping: "transaction:com.jpmc.midascore.entity.Transaction" + 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: create-drop + show-sql: false + + h2: + console: + enabled: true + +logging: + level: + com.jpmc.midascore: INFO \ No newline at end of file From bedf28a7eb7dacb8643b7ae3c6fcd8c157628aa4 Mon Sep 17 00:00:00 2001 From: Dhawal Agarwal Date: Fri, 1 Aug 2025 19:37:25 +0530 Subject: [PATCH 5/7] Transaction service and recorder files and user repository file to configured to find by name --- .../component/TransactionService.java | 109 ++++++++++++++++++ .../midascore/entity/TransactionRecord.java | 76 ++++++++++++ .../TransactionRecordRepository.java | 9 ++ .../midascore/repository/UserRepository.java | 12 +- 4 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/jpmc/midascore/component/TransactionService.java create mode 100644 src/main/java/com/jpmc/midascore/entity/TransactionRecord.java create mode 100644 src/main/java/com/jpmc/midascore/repository/TransactionRecordRepository.java diff --git a/src/main/java/com/jpmc/midascore/component/TransactionService.java b/src/main/java/com/jpmc/midascore/component/TransactionService.java new file mode 100644 index 00000000..3943f447 --- /dev/null +++ b/src/main/java/com/jpmc/midascore/component/TransactionService.java @@ -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 senderOpt = userRepository.findById(transaction.getSenderId()); + Optional 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 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; + } +} \ 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..1d105364 --- /dev/null +++ b/src/main/java/com/jpmc/midascore/entity/TransactionRecord.java @@ -0,0 +1,76 @@ +package com.jpmc.midascore.entity; + +import jakarta.persistence.*; + +@Entity +public class TransactionRecord { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "sender_id", nullable = false) + private UserRecord sender; + + @ManyToOne + @JoinColumn(name = "recipient_id", nullable = false) + private UserRecord recipient; + + @Column(nullable = false) + private float amount; + + @Column(nullable = false) + private float incentive; + + protected TransactionRecord() { + } + + public TransactionRecord(UserRecord sender, UserRecord recipient, float amount) { + this.sender = sender; + this.recipient = recipient; + this.amount = amount; + this.incentive = 0; + } + + public TransactionRecord(UserRecord sender, UserRecord recipient, float amount, float incentive) { + this.sender = sender; + this.recipient = recipient; + this.amount = amount; + this.incentive = incentive; + } + + public Long getId() { + return id; + } + + public UserRecord getSender() { + return sender; + } + + public UserRecord getRecipient() { + return recipient; + } + + public float getAmount() { + return amount; + } + + public float getIncentive() { + return incentive; + } + + public void setIncentive(float incentive) { + this.incentive = incentive; + } + + @Override + public String toString() { + return String.format("TransactionRecord[id=%d, sender='%s', recipient='%s', amount=%.2f, incentive=%.2f]", + id, + sender != null ? sender.getName() : "null", + recipient != null ? recipient.getName() : "null", + amount, + incentive); + } +} \ No newline at end of file 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..265bac0a --- /dev/null +++ b/src/main/java/com/jpmc/midascore/repository/TransactionRecordRepository.java @@ -0,0 +1,9 @@ +package com.jpmc.midascore.repository; + +import com.jpmc.midascore.entity.TransactionRecord; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +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..9c80eb9b 100644 --- a/src/main/java/com/jpmc/midascore/repository/UserRepository.java +++ b/src/main/java/com/jpmc/midascore/repository/UserRepository.java @@ -1,8 +1,12 @@ 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 org.springframework.stereotype.Repository; -public interface UserRepository extends CrudRepository { - UserRecord findById(long id); -} +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByName(String name); +} \ No newline at end of file From ce795f94c6c5d4090ddbab8a0dcee271e4420c4f Mon Sep 17 00:00:00 2001 From: Dhawal Agarwal Date: Fri, 1 Aug 2025 19:40:13 +0530 Subject: [PATCH 6/7] Incentivization of transaction processing system through API --- .../midascore/component/IncentiveService.java | 56 +++++++++++++++++++ .../com/jpmc/midascore/entity/Incentive.java | 28 ++++++++++ .../foundation/RestTemplateConfig.java | 14 +++++ 3 files changed, 98 insertions(+) create mode 100644 src/main/java/com/jpmc/midascore/component/IncentiveService.java create mode 100644 src/main/java/com/jpmc/midascore/entity/Incentive.java create mode 100644 src/main/java/com/jpmc/midascore/foundation/RestTemplateConfig.java 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..f18a683e --- /dev/null +++ b/src/main/java/com/jpmc/midascore/component/IncentiveService.java @@ -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 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; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/jpmc/midascore/entity/Incentive.java b/src/main/java/com/jpmc/midascore/entity/Incentive.java new file mode 100644 index 00000000..2009d458 --- /dev/null +++ b/src/main/java/com/jpmc/midascore/entity/Incentive.java @@ -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 + "}"; + } +} \ No newline at end of file diff --git a/src/main/java/com/jpmc/midascore/foundation/RestTemplateConfig.java b/src/main/java/com/jpmc/midascore/foundation/RestTemplateConfig.java new file mode 100644 index 00000000..caa53047 --- /dev/null +++ b/src/main/java/com/jpmc/midascore/foundation/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package com.jpmc.midascore.foundation; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} \ No newline at end of file From dcdc17ca8f5cd6a05ef256d3c48449fe8e85b146 Mon Sep 17 00:00:00 2001 From: Dhawal Agarwal Date: Fri, 1 Aug 2025 19:41:00 +0530 Subject: [PATCH 7/7] REST controller to expose user balances via an API endpoint --- .../component/BalanceController.java | 41 +++++++++++++++++++ .../com/jpmc/midascore/entity/Balance.java | 28 +++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 src/main/java/com/jpmc/midascore/component/BalanceController.java create mode 100644 src/main/java/com/jpmc/midascore/entity/Balance.java diff --git a/src/main/java/com/jpmc/midascore/component/BalanceController.java b/src/main/java/com/jpmc/midascore/component/BalanceController.java new file mode 100644 index 00000000..f7dd7986 --- /dev/null +++ b/src/main/java/com/jpmc/midascore/component/BalanceController.java @@ -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 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); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/jpmc/midascore/entity/Balance.java b/src/main/java/com/jpmc/midascore/entity/Balance.java new file mode 100644 index 00000000..07849557 --- /dev/null +++ b/src/main/java/com/jpmc/midascore/entity/Balance.java @@ -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 + + '}'; + } +} \ No newline at end of file