From 4825ea5d4ed2e2e76dfd2929ae820831b0fcafd1 Mon Sep 17 00:00:00 2001 From: Niel de Wet Date: Fri, 4 Dec 2020 12:31:38 +0200 Subject: [PATCH] Introduce EntryDateResolutionStrategy Users of MT940PageReader and MT942PageReader may supply an EntryDateResolutionStrategy to define how statement line entry dates are resolved, based on the value date. The default is EarlierMonthImpliesFollowingYearEntryDateResolutionStrategy. ShortestDeltaEntryDateResolutionStrategy is also provided. --- .../swift/submessage/field/StatementLine.java | 14 +++--- ...lowingYearEntryDateResolutionStrategy.java | 14 ++++++ .../subfield/EntryDateResolutionStrategy.java | 18 +++++++ ...rtestDeltaEntryDateResolutionStrategy.java | 47 ++++++++++++++++++ .../submessage/mt940/MT940PageReader.java | 10 +++- .../submessage/mt942/MT942PageReader.java | 10 +++- .../submessage/field/StatementLineTest.java | 3 +- ...tDeltaEntryDateResolutionStrategyTest.java | 48 +++++++++++++++++++ 8 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/qoomon/banking/swift/submessage/field/subfield/EarlierMonthImpliesFollowingYearEntryDateResolutionStrategy.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/field/subfield/ShortestDeltaEntryDateResolutionStrategyTest.java diff --git a/src/main/java/com/qoomon/banking/swift/submessage/field/StatementLine.java b/src/main/java/com/qoomon/banking/swift/submessage/field/StatementLine.java index 6f47a7c..59b15d5 100644 --- a/src/main/java/com/qoomon/banking/swift/submessage/field/StatementLine.java +++ b/src/main/java/com/qoomon/banking/swift/submessage/field/StatementLine.java @@ -5,9 +5,7 @@ import com.qoomon.banking.swift.notation.FieldNotationParseException; import com.qoomon.banking.swift.notation.SwiftDecimalFormatter; import com.qoomon.banking.swift.notation.SwiftNotation; -import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark; -import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditType; -import com.qoomon.banking.swift.submessage.field.subfield.TransactionTypeIdentificationCode; +import com.qoomon.banking.swift.submessage.field.subfield.*; import java.math.BigDecimal; import java.time.LocalDate; @@ -100,6 +98,10 @@ public StatementLine(LocalDate valueDate, } public static StatementLine of(GeneralField field) throws FieldNotationParseException { + return of(field, new EarlierMonthImpliesFollowingYearEntryDateResolutionStrategy()); + } + + public static StatementLine of(GeneralField field, EntryDateResolutionStrategy entryDateResolutionStrategy) throws FieldNotationParseException { Preconditions.checkArgument(field.getTag().equals(FIELD_TAG_61), "unexpected field tag '%s'", field.getTag()); @@ -110,11 +112,7 @@ public static StatementLine of(GeneralField field) throws FieldNotationParseExce // calculate entry date if (subFields.get(1) != null) { MonthDay entryMonthDay = MonthDay.parse(subFields.get(1), ENTRY_DATE_FORMATTER); - // calculate entry year - int entryYear = entryMonthDay.getMonthValue() >= valueDate.getMonthValue() - ? valueDate.getYear() - : valueDate.getYear() + 1; - entryDate = entryMonthDay.atYear(entryYear); + entryDate = entryDateResolutionStrategy.resolve(entryMonthDay, valueDate); } DebitCreditType debitCreditType; DebitCreditMark debitCreditMark; diff --git a/src/main/java/com/qoomon/banking/swift/submessage/field/subfield/EarlierMonthImpliesFollowingYearEntryDateResolutionStrategy.java b/src/main/java/com/qoomon/banking/swift/submessage/field/subfield/EarlierMonthImpliesFollowingYearEntryDateResolutionStrategy.java new file mode 100644 index 0000000..8e1848d --- /dev/null +++ b/src/main/java/com/qoomon/banking/swift/submessage/field/subfield/EarlierMonthImpliesFollowingYearEntryDateResolutionStrategy.java @@ -0,0 +1,14 @@ +package com.qoomon.banking.swift.submessage.field.subfield; + +import java.time.LocalDate; +import java.time.MonthDay; + +public class EarlierMonthImpliesFollowingYearEntryDateResolutionStrategy implements EntryDateResolutionStrategy { + @Override + public LocalDate resolve(MonthDay entryMonthDay, LocalDate valueDate) { + int entryYear = entryMonthDay.getMonthValue() >= valueDate.getMonthValue() + ? valueDate.getYear() + : valueDate.getYear() + 1; + return entryMonthDay.atYear(entryYear); + } +} 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..6163f35 --- /dev/null +++ b/src/main/java/com/qoomon/banking/swift/submessage/field/subfield/EntryDateResolutionStrategy.java @@ -0,0 +1,18 @@ +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: sub field 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 the given value date. + */ +public interface EntryDateResolutionStrategy { + LocalDate resolve(MonthDay entryMonthDay, LocalDate valueDate); +} 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..c4fe74a --- /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 value 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 value date. Otherwise + * it is either the previous year if the entry date is before the value date on the circular timeline, or the next year + * if the entry date is after the value 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 valueDate) { + int e = LocalDate.from(entryMonthDay.adjustInto(valueDate)).getDayOfYear(); + int v = valueDate.getDayOfYear(); + int lengthOfYear = valueDate.lengthOfYear(); + + int valueToEntryDelta = v - e; + boolean valueAfterEntry = valueToEntryDelta > 0; + int shortestDelta = min(abs(valueToEntryDelta), lengthOfYear - abs(valueToEntryDelta)); + + boolean spans31December = valueAfterEntry ? + e + shortestDelta != v : + v + shortestDelta != e; + + int entryYear; + if (!spans31December) { + entryYear = valueDate.getYear(); + } else { + if (valueAfterEntry) { + entryYear = valueDate.getYear() + 1; + } else { + entryYear = valueDate.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..663dbbd 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 @@ -7,6 +7,8 @@ import com.qoomon.banking.swift.submessage.PageSeparator; import com.qoomon.banking.swift.submessage.exception.PageParserException; import com.qoomon.banking.swift.submessage.field.*; +import com.qoomon.banking.swift.submessage.field.subfield.EarlierMonthImpliesFollowingYearEntryDateResolutionStrategy; +import com.qoomon.banking.swift.submessage.field.subfield.EntryDateResolutionStrategy; import java.io.Reader; import java.util.LinkedList; @@ -19,13 +21,19 @@ public class MT940PageReader extends PageReader { private final SwiftFieldReader fieldReader; + private final EntryDateResolutionStrategy entryDateResolutionStrategy; public MT940PageReader(Reader textReader) { + this(textReader, new EarlierMonthImpliesFollowingYearEntryDateResolutionStrategy()); + } + + public MT940PageReader(Reader textReader, EntryDateResolutionStrategy entryDateResolutionStrategy) { Preconditions.checkArgument(textReader != null, "textReader can't be null"); this.fieldReader = new SwiftFieldReader(textReader); + this.entryDateResolutionStrategy = entryDateResolutionStrategy; } @Override @@ -94,7 +102,7 @@ public MT940Page read() throws SwiftMessageParseException { break; } case StatementLine.FIELD_TAG_61: { - StatementLine statementLine = StatementLine.of(currentField); + StatementLine statementLine = StatementLine.of(currentField, entryDateResolutionStrategy); transactionList.add(new TransactionGroup(statementLine, null)); nextValidFieldSet = ImmutableSet.of( InformationToAccountOwner.FIELD_TAG_86, 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..33a8bc7 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 @@ -8,6 +8,8 @@ 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.EarlierMonthImpliesFollowingYearEntryDateResolutionStrategy; +import com.qoomon.banking.swift.submessage.field.subfield.EntryDateResolutionStrategy; import org.joda.money.BigMoney; import org.joda.money.CurrencyUnit; @@ -25,13 +27,19 @@ public class MT942PageReader extends PageReader { private final SwiftFieldReader fieldReader; + private final EntryDateResolutionStrategy entryDateResolutionStrategy; public MT942PageReader(Reader textReader) { + this(textReader, new EarlierMonthImpliesFollowingYearEntryDateResolutionStrategy()); + } + + public MT942PageReader(Reader textReader, EntryDateResolutionStrategy entryDateResolutionStrategy) { Preconditions.checkArgument(textReader != null, "textReader can't be null"); this.fieldReader = new SwiftFieldReader(textReader); + this.entryDateResolutionStrategy = entryDateResolutionStrategy; } @Override @@ -149,7 +157,7 @@ public MT942Page read() throws SwiftMessageParseException { break; } case StatementLine.FIELD_TAG_61: { - StatementLine statementLine = StatementLine.of(currentField); + StatementLine statementLine = StatementLine.of(currentField, entryDateResolutionStrategy); transactionList.add(new TransactionGroup(statementLine, null)); nextValidFieldSet = ImmutableSet.of( StatementLine.FIELD_TAG_61, diff --git a/src/test/java/com/qoomon/banking/swift/submessage/field/StatementLineTest.java b/src/test/java/com/qoomon/banking/swift/submessage/field/StatementLineTest.java index d8a89c4..f9de196 100644 --- a/src/test/java/com/qoomon/banking/swift/submessage/field/StatementLineTest.java +++ b/src/test/java/com/qoomon/banking/swift/submessage/field/StatementLineTest.java @@ -2,6 +2,7 @@ import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark; import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditType; +import com.qoomon.banking.swift.submessage.field.subfield.EarlierMonthImpliesFollowingYearEntryDateResolutionStrategy; import com.qoomon.banking.swift.submessage.field.subfield.TransactionTypeIdentificationCode; import org.assertj.core.api.Assertions; import org.junit.Test; @@ -22,7 +23,7 @@ public void of() throws Exception { GeneralField generalField = new GeneralField(StatementLine.FIELD_TAG_61, "160130" + "C" + "R" + "123,456" + "NSTO" + "abcdef" + "//xyz" + "\nfoobar"); // When - StatementLine field = StatementLine.of(generalField); + StatementLine field = StatementLine.of(generalField, new EarlierMonthImpliesFollowingYearEntryDateResolutionStrategy()); // Then assertThat(field.getTag()).isEqualTo(generalField.getTag()); 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