Skip to content

Fix statement line entry date year based on the statement date #117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.qoomon.banking.swift.submessage;

import com.qoomon.banking.swift.submessage.field.StatementLine;
import com.qoomon.banking.swift.submessage.field.TransactionGroup;
import com.qoomon.banking.swift.submessage.field.subfield.EntryDateResolutionStrategy;

import java.time.LocalDate;
import java.time.MonthDay;
import java.util.List;
import java.util.stream.Collectors;

public class TransactionListPostProcessor {
private final EntryDateResolutionStrategy entryDateResolutionStrategy;

public TransactionListPostProcessor(EntryDateResolutionStrategy entryDateResolutionStrategy) {
this.entryDateResolutionStrategy = entryDateResolutionStrategy;
}

public List<TransactionGroup> adjustEntryDates(List<TransactionGroup> transactions, LocalDate statementDate) {
return transactions.stream().map(tx -> {
StatementLine line = tx.getStatementLine();
return new TransactionGroup(new StatementLine(
line.getValueDate(),
entryDateResolutionStrategy.resolve(MonthDay.from(line.getEntryDate()), statementDate),
line.getDebitCreditType(),
line.getDebitCreditMark(),
line.getAmount(),
line.getFundsCode().orElse(null),
line.getTransactionTypeIdentificationCode(),
line.getReferenceForAccountOwner(),
line.getReferenceForBank().orElse(null),
line.getSupplementaryDetails().orElse(null)
), tx.getInformationToAccountOwner().orElse(null));
}).collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.qoomon.banking.swift.submessage.field.subfield;

import java.time.LocalDate;
import java.time.MonthDay;

/**
* Strategy to determine the correct {@link com.qoomon.banking.swift.submessage.field.StatementLine} entry date.
*
* Statement line :61: subfield 2 contains an optional entry date:
*
* Notation: [4!n]
* Format: 'MMDD'
*
* Implementers of this strategy must determine the correct year of the entry date based on the given statement date.
* Note that if the entry date is absent, the value date is assumed as the entry date of the {@link com.qoomon.banking.swift.submessage.field.StatementLine}.
*/
public interface EntryDateResolutionStrategy {
LocalDate resolve(MonthDay entryMonthDay, LocalDate statementDate);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.qoomon.banking.swift.submessage.field.subfield;

import java.time.LocalDate;
import java.time.MonthDay;

import static java.lang.Math.abs;
import static java.lang.Math.min;

/**
* Resolve entry date based on the shortest delta between the entry date and the statement date.
* <p>
* This strategy views the {@link MonthDay} timeline as a circle, like the hours on a clock. If the shortest delta
* around the circle does not span 31 December, the entry date's year is the same as that of the statement. Otherwise,
* it is either the previous year if the entry date is before the statement date on the circular timeline, or the next year
* if the entry date is after the statement date.
* <p>
* Limitations: This strategy can not account for an entry date more than 12 months in the past (or future).
*/
public class ShortestDeltaEntryDateResolutionStrategy implements EntryDateResolutionStrategy {
@Override
public LocalDate resolve(MonthDay entryMonthDay, LocalDate statementDate) {
int e = LocalDate.from(entryMonthDay.adjustInto(statementDate)).getDayOfYear();
int s = statementDate.getDayOfYear();
int lengthOfYear = statementDate.lengthOfYear();

int statementToEntryDelta = s - e;
boolean statementAfterEntry = statementToEntryDelta > 0;
int shortestDelta = min(abs(statementToEntryDelta), lengthOfYear - abs(statementToEntryDelta));

boolean spans31December = statementAfterEntry ?
e + shortestDelta != s :
s + shortestDelta != e;

int entryYear;
if (!spans31December) {
entryYear = statementDate.getYear();
} else {
if (statementAfterEntry) {
entryYear = statementDate.getYear() + 1;
} else {
entryYear = statementDate.getYear() - 1;
}
}

return entryMonthDay.atYear(entryYear);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import com.qoomon.banking.swift.message.exception.SwiftMessageParseException;
import com.qoomon.banking.swift.submessage.PageReader;
import com.qoomon.banking.swift.submessage.PageSeparator;
import com.qoomon.banking.swift.submessage.TransactionListPostProcessor;
import com.qoomon.banking.swift.submessage.exception.PageParserException;
import com.qoomon.banking.swift.submessage.field.*;
import com.qoomon.banking.swift.submessage.field.subfield.ShortestDeltaEntryDateResolutionStrategy;

import java.io.Reader;
import java.util.LinkedList;
Expand Down Expand Up @@ -159,13 +161,14 @@ public MT940Page read() throws SwiftMessageParseException {
}
}

TransactionListPostProcessor postProcessor = new TransactionListPostProcessor(new ShortestDeltaEntryDateResolutionStrategy());
return new MT940Page(
transactionReferenceNumber,
relatedReference,
accountIdentification,
statementNumber,
openingBalance,
transactionList,
postProcessor.adjustEntryDates(transactionList, closingBalance.getDate()),
closingBalance,
closingAvailableBalance,
forwardAvailableBalanceList,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import com.qoomon.banking.swift.message.exception.SwiftMessageParseException;
import com.qoomon.banking.swift.submessage.PageReader;
import com.qoomon.banking.swift.submessage.PageSeparator;
import com.qoomon.banking.swift.submessage.TransactionListPostProcessor;
import com.qoomon.banking.swift.submessage.exception.PageParserException;
import com.qoomon.banking.swift.submessage.field.*;
import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark;
import com.qoomon.banking.swift.submessage.field.subfield.ShortestDeltaEntryDateResolutionStrategy;
import org.joda.money.BigMoney;
import org.joda.money.CurrencyUnit;

Expand Down Expand Up @@ -205,6 +207,7 @@ public MT942Page read() throws SwiftMessageParseException {
}
}

TransactionListPostProcessor postProcessor = new TransactionListPostProcessor(new ShortestDeltaEntryDateResolutionStrategy());
return new MT942Page(
transactionReferenceNumber,
relatedReference,
Expand All @@ -213,7 +216,7 @@ public MT942Page read() throws SwiftMessageParseException {
floorLimitIndicatorDebit,
floorLimitIndicatorCredit,
dateTimeIndicator,
transactionList,
postProcessor.adjustEntryDates(transactionList, dateTimeIndicator.getDateTime().toLocalDate()),
transactionSummaryDebit,
transactionSummaryCredit,
informationToAccountOwner
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.qoomon.banking.swift.submessage;

import com.google.common.collect.Lists;
import com.qoomon.banking.swift.submessage.field.GeneralField;
import com.qoomon.banking.swift.submessage.field.StatementLine;
import com.qoomon.banking.swift.submessage.field.TransactionGroup;
import com.qoomon.banking.swift.submessage.field.subfield.EntryDateResolutionStrategy;
import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

import java.time.LocalDate;
import java.time.MonthDay;
import java.util.List;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class TransactionListPostProcessorTest {

@Test
public void adjustEntryDates_WHEN_adjusting_transactions_SHOULD_return_modified_transactions() throws Exception {
LocalDate statementDate = LocalDate.parse("2003-09-01");
EntryDateResolutionStrategy strategy = mock(EntryDateResolutionStrategy.class);
when(strategy.resolve(MonthDay.of(8, 30), statementDate)).thenReturn(LocalDate.parse("2003-08-30"));
when(strategy.resolve(MonthDay.of(1, 1), statementDate)).thenReturn(LocalDate.parse("2004-01-01"));

TransactionListPostProcessor classUnderTest = new TransactionListPostProcessor(strategy);

List<TransactionGroup> transactions = Lists.newArrayList(
new TransactionGroup(StatementLine.of(new GeneralField(StatementLine.FIELD_TAG_61, "030901" + "0830" + "CR123,45NSTOabcdef//xyz")), null),
new TransactionGroup(StatementLine.of(new GeneralField(StatementLine.FIELD_TAG_61, "030901" + "0101" + "CR123,45NSTOabcdef//xyz")), null)
);

List<TransactionGroup> adjustedTransactions = classUnderTest.adjustEntryDates(transactions, statementDate);
SoftAssertions softly = new SoftAssertions();
softly.assertThat(adjustedTransactions).hasSize(2);
softly.assertThat(adjustedTransactions.get(0).getStatementLine().getEntryDate()).isEqualTo(LocalDate.parse("2003-08-30"));
softly.assertThat(adjustedTransactions.get(1).getStatementLine().getEntryDate()).isEqualTo(LocalDate.parse("2004-01-01"));
softly.assertThat(adjustedTransactions.get(0)).withFailMessage("Post processor should not modify input").isNotSameAs(transactions.get(0));
softly.assertThat(adjustedTransactions.get(1)).withFailMessage("Post processor should not modify input").isNotSameAs(transactions.get(1));
softly.assertAll();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.qoomon.banking.swift.submessage.field.subfield;

import org.junit.Test;

import java.time.LocalDate;
import java.time.MonthDay;

import static org.assertj.core.api.Assertions.assertThat;

public class ShortestDeltaEntryDateResolutionStrategyTest {
private final EntryDateResolutionStrategy strategy = new ShortestDeltaEntryDateResolutionStrategy();

@Test
public void of_WHEN_smallest_delta_between_entry_date_and_value_date_does_not_span_December_THEN_entry_year_is_same_as_value_year() {
// When
LocalDate entryDate = strategy.resolve(MonthDay.parse("--05-12"), LocalDate.parse("2016-03-31"));

// Then
assertThat(entryDate).isEqualTo(LocalDate.parse("2016-05-12"));
}

@Test
public void of_WHEN_smallest_delta_does_not_span_December_and_entry_date_before_value_date_THEN_entry_year_is_same_as_value_year() {
// When
LocalDate entryDate = strategy.resolve(MonthDay.parse("--03-12"), LocalDate.parse("2016-05-31"));

// Then
assertThat(entryDate).isEqualTo(LocalDate.parse("2016-03-12"));
}

@Test
public void of_WHEN_smallest_delta_spans_December_and_entry_date_before_value_date_THEN_entry_year_is_next_year() {
// When
LocalDate entryDate = strategy.resolve(MonthDay.parse("--01-12"), LocalDate.parse("2015-10-31"));

// Then
assertThat(entryDate).isEqualTo(LocalDate.parse("2016-01-12"));
}

@Test
public void of_WHEN_smallest_delta_spans_December_and_entry_date_after_value_date_THEN_entry_year_is_previous_year() {
// When
LocalDate entryDate = strategy.resolve(MonthDay.parse("--11-12"), LocalDate.parse("2016-03-31"));

// Then
assertThat(entryDate).isEqualTo(LocalDate.parse("2015-11-12"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.Stream;

Expand Down Expand Up @@ -55,7 +56,38 @@ public void parse_WHEN_parse_valid_file_RETURN_message() throws Exception {
MT940Page MT940Page = pageList.get(0);
SoftAssertions softly = new SoftAssertions();
softly.assertThat(MT940Page.getTransactionGroupList()).hasSize(3);
softly.assertThat(MT940Page.getTransactionGroupList()).hasSize(3);
softly.assertAll();
}

@Test
public void parse_WHEN_parse_valid_file_SHOULD_adjust_transactions_entry_dates_year_according_to_closing_balance_date() throws Exception {

// Given
String mt940MessageText = "" +
":20:02618\n" +
":21:123456/DEV\n" +
":25:6-9412771\n" +
":28C:00102\n" +
":60F:C000103USD672,\n" +
":61:0309280827D880,FTRFBPHP/081203/0003//59512112915002\n" +
":86:same year\n" +
":61:0309300120D880,FTRFBPHP/081203/0003//59512112915002\n" +
":86:next year\n" +
":62F:C030901USD987,\n" +
"-";

MT940PageReader classUnderTest = new MT940PageReader(new StringReader(mt940MessageText));

// When
List<MT940Page> pageList = TestUtils.collectUntilNull(classUnderTest::read);

// Then
assertThat(pageList).hasSize(1);
MT940Page MT940Page = pageList.get(0);
SoftAssertions softly = new SoftAssertions();
softly.assertThat(MT940Page.getTransactionGroupList().get(0).getStatementLine().getEntryDate()).isEqualTo(LocalDate.parse("2003-08-27"));
softly.assertThat(MT940Page.getTransactionGroupList().get(1).getStatementLine().getEntryDate()).isEqualTo(LocalDate.parse("2004-01-20"));
softly.assertAll();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
import com.qoomon.banking.TestUtils;
import com.qoomon.banking.swift.message.exception.SwiftMessageParseException;
import com.qoomon.banking.swift.submessage.field.FloorLimitIndicator;
import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark;
import com.qoomon.banking.swift.submessage.mt940.MT940Page;
import com.qoomon.banking.swift.submessage.mt940.MT940PageReader;
import org.assertj.core.api.SoftAssertions;
import org.joda.money.BigMoney;
import org.joda.money.CurrencyUnit;
import org.junit.Test;

import java.io.FileReader;
Expand All @@ -20,6 +18,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.Stream;

Expand Down Expand Up @@ -68,6 +67,37 @@ public void parse_WHEN_parse_valid_file_RETURN_message() throws Exception {
assertThat(MT942Page.getStatementNumber().getSequenceNumber()).contains("1");
}

@Test
public void parse_WHEN_parse_valid_file_SHOULD_adjust_transactions_entry_dates_year_according_to_date_time_indicator() throws Exception {

// Given
String mt942MessageText = "" +
":20:02761\n" +
":25:6-9412771\n" +
":28C:1/1\n" +
":34F:USD123,\n" +
":13D:0303012359+0500\n" +
":61:0302280127D880,FTRFBPHP/081203/0003//59512112915002\n" +
":86:same year\n" +
":61:0312011120D880,FTRFBPHP/081203/0003//59512112915002\n" +
":86:previous year\n" +
":90D:75475USD123,\n" +
":90C:75475USD123,\n" +
"-";

MT942PageReader classUnderTest = new MT942PageReader(new StringReader(mt942MessageText));

// When
List<MT942Page> pageList = TestUtils.collectUntilNull(classUnderTest::read);

// Then
MT942Page MT942Page = pageList.get(0);
SoftAssertions softly = new SoftAssertions();
softly.assertThat(MT942Page.getTransactionGroupList().get(0).getStatementLine().getEntryDate()).isEqualTo(LocalDate.parse("2003-01-27"));
softly.assertThat(MT942Page.getTransactionGroupList().get(1).getStatementLine().getEntryDate()).isEqualTo(LocalDate.parse("2002-11-20"));
softly.assertAll();
}

@Test
public void getContent_SHOULD_return_input_text() throws Exception {

Expand Down