Skip to content

Commit

Permalink
fix(api): fix cash withdrawl/deposit transaction import from Nordigen (
Browse files Browse the repository at this point in the history
  • Loading branch information
VMelnalksnis committed Oct 21, 2024
1 parent 00dae24 commit 95833bb
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 71 deletions.
4 changes: 4 additions & 0 deletions docs/changelog.html
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ <h3>Fixed</h3>
Transfer, purchase and link lists not expanding in
<a href="https://github.com/VMelnalksnis/Gnomeshade/pull/1414">#1414</a>
</li>
<li>
Cash withdrawl/deposit transaction import from Nordigen in
<a href="https://github.com/VMelnalksnis/Gnomeshade/pull/1444">#1444</a>
</li>
</ul>
</section>

Expand Down
2 changes: 1 addition & 1 deletion docs/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
</url>
<url>
<loc>https://www.gnomeshade.org/changelog</loc>
<lastmod>2024-10-10</lastmod>
<lastmod>2024-10-21</lastmod>
</url>
</urlset>
51 changes: 2 additions & 49 deletions source/Gnomeshade.WebApi/V1/Controllers/NordigenController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@

using NodaTime;

using VMelnalksnis.ISO20022DotNet.MessageSets.BankToCustomerCashManagement.V2.AccountReport;
using VMelnalksnis.NordigenDotNet;
using VMelnalksnis.NordigenDotNet.Accounts;
using VMelnalksnis.NordigenDotNet.Requisitions;
Expand Down Expand Up @@ -200,52 +199,6 @@ await _transferRepository.AddAsync(
return Ok(results);
}

private static CreditDebitCode GetCreditDebitCode(BookedTransaction bookedTransaction) => bookedTransaction.AdditionalInformation switch
{
"PURCHASE" => CreditDebitCode.DBIT,
"INWARD TRANSFER" => CreditDebitCode.CRDT,
"INWARD CLEARING PAYMENT" => CreditDebitCode.CRDT,
"INWARD INSTANT PAYMENT" => CreditDebitCode.CRDT,
"RETURN OF PURCHASE" => CreditDebitCode.CRDT,
"CARD FEE" => CreditDebitCode.DBIT,
"BALANCE ENQUIRY FEE" => CreditDebitCode.DBIT,
"OUTWARD TRANSFER" => CreditDebitCode.DBIT,
"OUTWARD INSTANT PAYMENT" => CreditDebitCode.DBIT,
"INTEREST PAYMENT" => CreditDebitCode.DBIT,
"REIMBURSEMENT OF COMMISSION" => CreditDebitCode.DBIT,
"PRINCIPAL REPAYMENT" => CreditDebitCode.DBIT,
"CASH DEPOSIT" => CreditDebitCode.DBIT,
"CASH WITHDRAWAL" => CreditDebitCode.CRDT,
"LOAN DRAWDOWN" => CreditDebitCode.CRDT,
var information when information?.StartsWith("INWARD", StringComparison.OrdinalIgnoreCase) ?? false => CreditDebitCode.CRDT,
var information when information?.StartsWith("OUTWARD", StringComparison.OrdinalIgnoreCase) ?? false => CreditDebitCode.DBIT,
_ => bookedTransaction.BankTransactionCode switch
{
"PMNT" => CreditDebitCode.DBIT,

// This will leak all data about the transaction into logs, but that should not be an issue while self-hosting
// While only some fields are needed when this fails, those fields contain private information anyway
_ => throw new ArgumentOutOfRangeException(nameof(bookedTransaction.AdditionalInformation), bookedTransaction, "Failed to determine transaction type"),
},
};

private static (string? Domain, string? Family, string? SubFamily) GetCode(string? bankTransactionCode)
{
if (string.IsNullOrWhiteSpace(bankTransactionCode))
{
return (null, null, null);
}

var codes = bankTransactionCode.Split('-');
return codes switch
{
[var domain] => (domain, null, null),
[var domain, var family] => (domain, family, null),
[var domain, var family, var subFamily] => (domain, family, subFamily),
_ => throw new ArgumentOutOfRangeException(nameof(bankTransactionCode), bankTransactionCode, "Unexpected bank transaction code structure"),
};
}

private async Task<(TransactionEntity Transaction, TransferEntity Transfer)> Translate(
DbTransaction dbTransaction,
AccountReportResultBuilder resultBuilder,
Expand All @@ -258,14 +211,14 @@ private static (string? Domain, string? Family, string? SubFamily) GetCode(strin
_logger.ParsingTransaction(bookedTransaction);

var amount = Math.Abs(bookedTransaction.TransactionAmount.Amount);
var (domainCode, familyCode, subFamilyCode) = GetCode(bookedTransaction.BankTransactionCode);
var (domainCode, familyCode, subFamilyCode) = bookedTransaction.GetCode();

var importableTransaction = new ImportableTransaction(
bookedTransaction.TransactionId,
bookedTransaction.EntryReference,
amount,
bookedTransaction.TransactionAmount.Currency,
GetCreditDebitCode(bookedTransaction),
bookedTransaction.GetCreditDebitCode(),
bookedTransaction.BookingDate!.Value.AtStartOfDayInZone(dateTimeZone).ToInstant(),
bookedTransaction.ValueDate?.AtStartOfDayInZone(dateTimeZone).ToInstant(),
bookedTransaction.UnstructuredInformation,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2021 Valters Melnalksnis
// Licensed under the GNU Affero General Public License v3.0 or later.
// See LICENSE.txt file in the project root for full license information.

using System;

using VMelnalksnis.ISO20022DotNet.MessageSets.BankToCustomerCashManagement.V2.AccountReport;
using VMelnalksnis.NordigenDotNet.Accounts;

namespace Gnomeshade.WebApi.V1.Importing;

/// <summary>Helper methods for translating Nordigen transaction data to common format.</summary>
public static class BookedTransactionExtensions
{
/// <summary>Gets the ISO20022 compatible <see cref="CreditDebitCode"/> of the booked transaction.</summary>
/// <param name="transaction">The transaction for which to get the code.</param>
/// <returns>ISO20022 credit debit code of the transaction.</returns>
/// <exception cref="ArgumentOutOfRangeException">Could not determine the code for the transaction.</exception>
public static CreditDebitCode GetCreditDebitCode(this BookedTransaction transaction) => transaction.AdditionalInformation switch
{
"PURCHASE" => CreditDebitCode.DBIT,
"INWARD TRANSFER" => CreditDebitCode.CRDT,
"INWARD CLEARING PAYMENT" => CreditDebitCode.CRDT,
"INWARD INSTANT PAYMENT" => CreditDebitCode.CRDT,
"RETURN OF PURCHASE" => CreditDebitCode.CRDT,
"CARD FEE" => CreditDebitCode.DBIT,
"BALANCE ENQUIRY FEE" => CreditDebitCode.DBIT,
"OUTWARD TRANSFER" => CreditDebitCode.DBIT,
"OUTWARD INSTANT PAYMENT" => CreditDebitCode.DBIT,
"INTEREST PAYMENT" => CreditDebitCode.DBIT,
"REIMBURSEMENT OF COMMISSION" => CreditDebitCode.DBIT,
"PRINCIPAL REPAYMENT" => CreditDebitCode.DBIT,
"CASH DEPOSIT" => CreditDebitCode.CRDT,

Check warning on line 33 in source/Gnomeshade.WebApi/V1/Importing/BookedTransactionExtensions.cs

View check run for this annotation

Codecov / codecov/patch

source/Gnomeshade.WebApi/V1/Importing/BookedTransactionExtensions.cs#L21-L33

Added lines #L21 - L33 were not covered by tests
"CASH WITHDRAWAL" => CreditDebitCode.DBIT,
"LOAN DRAWDOWN" => CreditDebitCode.DBIT,

Check warning on line 35 in source/Gnomeshade.WebApi/V1/Importing/BookedTransactionExtensions.cs

View check run for this annotation

Codecov / codecov/patch

source/Gnomeshade.WebApi/V1/Importing/BookedTransactionExtensions.cs#L35

Added line #L35 was not covered by tests
var information when information?.StartsWith("INWARD", StringComparison.OrdinalIgnoreCase) ?? false => CreditDebitCode.CRDT,
var information when information?.StartsWith("OUTWARD", StringComparison.OrdinalIgnoreCase) ?? false => CreditDebitCode.DBIT,
_ => transaction.GetCode() switch
{
("PMNT", _, _) => CreditDebitCode.DBIT,

// This will leak all data about the transaction into logs, but that should not be an issue while self-hosting
// While only some fields are needed when this fails, those fields contain private information anyway
_ => throw new ArgumentOutOfRangeException(
nameof(transaction),
transaction,
"Failed to determine transaction type"),

Check warning on line 47 in source/Gnomeshade.WebApi/V1/Importing/BookedTransactionExtensions.cs

View check run for this annotation

Codecov / codecov/patch

source/Gnomeshade.WebApi/V1/Importing/BookedTransactionExtensions.cs#L44-L47

Added lines #L44 - L47 were not covered by tests
},
};

/// <summary>Gets the ISO20022 compatible payment codes of the booked transaction.</summary>
/// <param name="transaction">The transaction for which to get the payment codes.</param>
/// <returns>ISO2022 payment code of the transaction.</returns>
/// <exception cref="ArgumentOutOfRangeException"><see cref="Transaction.BankTransactionCode"/> does not contain a valid payment code.</exception>
public static (string? Domain, string? Family, string? SubFamily) GetCode(this BookedTransaction transaction)
{
var bankTransactionCode = transaction.BankTransactionCode;
if (string.IsNullOrWhiteSpace(bankTransactionCode))
{
return (null, null, null);
}

var codes = bankTransactionCode.Split('-');
return codes switch
{
[var domain] => (domain, null, null),
[var domain, var family] => (domain, family, null),

Check warning on line 67 in source/Gnomeshade.WebApi/V1/Importing/BookedTransactionExtensions.cs

View check run for this annotation

Codecov / codecov/patch

source/Gnomeshade.WebApi/V1/Importing/BookedTransactionExtensions.cs#L67

Added line #L67 was not covered by tests
[var domain, var family, var subFamily] => (domain, family, subFamily),
_ => throw new ArgumentOutOfRangeException(
nameof(transaction),
transaction,
"Unexpected bank transaction code structure"),

Check warning on line 72 in source/Gnomeshade.WebApi/V1/Importing/BookedTransactionExtensions.cs

View check run for this annotation

Codecov / codecov/patch

source/Gnomeshade.WebApi/V1/Importing/BookedTransactionExtensions.cs#L69-L72

Added lines #L69 - L72 were not covered by tests
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
// Licensed under the GNU Affero General Public License v3.0 or later.
// See LICENSE.txt file in the project root for full license information.

using Ardalis.SmartEnum;
using System.Diagnostics.CodeAnalysis;

using JetBrains.Annotations;
using Ardalis.SmartEnum;

namespace Gnomeshade.WebApi.V1.Importing.TransactionCodes;

/// <summary>
/// Highest definition level to identify the sub-ledger.
/// The domain defines the business area of the underlying transaction (e.g., payments, securities...).
/// </summary>
[UsedImplicitly(ImplicitUseKindFlags.Access, ImplicitUseTargetFlags.Members)]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public sealed class Domain : SmartEnum<Domain>
{
/// <summary>
Expand Down Expand Up @@ -66,7 +66,7 @@ public sealed class Domain : SmartEnum<Domain>

/// <summary>
/// The Trade Services domain provides the bank transaction codes related to
/// all of the Trade Services operations that need to be reported in the statements.
/// all the Trade Services operations that need to be reported in the statements.
/// </summary>
public static readonly Domain TradeServices = new("TRAD", 8);

Expand Down
10 changes: 4 additions & 6 deletions source/Gnomeshade.WebApi/V1/Importing/TransactionCodes/Family.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@
// Licensed under the GNU Affero General Public License v3.0 or later.
// See LICENSE.txt file in the project root for full license information.

using Ardalis.SmartEnum;
using System.Diagnostics.CodeAnalysis;

using JetBrains.Annotations;
using Ardalis.SmartEnum;

// ReSharper disable StringLiteralTypo

namespace Gnomeshade.WebApi.V1.Importing.TransactionCodes;

/// <summary>
/// Medium definition level: e.g. type of payments: credit transfer, direct debit.
/// </summary>
[UsedImplicitly(ImplicitUseKindFlags.Access, ImplicitUseTargetFlags.Members)]
/// <summary>Medium definition level: e.g. type of payments: credit transfer, direct debit.</summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public class Family : SmartEnum<Family>
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@
// See LICENSE.txt file in the project root for full license information.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

using Ardalis.SmartEnum;

using JetBrains.Annotations;

#pragma warning disable CS1591

// ReSharper disable StringLiteralTypo

namespace Gnomeshade.WebApi.V1.Importing.TransactionCodes;

/// <summary>Lowest definition level: e.g. type of cheques: drafts, etc.</summary>
[UsedImplicitly(ImplicitUseKindFlags.Access, ImplicitUseTargetFlags.Members)]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public sealed class SubFamily : SmartEnum<SubFamily>
{
public static readonly SubFamily NotAvailable = new("NTAV", 1);
Expand Down Expand Up @@ -48,8 +47,5 @@ private SubFamily(string name, int value)
{
}

internal static IEnumerable<SubFamily> BankSubFamilies { get; } = new[]
{
Charges, Fees, CashWithdrawl, CashDeposit,
};
internal static IEnumerable<SubFamily> BankSubFamilies { get; } = [Charges, Fees];
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ protected TransactionImportService(
PreferredCurrencyId = currency.Id,
Name = bankBic,
Bic = bankBic,
Currencies = new() { new() { CurrencyId = currency.Id } },
Currencies = [new() { CurrencyId = currency.Id }],
};

var accountId = await _accountUnitOfWork.AddWithCounterpartyAsync(account, dbTransaction);
Expand Down Expand Up @@ -163,7 +163,7 @@ protected TransactionImportService(
PreferredCurrencyId = currency.Id,
Iban = iban,
AccountNumber = iban,
Currencies = new() { new() { CurrencyId = currency.Id } },
Currencies = [new() { CurrencyId = currency.Id }],
};

var accountId = await _accountUnitOfWork.AddAsync(account, dbTransaction);
Expand Down Expand Up @@ -294,7 +294,7 @@ protected TransactionImportService(
Name = otherAccountName,
Iban = otherAccountIban,
AccountNumber = otherAccountIban,
Currencies = new() { new() { CurrencyId = otherCurrency.Id } },
Currencies = [new() { CurrencyId = otherCurrency.Id }],
};

var accountId = await _accountUnitOfWork.AddWithCounterpartyAsync(accountToCreate, dbTransaction);
Expand All @@ -318,7 +318,7 @@ protected TransactionImportService(
OwnerId = user.Id,
Name = ReservedNames.Unidentified,
PreferredCurrencyId = currency.Id,
Currencies = new() { new() { CurrencyId = currency.Id } },
Currencies = [new() { CurrencyId = currency.Id }],
};

var accountId = await _accountUnitOfWork.AddWithCounterpartyAsync(account, dbTransaction);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2021 Valters Melnalksnis
// Licensed under the GNU Affero General Public License v3.0 or later.
// See LICENSE.txt file in the project root for full license information.

using Gnomeshade.WebApi.V1.Importing;

using VMelnalksnis.ISO20022DotNet.MessageSets.BankToCustomerCashManagement.V2.AccountReport;
using VMelnalksnis.NordigenDotNet.Accounts;

namespace Gnomeshade.WebApi.Tests.V1.Importing;

public sealed class BookedTransactionExtensionsTests
{
[TestCase("CASH WITHDRAWAL", "PMNT-CCRD-CWDL", CreditDebitCode.DBIT)]
public void GetCreditDebitCode(string? information, string? code, CreditDebitCode creditDebitCode)
{
new BookedTransaction { AdditionalInformation = information, BankTransactionCode = code }
.GetCreditDebitCode()
.Should()
.Be(creditDebitCode);
}

[TestCase(null, null, null, null)]
[TestCase("PMNT-CCRD-CWDL", "PMNT", "CCRD", "CWDL")]
public void GetCode(string? code, string? domain, string? family, string? subFamily)
{
new BookedTransaction { BankTransactionCode = code }
.GetCode()
.Should()
.Be((domain, family, subFamily));
}
}

0 comments on commit 95833bb

Please sign in to comment.