From e92da9460fee77c9a08230b497b726ce12644af6 Mon Sep 17 00:00:00 2001 From: Niel de Wet Date: Sat, 8 Jan 2022 16:50:18 +0200 Subject: [PATCH] Fix statement line entry date year based on the statement date --- .../TransactionListPostProcessor.java | 36 ++++++++++++++ .../subfield/EntryDateResolutionStrategy.java | 19 ++++++++ ...rtestDeltaEntryDateResolutionStrategy.java | 47 ++++++++++++++++++ .../submessage/mt940/MT940PageReader.java | 5 +- .../submessage/mt942/MT942PageReader.java | 5 +- .../TransactionListPostProcessorTest.java | 43 +++++++++++++++++ ...tDeltaEntryDateResolutionStrategyTest.java | 48 +++++++++++++++++++ .../submessage/mt940/MT940PageReaderTest.java | 34 ++++++++++++- .../submessage/mt942/MT942PageReaderTest.java | 36 ++++++++++++-- 9 files changed, 267 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/qoomon/banking/swift/submessage/TransactionListPostProcessor.java create mode 100644 src/main/java/com/qoomon/banking/swift/submessage/field/subfield/EntryDateResolutionStrategy.java create mode 100644 src/main/java/com/qoomon/banking/swift/submessage/field/subfield/ShortestDeltaEntryDateResolutionStrategy.java create mode 100644 src/test/java/com/qoomon/banking/swift/submessage/TransactionListPostProcessorTest.java create mode 100644 src/test/java/com/qoomon/banking/swift/submessage/field/subfield/ShortestDeltaEntryDateResolutionStrategyTest.java diff --git a/src/main/java/com/qoomon/banking/swift/submessage/TransactionListPostProcessor.java b/src/main/java/com/qoomon/banking/swift/submessage/TransactionListPostProcessor.java new file mode 100644 index 0000000..e5b03e8 --- /dev/null +++ b/src/main/java/com/qoomon/banking/swift/submessage/TransactionListPostProcessor.java @@ -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 adjustEntryDates(List 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()); + } +} diff --git a/src/main/java/com/qoomon/banking/swift/submessage/field/subfield/EntryDateResolutionStrategy.java b/src/main/java/com/qoomon/banking/swift/submessage/field/subfield/EntryDateResolutionStrategy.java new file mode 100644 index 0000000..8c0a54c --- /dev/null +++ b/src/main/java/com/qoomon/banking/swift/submessage/field/subfield/EntryDateResolutionStrategy.java @@ -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); +} diff --git a/src/main/java/com/qoomon/banking/swift/submessage/field/subfield/ShortestDeltaEntryDateResolutionStrategy.java b/src/main/java/com/qoomon/banking/swift/submessage/field/subfield/ShortestDeltaEntryDateResolutionStrategy.java new file mode 100644 index 0000000..6473c5c --- /dev/null +++ b/src/main/java/com/qoomon/banking/swift/submessage/field/subfield/ShortestDeltaEntryDateResolutionStrategy.java @@ -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. + *

+ * 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. + *

+ * 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); + } +} diff --git a/src/main/java/com/qoomon/banking/swift/submessage/mt940/MT940PageReader.java b/src/main/java/com/qoomon/banking/swift/submessage/mt940/MT940PageReader.java index dd1dbd6..2c1b1a8 100644 --- a/src/main/java/com/qoomon/banking/swift/submessage/mt940/MT940PageReader.java +++ b/src/main/java/com/qoomon/banking/swift/submessage/mt940/MT940PageReader.java @@ -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; @@ -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, diff --git a/src/main/java/com/qoomon/banking/swift/submessage/mt942/MT942PageReader.java b/src/main/java/com/qoomon/banking/swift/submessage/mt942/MT942PageReader.java index fd3d2d1..61dc13a 100644 --- a/src/main/java/com/qoomon/banking/swift/submessage/mt942/MT942PageReader.java +++ b/src/main/java/com/qoomon/banking/swift/submessage/mt942/MT942PageReader.java @@ -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; @@ -205,6 +207,7 @@ public MT942Page read() throws SwiftMessageParseException { } } + TransactionListPostProcessor postProcessor = new TransactionListPostProcessor(new ShortestDeltaEntryDateResolutionStrategy()); return new MT942Page( transactionReferenceNumber, relatedReference, @@ -213,7 +216,7 @@ public MT942Page read() throws SwiftMessageParseException { floorLimitIndicatorDebit, floorLimitIndicatorCredit, dateTimeIndicator, - transactionList, + postProcessor.adjustEntryDates(transactionList, dateTimeIndicator.getDateTime().toLocalDate()), transactionSummaryDebit, transactionSummaryCredit, informationToAccountOwner diff --git a/src/test/java/com/qoomon/banking/swift/submessage/TransactionListPostProcessorTest.java b/src/test/java/com/qoomon/banking/swift/submessage/TransactionListPostProcessorTest.java new file mode 100644 index 0000000..7f71069 --- /dev/null +++ b/src/test/java/com/qoomon/banking/swift/submessage/TransactionListPostProcessorTest.java @@ -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 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 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(); + } +} \ No newline at end of file diff --git a/src/test/java/com/qoomon/banking/swift/submessage/field/subfield/ShortestDeltaEntryDateResolutionStrategyTest.java b/src/test/java/com/qoomon/banking/swift/submessage/field/subfield/ShortestDeltaEntryDateResolutionStrategyTest.java new file mode 100644 index 0000000..0d76745 --- /dev/null +++ b/src/test/java/com/qoomon/banking/swift/submessage/field/subfield/ShortestDeltaEntryDateResolutionStrategyTest.java @@ -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")); + } +} \ No newline at end of file diff --git a/src/test/java/com/qoomon/banking/swift/submessage/mt940/MT940PageReaderTest.java b/src/test/java/com/qoomon/banking/swift/submessage/mt940/MT940PageReaderTest.java index 5ab559c..e047fb0 100644 --- a/src/test/java/com/qoomon/banking/swift/submessage/mt940/MT940PageReaderTest.java +++ b/src/test/java/com/qoomon/banking/swift/submessage/mt940/MT940PageReaderTest.java @@ -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; @@ -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 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 diff --git a/src/test/java/com/qoomon/banking/swift/submessage/mt942/MT942PageReaderTest.java b/src/test/java/com/qoomon/banking/swift/submessage/mt942/MT942PageReaderTest.java index 525a912..0d4d3e8 100644 --- a/src/test/java/com/qoomon/banking/swift/submessage/mt942/MT942PageReaderTest.java +++ b/src/test/java/com/qoomon/banking/swift/submessage/mt942/MT942PageReaderTest.java @@ -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; @@ -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; @@ -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 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 {