Skip to content

Commit

Permalink
fix : handle edge case scenarios for generating capital gains (#347)
Browse files Browse the repository at this point in the history
* fix : handle edge case scenarios for generating capital gains

* feat : adds test data to generate report

* feat : handle multiple transactions test data added
  • Loading branch information
rajadilipkolli committed Apr 27, 2024
1 parent 7b5a30d commit 46e677c
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ public static void write(Class<?> originClass, LogLevel logLevel, String message
default -> logger.warn("No suitable logLevel found");
}
}

private LogWriter() {
throw new UnsupportedOperationException("Constructor can't be initialized");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ void validateOpenBalance(UserSchemeDTO scheme) {
void processTransactions(Fund fund, List<UserTransactionDTO> transactions, UserSchemeDTO scheme) {
try {
FIFOUnits fifo = new FIFOUnits(fund, transactions);
investedAmount = this.investedAmount.add(fifo.getInvested());
investedAmount = this.investedAmount.add(fifo.getTotalInvested());
currentValue += scheme.valuation().value();
gainEntries.addAll(fifo.getGains());
gainEntries.addAll(fifo.getRecordedGains());
} catch (GainsException exc) {
this.errors.add(fund.scheme() + ", " + exc.getMessage());
}
Expand All @@ -73,14 +73,14 @@ Map<String, Object> prepareGains(List<GainEntry> gainEntries) {
// Group the gains by fy and fund
Map<String, List<GainEntry>> groupedGains = new HashMap<>();
for (GainEntry txn : gainEntries) {
String key = txn.getFinYear() + "-" + txn.getFundType();
String key = txn.getFinYear() + "#" + txn.getFundType();
if (!groupedGains.containsKey(key)) {
groupedGains.put(key, new ArrayList<>());
}
groupedGains.get(key).add(txn);
}
for (Map.Entry<String, List<GainEntry>> entry : groupedGains.entrySet()) {
String[] keys = entry.getKey().split("-");
String[] keys = entry.getKey().split("#");
String fy = keys[0];
FundType fund = FundType.valueOf(keys[1]);
if (!summary.containsKey(fy)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@ public PortfolioService(
this.capitalGainsService = capitalGainsService;
}

public String upload(MultipartFile portfolioFile) throws IOException {
public Map<String, Object> upload(MultipartFile portfolioFile) throws IOException {
CasDTO casDTO = parseCasDTO(portfolioFile);
capitalGainsService.processData(casDTO);
return processCasDTO(casDTO);
String response = processCasDTO(casDTO);
Map<String, Object> investmentSummary = capitalGainsService.processData(casDTO);
investmentSummary.put("importSummary", response);
return investmentSummary;
}

public PortfolioResponse getPortfolioByPAN(String panNumber, LocalDate evaluationDate) {
Expand Down
254 changes: 167 additions & 87 deletions src/main/java/com/learning/mfscreener/utils/FIFOUnits.java

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions src/main/java/com/learning/mfscreener/utils/GainEntry.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

public class GainEntry {

private final Map<String, BigDecimal> CII = loadCIIData();
private final Map<String, BigDecimal> CII = loadCostInflationIndexData();
private final LocalDate cutoffDate;
private final LocalDate sellCutoffDate;

Expand Down Expand Up @@ -248,7 +248,7 @@ private GainType getGainType() {
return this.saleDate.isAfter(ltcg.get(this.fundType.name())) ? GainType.LTCG : GainType.STCG;
}

private Map<String, BigDecimal> loadCIIData() {
private Map<String, BigDecimal> loadCostInflationIndexData() {
Map<String, BigDecimal> ciiDataMap = new HashMap<>();
ciiDataMap.put("FY2001-02", BigDecimal.valueOf(100));
ciiDataMap.put("FY2002-03", BigDecimal.valueOf(105));
Expand All @@ -272,6 +272,9 @@ private Map<String, BigDecimal> loadCIIData() {
ciiDataMap.put("FY2020-21", BigDecimal.valueOf(301));
ciiDataMap.put("FY2021-22", BigDecimal.valueOf(317));
ciiDataMap.put("FY2022-23", BigDecimal.valueOf(331));
ciiDataMap.put("FY2023-24", BigDecimal.valueOf(348));
// TODO to be updated when notified
ciiDataMap.put("FY2024-25", BigDecimal.valueOf(348));
return ciiDataMap;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
import jakarta.validation.constraints.PastOrPresent;
import java.io.IOException;
import java.time.LocalDate;
import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;

public interface PortfolioApi {

@Operation(summary = "Persists the transaction details.")
ResponseEntity<String> upload(@RequestPart("file") MultipartFile multipartFile) throws IOException;
ResponseEntity<Map<String, Object>> upload(@RequestPart("file") MultipartFile multipartFile) throws IOException;

@Operation(
summary =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.learning.mfscreener.web.api.PortfolioApi;
import java.io.IOException;
import java.time.LocalDate;
import java.util.Map;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
Expand All @@ -31,7 +32,8 @@ public PortfolioController(PortfolioService portfolioService) {

@Override
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> upload(@RequestPart("file") MultipartFile multipartFile) throws IOException {
public ResponseEntity<Map<String, Object>> upload(@RequestPart("file") MultipartFile multipartFile)
throws IOException {
return ResponseEntity.ok(portfolioService.upload(multipartFile));
}

Expand Down
90 changes: 90 additions & 0 deletions src/test/java/com/learning/mfscreener/utils/TestData.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,57 @@ private static UserSchemeDTO getIciciTechnologyScheme() {
0.859d,
TransactionType.PURCHASE_SIP,
null);
UserTransactionDTO userTransactionDTO1 = new UserTransactionDTO(
LocalDate.parse("2021-05-24"),
"SIP Purchase - INA100009859",
100.0d,
0.823d,
121.58d,
0.823d,
TransactionType.PURCHASE_SIP,
null);
UserTransactionDTO buyTransaction = new UserTransactionDTO(
LocalDate.parse("2022-09-08"),
"Purchase-BSE - - INA200005166",
999.95d,
6.954d,
143.79d,
58.584d,
TransactionType.PURCHASE,
null);
UserTransactionDTO buyTransactionTax = new UserTransactionDTO(
LocalDate.parse("2022-09-08"),
"*** Stamp Duty ***",
0.05d,
null,
null,
null,
TransactionType.STAMP_DUTY_TAX,
null);
UserTransactionDTO sellTransaction = new UserTransactionDTO(
LocalDate.parse("2022-09-08"),
"*Redemption - ELECTRONIC PAYMENT-BSE - - N256222117703332 ,\t\tess STT l",
-1000.0,
-6.955,
143.79,
51.629,
TransactionType.REDEMPTION,
null);
UserTransactionDTO sellTransactionTax = new UserTransactionDTO(
LocalDate.parse("2022-09-08"),
"*** SSTT Paid ***",
0.01,
null,
null,
null,
TransactionType.STT_TAX,
null);
transactions.add(userTransactionDTO);
transactions.add(userTransactionDTO1);
transactions.add(buyTransaction);
transactions.add(buyTransactionTax);
transactions.add(sellTransaction);
transactions.add(sellTransactionTax);
return new UserSchemeDTO(
"ICICI Prudential Technology Fund - Direct Plan - Growth (Non-Demat) - ISIN: INF109K01Z48",
"INF109K01Z48",
Expand Down Expand Up @@ -105,6 +155,42 @@ private static UserSchemeDTO axisSchemeDTO(boolean addTransaction) {
private static UserSchemeDTO getIciciSchemeDTO() {
List<UserTransactionDTO> transactions = new ArrayList<>();
UserTransactionDTO userTransactionDTO = new UserTransactionDTO(
LocalDate.parse("2021-07-19"),
"Switch In - From Liquid Fund - DP Growth - INA000006651",
24383.78d,
153.371d,
158.9851d,
153.371d,
TransactionType.SWITCH_IN,
null);
UserTransactionDTO taxTransaction = new UserTransactionDTO(
LocalDate.parse("2021-07-19"),
"*** Stamp Duty ***",
1.22d,
null,
null,
null,
TransactionType.STAMP_DUTY_TAX,
null);
UserTransactionDTO sellUserTransactionDTO = new UserTransactionDTO(
LocalDate.parse("2022-08-01"),
"*Switch Out - To Nifty 50 Index Fund-DP Growth-BSE - , less STT",
-5000.0d,
-28.261d,
176.9251d,
125.110d,
TransactionType.SWITCH_OUT,
null);
UserTransactionDTO sellTaxTransaction = new UserTransactionDTO(
LocalDate.parse("2022-08-01"),
"*** STT Paid ***",
0.05d,
null,
null,
null,
TransactionType.STT_TAX,
null);
UserTransactionDTO userTransactionDTO1 = new UserTransactionDTO(
LocalDate.parse("2021-01-14"),
"SIP Purchase-BSE - - INA200005166",
499.98d,
Expand All @@ -114,6 +200,10 @@ private static UserSchemeDTO getIciciSchemeDTO() {
TransactionType.PURCHASE_SIP,
null);
transactions.add(userTransactionDTO);
transactions.add(taxTransaction);
transactions.add(sellUserTransactionDTO);
transactions.add(sellTaxTransaction);
transactions.add(userTransactionDTO1);
return new UserSchemeDTO(
"ICICI Prudential Nifty Next 50 Index Fund - Direct Plan - Growth (Non-Demat) - ISIN: INF109K01Y80",
"INF109K01Y80",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.learning.mfscreener.web.controllers;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
Expand Down Expand Up @@ -47,7 +48,9 @@ void uploadFile() throws Exception {
// Perform the file upload request
mockMvc.perform(multipart("/api/portfolio/upload").file(multipartFile))
.andExpect(status().isOk())
.andExpect(content().string("Imported 1 folios and 1 transactions"));
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.importSummary", is("Imported 1 folios and 5 transactions")))
.andExpect(jsonPath("$.FY2022-23", notNullValue()));
} finally {
tempFile.deleteOnExit();
}
Expand All @@ -74,7 +77,8 @@ void uploadFileWithNoChanges() throws Exception {
// Perform the file upload request
mockMvc.perform(multipart("/api/portfolio/upload").file(multipartFile))
.andExpect(status().isOk())
.andExpect(content().string("Nothing to Update"));
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.importSummary", is("Nothing to Update")));
} finally {
tempFile.deleteOnExit();
}
Expand All @@ -101,7 +105,8 @@ void uploadFileWithNewFolio() throws Exception {
// Perform the file upload request
mockMvc.perform(multipart("/api/portfolio/upload").file(multipartFile))
.andExpect(status().isOk())
.andExpect(content().string("Imported 1 folios and 1 transactions"));
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.importSummary", is("Imported 1 folios and 1 transactions")));
} finally {
tempFile.deleteOnExit();
}
Expand All @@ -128,7 +133,9 @@ void uploadFileWithNewScheme() throws Exception {
// Perform the file upload request
mockMvc.perform(multipart("/api/portfolio/upload").file(multipartFile))
.andExpect(status().isOk())
.andExpect(content().string("Imported 0 folios and 1 transactions"));
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.importSummary", is("Imported 0 folios and 6 transactions")))
.andExpect(jsonPath("$.FY2022-23", notNullValue()));
} finally {
tempFile.deleteOnExit();
}
Expand All @@ -155,7 +162,8 @@ void uploadFileWithNewTransaction() throws Exception {
// Perform the file upload request
mockMvc.perform(multipart("/api/portfolio/upload").file(multipartFile))
.andExpect(status().isOk())
.andExpect(content().string("Imported 0 folios and 1 transactions"));
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.importSummary", is("Imported 0 folios and 1 transactions")));
} finally {
tempFile.deleteOnExit();
}
Expand All @@ -182,7 +190,8 @@ void uploadFileWithNewFolioAndSchemeAndTransaction() throws Exception {
// Perform the file upload request
mockMvc.perform(multipart("/api/portfolio/upload").file(multipartFile))
.andExpect(status().isOk())
.andExpect(content().string("Imported 1 folios and 3 transactions"));
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.importSummary", is("Imported 1 folios and 3 transactions")));
} finally {
tempFile.deleteOnExit();
}
Expand All @@ -207,13 +216,14 @@ void addOnlySchemeWhichIsImpossible() throws Exception {
// Perform the file upload request
mockMvc.perform(multipart("/api/portfolio/upload").file(multipartFile))
.andExpect(status().isOk())
.andExpect(content().string("Imported 1 folios and 0 transactions"));
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.importSummary", is("Imported 1 folios and 0 transactions")));
} finally {
tempFile.deleteOnExit();
}
}

private static FileWriter getFileWriter(File tempFile) throws IOException, IOException {
private static FileWriter getFileWriter(File tempFile) throws IOException {
FileWriter fileWriter = new FileWriter(tempFile);
fileWriter.write(
"""
Expand Down

0 comments on commit 46e677c

Please sign in to comment.