Skip to content

Commit

Permalink
Implemented NIBC (Dutch bank)
Browse files Browse the repository at this point in the history
  • Loading branch information
VibeNL committed Jan 4, 2024
1 parent fb74903 commit fb6ce99
Show file tree
Hide file tree
Showing 14 changed files with 256 additions and 13 deletions.
6 changes: 3 additions & 3 deletions ConsoleHelper/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using GhostfolioSidekick.FileImporter.DeGiro;
using GhostfolioSidekick.FileImporter.Generic;
using GhostfolioSidekick.FileImporter.Nexo;
using GhostfolioSidekick.FileImporter.NIBC;
using GhostfolioSidekick.FileImporter.ScalableCaptial;
using GhostfolioSidekick.FileImporter.Trading212;
using GhostfolioSidekick.Ghostfolio.API;
Expand All @@ -18,8 +19,6 @@ internal class Program

static void Main(string[] args)
{
Console.WriteLine("Hello, World!");

foreach (var item in args)
{
var split = item.Split('=');
Expand All @@ -33,11 +32,12 @@ static void Main(string[] args)
new AccountMaintainerTask(logger, api, cs),
new CreateManualSymbolTask(logger, api, cs),
new FileImporterTask(logger, api, cs, new IFileImporter[] {
new BitvavoParser(cs, api),
new BunqParser(api),
new DeGiroParser(api),
new GenericParser(api),
new NexoParser(cs, api),
new BitvavoParser(cs, api),
new NIBCParser(api),
new ScalableCapitalParser(api),
new Trading212Parser(api)
}),
Expand Down
127 changes: 127 additions & 0 deletions GhostfolioSidekick.UnitTests/FileImporter/NIBC/NIBCParserTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using AutoFixture;
using FluentAssertions;
using GhostfolioSidekick.FileImporter.NIBC;
using GhostfolioSidekick.Ghostfolio.API;
using GhostfolioSidekick.Model;
using Moq;

namespace GhostfolioSidekick.UnitTests.FileImporter.NIBC
{
public class NIBCParserTests
{
readonly Mock<IGhostfolioAPI> api;

public NIBCParserTests()
{
api = new Mock<IGhostfolioAPI>();
}

[Fact]
public async Task CanParseActivities_TestFiles_True()
{
// Arrange
var parser = new NIBCParser(api.Object);

foreach (var file in Directory.GetFiles("./FileImporter/TestFiles/NIBC/", "*.csv", SearchOption.AllDirectories))
{
// Act
var canParse = await parser.CanParseActivities(new[] { file });

// Assert
canParse.Should().BeTrue($"File {file} cannot be parsed");
}
}

[Fact]
public async Task ConvertActivitiesForAccount_SingleDeposit_Converted()
{
// Arrange
var parser = new NIBCParser(api.Object);
var fixture = new Fixture();

var account = fixture.Build<Account>().With(x => x.Balance, Balance.Empty(DefaultCurrency.EUR)).Create();

api.Setup(x => x.GetAccountByName(account.Name)).ReturnsAsync(account);

// Act
account = await parser.ConvertActivitiesForAccount(account.Name, new[] { "./FileImporter/TestFiles/NIBC/CashTransactions/single_deposit.csv" });

// Assert
account.Balance.Current(DummyPriceConverter.Instance).Should().BeEquivalentTo(new Money(DefaultCurrency.EUR, 250M, new DateTime(2020, 01, 27, 0, 0, 0, DateTimeKind.Utc)));
}

[Fact]
public async Task ConvertActivitiesForAccount_SingleWithdrawal_Converted()
{
// Arrange
var parser = new NIBCParser(api.Object);
var fixture = new Fixture();

var account = fixture.Build<Account>().With(x => x.Balance, Balance.Empty(DefaultCurrency.EUR)).Create();

api.Setup(x => x.GetAccountByName(account.Name)).ReturnsAsync(account);

// Act
account = await parser.ConvertActivitiesForAccount(account.Name, new[] { "./FileImporter/TestFiles/NIBC/CashTransactions/single_withdrawal.csv" });

// Assert
account.Balance.Current(DummyPriceConverter.Instance).Should().BeEquivalentTo(new Money(DefaultCurrency.EUR, -10000M, new DateTime(2022, 01, 06, 0, 0, 0, DateTimeKind.Utc)));
}

[Fact]
public async Task ConvertActivitiesForAccount_SingleInterest_Converted()
{
// Arrange
var parser = new NIBCParser(api.Object);
var fixture = new Fixture();

var account = fixture.Build<Account>().With(x => x.Balance, Balance.Empty(DefaultCurrency.EUR)).Create();

api.Setup(x => x.GetAccountByName(account.Name)).ReturnsAsync(account);

// Act
account = await parser.ConvertActivitiesForAccount(account.Name, new[] { "./FileImporter/TestFiles/NIBC/CashTransactions/single_interest.csv" });

// Assert
account.Balance.Current(DummyPriceConverter.Instance).Should().BeEquivalentTo(new Money(DefaultCurrency.EUR, 0.51M, new DateTime(2021, 09, 30, 0, 0, 0, DateTimeKind.Utc)));
account.Activities.Should().BeEquivalentTo(new[] { new Activity {
Asset = null,
Comment = "Transaction Reference: [C1I30IN0000A000Q]",
Date = new DateTime(2021, 09, 30, 0,0,0, DateTimeKind.Utc),
Fees = Enumerable.Empty<Money>(),
Quantity = 1m,
ActivityType = ActivityType.Interest,
UnitPrice = new Money(DefaultCurrency.EUR, 0.51M, new DateTime(2021, 09, 30, 0,0,0, DateTimeKind.Utc)),
ReferenceCode = "C1I30IN0000A000Q"
} });
}

[Fact]
public async Task ConvertActivitiesForAccount_SingleBonusInterest_Converted()
{
// Arrange
var parser = new NIBCParser(api.Object);
var fixture = new Fixture();

var account = fixture.Build<Account>().With(x => x.Balance, Balance.Empty(DefaultCurrency.EUR)).Create();

api.Setup(x => x.GetAccountByName(account.Name)).ReturnsAsync(account);

// Act
account = await parser.ConvertActivitiesForAccount(account.Name, new[] { "./FileImporter/TestFiles/NIBC/CashTransactions/single_bonus_interest.csv" });

// Assert
account.Balance.Current(DummyPriceConverter.Instance).Should().BeEquivalentTo(new Money(DefaultCurrency.EUR, 1.1M, new DateTime(2021, 6, 30, 0, 0, 0, DateTimeKind.Utc)));
account.Activities.Should().BeEquivalentTo(new[] { new Activity {
Asset = null,
Comment = "Transaction Reference: [C1F30IN0000A000QBonus]",
Date = new DateTime(2021,6,30, 0,0,0, DateTimeKind.Utc),
Fees = Enumerable.Empty<Money>(),
Quantity = 1m,
ActivityType = ActivityType.Interest,
UnitPrice = new Money(DefaultCurrency.EUR, 1.1M, new DateTime(2021,6,30, 0,0,0, DateTimeKind.Utc)),
ReferenceCode = "C1F30IN0000A000QBonus"
} });
}
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Timezone,Date,Time,Type,Currency,Amount,Price (EUR),EUR received / paid,Fee currency,Fee amount,Status,Transaction ID,Address
Europe/Amsterdam,2023-04-21,08:48:55,deposit,EUR,1,,,EUR,0,Completed,796a11aa-998f-4425-a503-07543300cda1,NL53***24
Europe/Amsterdam,2023-04-21,08:48:55,deposit,EUR,1,,,EUR,0,Completed,796a11aa-998f-4425-a503-07543300cda1,********
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Timezone,Date,Time,Type,Currency,Amount,Price (EUR),EUR received / paid,Fee currency,Fee amount,Status,Transaction ID,Address
Europe/Amsterdam,2023-10-24,21:23:37,withdrawal,EUR,-101.88,,,EUR,0,Completed,1e651f3e-e5be-4a87-a8fa-00e6832bdbc7,NL53***24
Europe/Amsterdam,2023-10-24,21:23:37,withdrawal,EUR,-101.88,,,EUR,0,Completed,1e651f3e-e5be-4a87-a8fa-00e6832bdbc7,********
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Nr v/d rekening :;XXXXXXXXXXXXX;
Nr v/d verrichting;Boekhdk. datum;Beschrijving;Bedrag v/d verrichting;Munt;Valutadatum;Rekening tegenpartij;Naam v/d tegenpartij :;Mededeling 1 :;Mededeling 2 :;Ref. v/d verrichting
0000009;30-06-2021;Bonusrente;1,10;EUR;01-07-2021;XXXXXXXXXXXXX; ; ; ;C1F30IN0000A000Q
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Nr v/d rekening :;XXXXXXXXXXXXX;
Nr v/d verrichting;Boekhdk. datum;Beschrijving;Bedrag v/d verrichting;Munt;Valutadatum;Rekening tegenpartij;Naam v/d tegenpartij :;Mededeling 1 :;Mededeling 2 :;Ref. v/d verrichting
0000001;27-01-2020;Inkomende overboeking;250,00;EUR;27-01-2020;XXXXXXXXXXX;XXXXXXXXXX;Eerste storing; ;C0A27XM003000782
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Nr v/d rekening :;XXXXXXXXXXXXX;
Nr v/d verrichting;Boekhdk. datum;Beschrijving;Bedrag v/d verrichting;Munt;Valutadatum;Rekening tegenpartij;Naam v/d tegenpartij :;Mededeling 1 :;Mededeling 2 :;Ref. v/d verrichting
0000016;30-09-2021;Renteuitkering;0,51;EUR;01-10-2021;XXXXXXXXXXXXX; ; ; ;C1I30IN0000A000Q
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Nr v/d rekening :;XXXXXXXXXXXXX;
Nr v/d verrichting;Boekhdk. datum;Beschrijving;Bedrag v/d verrichting;Munt;Valutadatum;Rekening tegenpartij;Naam v/d tegenpartij :;Mededeling 1 :;Mededeling 2 :;Ref. v/d verrichting
0000001;06-01-2022;Uitgaande overboeking;-10.000,00;EUR;06-01-2022;XXXXXXXXXXXXXXXX;XXXXXXXXXXXXXXXX;Some comment; ;C2A06CW1G00A044D
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Action,Time,ISIN,Ticker,Name,No. of shares,Price / share,Currency (Price / share),Exchange rate,Currency (Result),Total,Currency (Total),Withholding tax,Currency (Withholding tax),Charge amount,Currency (Charge amount),Stamp duty reserve tax,Currency (Stamp duty reserve tax),Notes,ID,French transaction tax,Currency (French transaction tax),Currency conversion fee,Currency (Currency conversion fee)
Withdrawal,2023-11-17 05:49:12.337,,,,,,,,,1000.00,"EUR",,,,,,,"Sent to Bank Account ABNANL2A / NL53ABNA0420435824",5d72520a-388c-428a-90bf-6d9fcff55534,,,,
Withdrawal,2023-11-17 05:49:12.337,,,,,,,,,1000.00,"EUR",,,,,,,"Sent to Bank Account ABNANL2A / ********",5d72520a-388c-428a-90bf-6d9fcff55534,,,,
12 changes: 12 additions & 0 deletions GhostfolioSidekick.UnitTests/GhostfolioSidekick.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,18 @@
<None Update="FileImporter\TestFiles\Nexo\Specials\single_referralbonus_pending.csv">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="FileImporter\TestFiles\NIBC\CashTransactions\single_deposit.csv">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="FileImporter\TestFiles\NIBC\CashTransactions\single_bonus_interest.csv">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="FileImporter\TestFiles\NIBC\CashTransactions\single_interest.csv">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="FileImporter\TestFiles\NIBC\CashTransactions\single_withdrawal.csv">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="FileImporter\TestFiles\ScalableCapital\BuyOrders\SingleBuy\rkk.csv">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
Expand Down
69 changes: 69 additions & 0 deletions GhostfolioSidekick/FileImporter/NIBC/NIBCParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using CsvHelper.Configuration;
using GhostfolioSidekick.Ghostfolio.API;
using System.Globalization;

namespace GhostfolioSidekick.FileImporter.NIBC
{
public class NIBCParser : RecordBaseImporter<NIBCRecord>
{
public NIBCParser(IGhostfolioAPI api) : base(api)
{
}

protected override async Task<IEnumerable<Model.Activity>> ConvertOrders(NIBCRecord record, Model.Account account, IEnumerable<NIBCRecord> allRecords)
{
var activityType = GetActivityType(record);

if (activityType == null)
{
return [];
}

var id = record.TransactionID + (record.Description == "Bonusrente" ? "Bonus" : string.Empty);

var order = new Model.Activity(
activityType.Value,
null,
record.Date,
1,
new Model.Money(CurrencyHelper.ParseCurrency(record.Currency), Math.Abs(record.Amount), record.Date),
null,
TransactionReferenceUtilities.GetComment(id),
id
);

return new[] { order };
}

private Model.ActivityType? GetActivityType(NIBCRecord record)
{
if (record.Description == "Inkomende overboeking")
{
return Model.ActivityType.CashDeposit;
}

if (record.Description == "Uitgaande overboeking")
{
return Model.ActivityType.CashWithdrawal;
}

if (record.Description == "Renteuitkering" || record.Description == "Bonusrente")
{
return Model.ActivityType.Interest;
}

return null;
}

protected override CsvConfiguration GetConfig()
{
return new CsvConfiguration(CultureInfo.InvariantCulture)
{
HasHeaderRecord = true,
CacheFields = true,
Delimiter = ";",
ShouldSkipRecord = (r) => r.Row[0].StartsWith("Nr v/d rekening"),
};
}
}
}
24 changes: 24 additions & 0 deletions GhostfolioSidekick/FileImporter/NIBC/NIBCRecord.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using CsvHelper.Configuration.Attributes;

namespace GhostfolioSidekick.FileImporter.NIBC
{
public class NIBCRecord
{
[Name("Boekhdk. datum")]
[Format("dd-MM-yyyy")]
public DateTime Date { get; set; }

[Name("Beschrijving")]
public string Description { get; set; }

[Name("Bedrag v/d verrichting")]
[CultureInfo("nl-NL")]
public decimal Amount { get; set; }

[Name("Munt")]
public string Currency { get; set; }

[Name("Ref. v/d verrichting")]
public string TransactionID { get; set; }
}
}
4 changes: 3 additions & 1 deletion GhostfolioSidekick/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using GhostfolioSidekick.FileImporter.DeGiro;
using GhostfolioSidekick.FileImporter.Generic;
using GhostfolioSidekick.FileImporter.Nexo;
using GhostfolioSidekick.FileImporter.NIBC;
using GhostfolioSidekick.FileImporter.ScalableCaptial;
using GhostfolioSidekick.FileImporter.Trading212;
using GhostfolioSidekick.Ghostfolio.API;
Expand Down Expand Up @@ -45,11 +46,12 @@ static async Task Main(string[] args)
services.AddScoped<IScheduledWork, MarketDataMaintainerTask>();
services.AddScoped<IScheduledWork, AccountMaintainerTask>();

services.AddScoped<IFileImporter, BitvavoParser>();
services.AddScoped<IFileImporter, BunqParser>();
services.AddScoped<IFileImporter, DeGiroParser>();
services.AddScoped<IFileImporter, GenericParser>();
services.AddScoped<IFileImporter, NexoParser>();
services.AddScoped<IFileImporter, BitvavoParser>();
services.AddScoped<IFileImporter, NIBCParser>();
services.AddScoped<IFileImporter, ScalableCapitalParser>();
services.AddScoped<IFileImporter, Trading212Parser>();

Expand Down
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,6 @@ This settings does control if a workaround is used for dust (very small amount o
This settings is the threshold of local currency of the asset if a value is considered dust.
Any holding less than this value is considered dust and will be added to the last sell/send activity.

#### Note
The following parser also map to a list of known cryptocurrencies. i.e. BTC becomes bitcoin

- Nexo
- Bitvavo

#### Platform and Account
Creates platforms and accounts if not yet created

Expand Down Expand Up @@ -129,13 +123,16 @@ Maintaining symbols in ghostfolio
| assetClass | one of: 'CASH', 'COMMODITY', 'EQUITY', 'FIXED_INCOME', 'REAL_ESTATE' | Same list as Ghostfolio |

### Supported formats
The goal is to support all platforms as best as possible. Due to the continuous growth of Ghostfolio, new features may be added when possible.

| Platform | Source of the files | Buy | Sell | Dividend | Interest & Cash balance |
|--|--|--|--|--|--|
| Generic importer | See below | X | X | X | X |
| Trading 212 | Export of transaction history | X | X | X | X |
| De Giro | Export of account history | X | X | X | X |
| Scalable Capital | The CSV files of the Baader bank. Type WUM and RKK | X | X | X | X |
| Bunq (bank) | Export CSV (Semicolom delimited) | - | - | - | X |
| NIBC (bank) | Export CSV (Semicolom delimited) | - | - | - | X |
| Nexo (Experimental) | Export of transaction history | X | - | - | X |
| Bitvavo (Experimental) | Export of transaction history | X | X | - | X |

Expand Down

0 comments on commit fb6ce99

Please sign in to comment.