This commit is contained in:
Claudio Schaad 2019-07-06 15:12:08 +02:00
commit 6da5187e4b
31 changed files with 15457 additions and 0 deletions

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
################################################################################
# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
################################################################################
/Schaad.Finance/bin/Debug
/Schaad.Finance/obj
*.user
/Schaad.Finance.Api/bin/Debug/netstandard2.0
/Schaad.Finance.Api/obj
/Schaad.Finance.Tests/bin/Debug/netcoreapp2.0
/Schaad.Finance.Tests/obj
/Schaad.Finance.Api/.vs/Schaad.Finances.Api
/.vs/Schaad.Finance

View file

@ -0,0 +1,42 @@
using System.Collections.Generic;
namespace Schaad.Finance.Api.AccountStatements
{
public class AccountStatement
{
/// <summary>
/// The account number (IBAN)
/// </summary>
public string AccountNumber { get; set; }
/// <summary>
/// Account Currency
/// </summary>
public string Currency { get; set; }
/// <summary>
/// The starting balance
/// </summary>
public Balance StartBalance { get; set; }
/// <summary>
/// The end balance
/// </summary>
public Balance EndBalance { get; set; }
/// <summary>
/// Summary of all credit transactions (camt)
/// </summary>
public TransactionSummary CreditTransactionSummary { get; set; }
/// <summary>
/// Summary of all debit transactions (camt)
/// </summary>
public TransactionSummary DebitTransactionSummary { get; set; }
/// <summary>
/// A list of transactions
/// </summary>
public List<Transaction> Transactions { get; set; } = new List<Transaction>();
}
}

View file

@ -0,0 +1,18 @@
namespace Schaad.Finance.Api.AccountStatements
{
public class AccountStatementResult
{
public readonly AccountStatement AccountStatement;
public readonly bool IsSuccess;
public readonly string Error;
public AccountStatementResult(AccountStatement accountStatement, string error)
{
AccountStatement = accountStatement;
Error = error;
IsSuccess = string.IsNullOrEmpty(error);
}
}
}

View file

@ -0,0 +1,25 @@
using System;
namespace Schaad.Finance.Api.AccountStatements
{
/// <summary>
/// Detailed balance info
/// </summary>
public class Balance
{
/// <summary>
/// Date of balance
/// </summary>
public DateTime BookingDate { get; set; }
/// <summary>
/// Currency of balance
/// </summary>
public string Currency { get; set; }
/// <summary>
/// Value
/// </summary>
public double Value { get; set; }
}
}

View file

@ -0,0 +1,57 @@
using System;
using System.Security.Cryptography;
using System.Text;
namespace Schaad.Finance.Api.AccountStatements
{
/// <summary>
/// A transaction
/// </summary>
public class Transaction
{
public string Id
{
set
{
id = value;
}
get
{
if (string.IsNullOrEmpty(id))
{
return GetHashString(ValueDate.ToString("yyyyMMdd") + Value + Text);
}
else
{
return id;
}
}
}
private string id;
public DateTime ValueDate { get; set; }
public DateTime BookingDate { get; set; }
public double Value { get; set; }
public string Text { get; set; }
private string GetHashString(string inputString)
{
var sb = new StringBuilder();
foreach (byte b in GetHash(inputString))
{
sb.Append(b.ToString("X2"));
}
return sb.ToString();
}
private byte[] GetHash(string inputString)
{
var algorithm = MD5.Create(); //or use SHA1.Create();
return algorithm.ComputeHash(Encoding.UTF8.GetBytes(inputString));
}
}
}

View file

@ -0,0 +1,9 @@
namespace Schaad.Finance.Api.AccountStatements
{
public class TransactionSummary
{
public int NumberOfTransaction { get; set; }
public decimal Sum { get; set; }
}
}

View file

@ -0,0 +1,7 @@
namespace Schaad.Finance.Api
{
public enum CreditCardProvider
{
Cembra
}
}

View file

@ -0,0 +1,13 @@
using System;
namespace Schaad.Finance.Api.Datasets
{
public class CreditCardTransaction
{
public DateTime BookingDate { get; set; }
public DateTime TransactionDate { get; set; }
public string Transaction { get; set; }
public string TransactionType { get; set; }
public decimal Amount { get; set; }
}
}

View file

@ -0,0 +1,12 @@
using Schaad.Finance.Api.AccountStatements;
using System.Collections.Generic;
using System.Text;
namespace Schaad.Finance.Api
{
public interface IAccountStatementParsingService
{
List<AccountStatement> ReadFile(string filePath, Encoding encoding);
string ValidateAccountStatement(AccountStatement accountStatement);
}
}

View file

@ -0,0 +1,11 @@
using Schaad.Finance.Api.AccountStatements;
using System.Collections.Generic;
using System.Text;
namespace Schaad.Finance.Api
{
public interface IAccountStatementService
{
List<AccountStatementResult> ReadFile(string filePath, Encoding encoding);
}
}

View file

@ -0,0 +1,11 @@
using Schaad.Finance.Api.Datasets;
using System.Collections.Generic;
using System.Text;
namespace Schaad.Finance.Api
{
public interface ICreditCardStatementService
{
IReadOnlyList<CreditCardTransaction> ReadFile(CreditCardProvider creditCardProvider, string filePath, Encoding encoding);
}
}

View file

@ -0,0 +1,10 @@
using System;
namespace Schaad.Finance.Api
{
public interface IFxService
{
decimal ConvertCurrency(decimal amount, string fromCurrency, string toCurrency, string fixerIoApiKey);
decimal GetFxRate(string fromCurrency, string toCurrency, DateTime date, string fixerIoApiKey);
}
}

View file

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<None Remove="ve-3237.tmp" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,33 @@
using NUnit.Framework;
using Schaad.Finance.Services;
namespace Schaad.Finance.Tests
{
[TestFixture]
public class AccountStatementServiceTest
{
private AccountStatementService service;
[SetUp]
public void Startup()
{
service = new AccountStatementService();
}
[Test]
public void ReadFile_MT940()
{
var statements = service.ReadFile(@"Data\mt940.sta", System.Text.Encoding.UTF8);
Assert.That(statements.TrueForAll(s => s.IsSuccess));
}
[Test]
public void ReadFile_CAMT053()
{
var statements = service.ReadFile(@"Data\camt053.xml", System.Text.Encoding.UTF8);
Assert.That(statements.TrueForAll(s => s.IsSuccess));
}
}
}

View file

@ -0,0 +1,28 @@
using NUnit.Framework;
using Schaad.Finance.Api;
using Schaad.Finance.Services;
using System.Linq;
namespace Schaad.Finance.Tests
{
[TestFixture]
public class CreditCardStatementServiceTest
{
private CreditCardStatementService service;
[SetUp]
public void Startup()
{
service = new CreditCardStatementService();
}
[Test]
public void ReadFile_Cembra()
{
var transactions = service.ReadFile(CreditCardProvider.Cembra, @"Data\cembra.csv", System.Text.Encoding.UTF8);
Assert.That(transactions.Count, Is.EqualTo(4));
Assert.That(transactions.Sum(t => t.Amount), Is.EqualTo(144.60m));
}
}
}

View file

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.04">
<BkToCstmrStmt>
<GrpHdr>
<MsgId>MSG-C053-190706014611-01</MsgId>
<CreDtTm>2019-07-06T00:00:00</CreDtTm>
<MsgPgntn>
<PgNb>1</PgNb>
<LastPgInd>true</LastPgInd>
</MsgPgntn>
<AddtlInf>RECONSTRUCTION</AddtlInf>
</GrpHdr>
<Stmt>
<Id>STM-C053-190706014611-02</Id>
<ElctrncSeqNb>999999</ElctrncSeqNb>
<CreDtTm>2019-07-06T00:00:00</CreDtTm>
<FrToDt>
<FrDtTm>2019-07-01T00:00:00</FrDtTm>
<ToDtTm>2019-07-05T00:00:00</ToDtTm>
</FrToDt>
<CpyDplctInd>DUPL</CpyDplctInd>
<Acct>
<Id>
<IBAN>CH3789144975191379986</IBAN>
</Id>
<Ownr>
<Nm>Schaad Claudio</Nm>
</Ownr>
</Acct>
<Bal>
<Tp>
<CdOrPrtry>
<Cd>OPBD</Cd>
</CdOrPrtry>
</Tp>
<Amt Ccy="CHF">2468.71</Amt>
<CdtDbtInd>CRDT</CdtDbtInd>
<Dt>
<Dt>2019-07-01</Dt>
</Dt>
</Bal>
<Bal>
<Tp>
<CdOrPrtry>
<Cd>CLBD</Cd>
</CdOrPrtry>
</Tp>
<Amt Ccy="CHF">2444.01</Amt>
<CdtDbtInd>CRDT</CdtDbtInd>
<Dt>
<Dt>2019-07-05</Dt>
</Dt>
</Bal>
<TxsSummry>
<TtlDbtNtries>
<NbOfNtries>2</NbOfNtries>
<Sum>24.70</Sum>
</TtlDbtNtries>
</TxsSummry>
<Ntry>
<NtryRef>1532133113</NtryRef>
<Amt Ccy="CHF">15.45</Amt>
<CdtDbtInd>DBIT</CdtDbtInd>
<Sts>BOOK</Sts>
<BookgDt>
<Dt>2019-07-01</Dt>
</BookgDt>
<ValDt>
<Dt>2019-06-28</Dt>
</ValDt>
<BkTxCd>
<Domn>
<Cd>PMNT</Cd>
<Fmly>
<Cd>*</Cd>
<SubFmlyCd>*</SubFmlyCd>
</Fmly>
</Domn>
</BkTxCd>
<NtryDtls>
<TxDtls>
<Amt Ccy="CHF">15.45</Amt>
<CdtDbtInd>DBIT</CdtDbtInd>
<BkTxCd>
<Domn>
<Cd>PMNT</Cd>
<Fmly>
<Cd>*</Cd>
<SubFmlyCd>*</SubFmlyCd>
</Fmly>
</Domn>
</BkTxCd>
<RltdPties/>
<RmtInf>
<Ustrd>Migros M EX Sihlpass</Ustrd>
</RmtInf>
<AddtlTxInf>Warenbezug und Dienstleistungen
Migros M EX Sihlpass
Maestro Karte
Kartennummer xxx
28.06.2019 18:43</AddtlTxInf>
</TxDtls>
</NtryDtls>
<AddtlNtryInf>Warenbezug und Dienstleistungen
Migros M EX Sihlpass
Maestro Karte
Kartennummer xxx
28.06.2019 18:43</AddtlNtryInf>
</Ntry>
<Ntry>
<NtryRef>1531481701</NtryRef>
<Amt Ccy="CHF">9.25</Amt>
<CdtDbtInd>DBIT</CdtDbtInd>
<Sts>BOOK</Sts>
<BookgDt>
<Dt>2019-07-01</Dt>
</BookgDt>
<ValDt>
<Dt>2019-06-28</Dt>
</ValDt>
<BkTxCd>
<Domn>
<Cd>PMNT</Cd>
<Fmly>
<Cd>*</Cd>
<SubFmlyCd>*</SubFmlyCd>
</Fmly>
</Domn>
</BkTxCd>
<NtryDtls>
<TxDtls>
<Amt Ccy="CHF">9.25</Amt>
<CdtDbtInd>DBIT</CdtDbtInd>
<BkTxCd>
<Domn>
<Cd>PMNT</Cd>
<Fmly>
<Cd>*</Cd>
<SubFmlyCd>*</SubFmlyCd>
</Fmly>
</Domn>
</BkTxCd>
<RltdPties/>
<RmtInf>
<Ustrd>Coop-5013 ZH Maagpla</Ustrd>
</RmtInf>
<AddtlTxInf>Warenbezug und Dienstleistungen
Coop-5013 ZH Maagpla
Maestro Karte
Kartennummer xxx
28.06.2019 06:49</AddtlTxInf>
</TxDtls>
</NtryDtls>
<AddtlNtryInf>Warenbezug und Dienstleistungen
Coop-5013 ZH Maagpla
Maestro Karte
Kartennummer xxx
28.06.2019 06:49</AddtlNtryInf>
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>

View file

@ -0,0 +1,5 @@
Booking Date;Transaction Date;;Transaction;Type;Amount (CHF);
05.07.2019;05.07.2019;;MIGROS WEINFELDEN;Debit;37.00;
04.07.2019;04.07.2019;;MIGROS WEINFELDEN;Debit;46.95;
02.07.2019;02.07.2019;;MIGROS WEINFELDEN;Debit;47.75;
02.07.2019;02.07.2019;;ITUNES.COM/BILL 0800001853 IRL;Debit;12.90;
1 Booking Date Transaction Date Transaction Type Amount (CHF)
2 05.07.2019 05.07.2019 MIGROS WEINFELDEN Debit 37.00
3 04.07.2019 04.07.2019 MIGROS WEINFELDEN Debit 46.95
4 02.07.2019 02.07.2019 MIGROS WEINFELDEN Debit 47.75
5 02.07.2019 02.07.2019 ITUNES.COM/BILL 0800001853 IRL Debit 12.90

View file

@ -0,0 +1,47 @@
:20:STARTUMS
:25:123456789
:28:00001
:NS:22 Schaad Claudio
23Privatkonto
3000000784
314448809
32000
:60F:C190701CHF000000002468,71
:61:1906280701D000000000015,45FMSCNONREF
:NS:01Warenbezug und Dienstleistungen
02Migros M EX Sihlpass
03Maestro Karte
04Kartennummer xxx
0528.06.2019 18:43
06
07
08
09
10
11
12
13
14
15
16
17Warenbezug und Dienstleistungen
:61:1906280701D000000000009,25FMSCNONREF
:NS:01Warenbezug und Dienstleistungen
02Coop-5013 ZH Maagpla
03Maestro Karte
04Kartennummer xxx
0528.06.2019 06:49
06
07
08
09
10
11
12
13
14
15
16
17Warenbezug und Dienstleistungen
:62F:C190705CHF000000002444,01

View file

@ -0,0 +1,35 @@
using NUnit.Framework;
using Schaad.Finance.Services;
namespace Schaad.Finance.Tests
{
[TestFixture]
public class FxServiceTest
{
private FxService service;
private const string FIXERIO_API_KEY = "<insert fixer io api key here>";
[SetUp]
public void Startup()
{
service = new FxService();
}
[Test]
public void GetFxRate_EUR2CHF()
{
var fxRate = service.GetFxRate("EUR", "CHF", new System.DateTime(2019, 07, 01), FIXERIO_API_KEY);
Assert.That(fxRate, Is.EqualTo(1.114262m));
}
[Test]
public void ConvertCurrency_100EUR2CHF()
{
var amount = service.ConvertCurrency(100, "EUR", "CHF", FIXERIO_API_KEY);
Assert.That(amount, Is.GreaterThan(100));
}
}
}

View file

@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="MSTest.TestAdapter" Version="1.4.0" />
<PackageReference Include="MSTest.TestFramework" Version="1.4.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Schaad.Finance\Schaad.Finance.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Data\" />
</ItemGroup>
<ItemGroup>
<Reference Include="Schaad.Web">
<HintPath>..\dlls\Schaad.Web.dll</HintPath>
</Reference>
<Reference Include="Schaad.Web.Api">
<HintPath>..\dlls\Schaad.Web.Api.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<None Update="Data\camt053.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Data\cembra.csv">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Data\mt940.sta">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

37
Schaad.Finance.sln Normal file
View file

@ -0,0 +1,37 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27703.2035
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Schaad.Finance.Api", "Schaad.Finance.Api\Schaad.Finance.Api.csproj", "{C5845C75-0772-4819-B0DB-D57B5AF8921D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Schaad.Finance", "Schaad.Finance\Schaad.Finance.csproj", "{4BCEDAF9-FE9B-49D2-9CAF-A7B06B4FF521}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Schaad.Finance.Tests", "Schaad.Finance.Tests\Schaad.Finance.Tests.csproj", "{C3A9FC28-9CF7-4775-90B7-B9F29DC299FD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C5845C75-0772-4819-B0DB-D57B5AF8921D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5845C75-0772-4819-B0DB-D57B5AF8921D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5845C75-0772-4819-B0DB-D57B5AF8921D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5845C75-0772-4819-B0DB-D57B5AF8921D}.Release|Any CPU.Build.0 = Release|Any CPU
{4BCEDAF9-FE9B-49D2-9CAF-A7B06B4FF521}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4BCEDAF9-FE9B-49D2-9CAF-A7B06B4FF521}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4BCEDAF9-FE9B-49D2-9CAF-A7B06B4FF521}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4BCEDAF9-FE9B-49D2-9CAF-A7B06B4FF521}.Release|Any CPU.Build.0 = Release|Any CPU
{C3A9FC28-9CF7-4775-90B7-B9F29DC299FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C3A9FC28-9CF7-4775-90B7-B9F29DC299FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3A9FC28-9CF7-4775-90B7-B9F29DC299FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3A9FC28-9CF7-4775-90B7-B9F29DC299FD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {38CCE775-5C10-4F00-BA79-EB86BE0BB2B5}
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using Schaad.Finance.Api;
using Schaad.Finance.Api.AccountStatements;
using Schaad.Finance.Formats.AccountStatements.Models;
namespace Schaad.Finance.Formats.AccountStatements
{
/// <summary>
/// Reads CAMT053 format
/// https://www.six-interbank-clearing.com/dam/downloads/de/standardization/iso/swiss-recommendations/implementation-guidelines-camt.pdf
/// </summary>
public class CAMT053 : IAccountStatementParsingService
{
public List<AccountStatement> ReadFile(string filePath, Encoding encoding)
{
var accountStatements = new List<AccountStatement>();
var serializer = new XmlSerializer(typeof(Document));
using (var fileStream = new FileStream(filePath, FileMode.Open))
{
using (var reader = new StreamReader(fileStream, encoding))
{
var document = (Document)serializer.Deserialize(reader);
foreach (var stmt in document.BkToCstmrStmt.Stmt)
{
var accountStatement = new AccountStatement();
accountStatements.Add(accountStatement);
accountStatement.AccountNumber = stmt.Acct.Id.Item.ToString();
accountStatement.Currency = stmt.Acct.Ccy;
foreach (var balance in stmt.Bal)
{
// var balanceAmount = balance.Amt.Value;
// var balanceCurrency = balance.Amt.Ccy;
// var balanceDate = balance.Dt.Item;
// var creditDebit = balance.CdtDbtInd;
var balanceType = (BalanceType12Code)balance.Tp.CdOrPrtry.Item;
if (balanceType == BalanceType12Code.OPBD)
{
accountStatement.StartBalance = new Balance
{
BookingDate = balance.Dt.Item,
Value = (double)balance.Amt.Value,
};
}
else if (balanceType == BalanceType12Code.CLBD)
{
accountStatement.EndBalance = new Balance
{
BookingDate = balance.Dt.Item,
Value = (double)balance.Amt.Value,
};
}
}
if (stmt.TxsSummry != null)
{
if (stmt.TxsSummry.TtlCdtNtries != null)
{
accountStatement.CreditTransactionSummary = new TransactionSummary
{
NumberOfTransaction = int.Parse(stmt.TxsSummry.TtlCdtNtries.NbOfNtries),
Sum = stmt.TxsSummry.TtlCdtNtries.Sum
};
}
else
{
accountStatement.CreditTransactionSummary = new TransactionSummary();
}
if (stmt.TxsSummry.TtlDbtNtries != null)
{
accountStatement.DebitTransactionSummary = new TransactionSummary
{
NumberOfTransaction = int.Parse(stmt.TxsSummry.TtlDbtNtries.NbOfNtries),
Sum = stmt.TxsSummry.TtlDbtNtries.Sum
};
}
else
{
accountStatement.DebitTransactionSummary = new TransactionSummary();
}
}
accountStatement.Transactions = new List<Transaction>();
if (stmt.Ntry != null)
{
foreach (var entry in stmt.Ntry)
{
var transaction = new Transaction();
accountStatement.Transactions.Add(transaction);
// var amount = entry.Amt.Value;
// var currency = entry.Amt.Ccy;
// var valueDate = entry.ValDt.Item;
// var bookingDate = entry.BookgDt.Item;
//var creditDepit = entry.CdtDbtInd;
// var trxId = entry.NtryRef;
// var text = entry.AddtlNtryInf;
transaction.Id = stmt.Id + entry.NtryRef;
transaction.BookingDate = entry.BookgDt.Item;
transaction.Text = entry.AddtlNtryInf;
transaction.Value = (double)entry.Amt.Value;
transaction.ValueDate = entry.ValDt.Item;
if (entry.CdtDbtInd == CreditDebitCode.DBIT)
{
transaction.Value *= -1;
}
}
}
}
}
}
return accountStatements;
}
public string ValidateAccountStatement(AccountStatement accountStatement)
{
if (accountStatement.StartBalance.Currency != accountStatement.EndBalance.Currency)
{
return $"Startbalance currency ({accountStatement.StartBalance.Currency}) does not match endbalance currency ({accountStatement.EndBalance.Currency})";
}
if (accountStatement.CreditTransactionSummary != null && accountStatement.DebitTransactionSummary != null)
{
var transactionSum = Math.Round(accountStatement.Transactions.Sum(t => (decimal)t.Value), 2);
var totalNumber = accountStatement.Transactions.Count;
var transactionSumValidation = accountStatement.CreditTransactionSummary.Sum - accountStatement.DebitTransactionSummary.Sum;
var totalNumberValidation = accountStatement.CreditTransactionSummary.NumberOfTransaction + accountStatement.DebitTransactionSummary.NumberOfTransaction;
if (transactionSum != transactionSumValidation)
{
return $"Transaction sum ({transactionSum}) does not match validation sum ({transactionSumValidation})";
}
if (totalNumber != totalNumberValidation)
{
return $"Transaction count ({totalNumber}) does not match validation count ({totalNumberValidation})";
}
}
else if (accountStatement.StartBalance != null && accountStatement.EndBalance != null)
{
var transactionSum = Math.Round(accountStatement.Transactions.Sum(t => t.Value), 2);
var end = accountStatement.StartBalance.Value + transactionSum;
if (end != accountStatement.EndBalance.Value)
{
return $"Enbalance ({accountStatement.EndBalance.Value}) does not match startbalance ({accountStatement.StartBalance.Value}) + transactions ({transactionSum})";
}
}
return string.Empty;
}
}
}

View file

@ -0,0 +1,195 @@
using Schaad.Finance.Api;
using Schaad.Finance.Api.AccountStatements;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Schaad.Finance.Formats.AccountStatements
{
/// <summary>
/// Reads MT940 format
/// http://www.kontopruef.de/mt940s.shtml
/// </summary>
public class MT940 : IAccountStatementParsingService
{
public List<AccountStatement> ReadFile(string filePath, Encoding encoding)
{
var accountList = new List<AccountStatement>();
AccountStatement account = null;
Transaction currentTransaction = null;
List<string> transactionTexts = null;
foreach (var line in File.ReadLines(filePath, encoding))
{
if (line.Length < 3)
{
continue;
}
var codeValue = ParseLine(line);
switch (codeValue.Item1)
{
// Account
case ":25:":
account = new AccountStatement();
accountList.Add(account);
account.AccountNumber = codeValue.Item2;
break;
// Start balance
case ":60F:":
account.StartBalance = ParseBalance(codeValue.Item2);
break;
// End balance
case ":62F:":
account.EndBalance = ParseBalance(codeValue.Item2);
break;
// Transaction
case ":61:":
// have we an old transaction
if (currentTransaction != null && transactionTexts.Any())
{
currentTransaction.Text = string.Join(", ", transactionTexts);
}
transactionTexts = new List<string>();
currentTransaction = ParseTransaction(codeValue.Item2);
account.Transactions.Add(currentTransaction);
break;
// Transaction text
case ":NS:":
//if (currentTransaction != null) currentTransaction.Text = codeValue.Item2;
break;
}
if (codeValue.Item1.IndexOf(':') == -1 && currentTransaction != null)
{
if (transactionTexts.Contains(codeValue.Item2) == false)
{
transactionTexts.Add(codeValue.Item2);
}
}
}
// have we an old transaction
if (currentTransaction != null && transactionTexts.Any())
{
currentTransaction.Text = string.Join(", ", transactionTexts);
}
return accountList;
}
public string ValidateAccountStatement(AccountStatement accountStatement)
{
if (accountStatement.StartBalance.Currency != accountStatement.EndBalance.Currency)
{
return $"Startbalance currency ({accountStatement.StartBalance.Currency}) does not match endbalance currency ({accountStatement.EndBalance.Currency})";
}
var transactionSum = Math.Round(accountStatement.Transactions.Sum(t => t.Value), 2);
var end = accountStatement.StartBalance.Value + transactionSum;
if (end != accountStatement.EndBalance.Value)
{
return $"Enbalance ({accountStatement.EndBalance.Value}) does not match startbalance ({accountStatement.StartBalance.Value}) + transactions ({transactionSum})";
}
return string.Empty;
}
/// <summary>
/// Parse a line
/// </summary>
/// <param name="line"></param>
/// <returns></returns>
private Tuple<string, string> ParseLine(string line)
{
if (line.StartsWith(":"))
{
var pos = line.IndexOf(':', 1);
var code = line.Substring(0, pos + 1);
return new Tuple<string, string>(code, line.Substring(code.Length));
}
else
return new Tuple<string, string>(line.Substring(0, 2), line.Substring(2));
}
/// <summary>
/// Parse a balance
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private Balance ParseBalance(string value)
{
var balance = new Balance();
bool isDebit = false;
if (value.First() == 'C')
isDebit = false;
else if (value.First() == 'D')
isDebit = true;
value = value.Remove(0, 1);
balance.BookingDate = ParseDate(value.Substring(0, 6));
value = value.Remove(0, 6);
balance.Currency = value.Substring(0, 3);
value = value.Remove(0, 3);
balance.Value = ParseBalanceValue(value);
if (isDebit)
balance.Value *= -1.0;
return balance;
}
private double ParseBalanceValue(string balanceValue)
{
return Double.Parse(balanceValue.Replace(',', '.'));
}
/// <summary>
/// Parse a transaction
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private Transaction ParseTransaction(string value)
{
var transaction = new Transaction();
transaction.ValueDate = ParseDate(value.Substring(0, 6));
value = value.Remove(0, 6);
string year = (transaction.ValueDate.Year - 2000).ToString();
transaction.BookingDate = ParseDate(year + value.Substring(0, 4));
value = value.Remove(0, 4);
bool isDebit = false;
if (value.First() == 'C')
isDebit = false;
else if (value.First() == 'D')
isDebit = true;
else
throw new NotSupportedException("Code not supported: " + value.First());
value = value.Remove(0, 1);
transaction.Value = ParseBalanceValue(value.Substring(0, 15));
value = value.Remove(0, 15);
if (isDebit)
transaction.Value *= -1.0;
return transaction;
}
/// <summary>
/// Parse a date in format: JJMMTT
/// </summary>
/// <param name="dateValue"></param>
/// <returns></returns>
private DateTime ParseDate(string dateValue)
{
if (dateValue.Length != 6)
throw new Exception("Wrong format for date. Need: JJMMTT");
return new DateTime(2000 + Int32.Parse(dateValue.Substring(0, 2)), Int32.Parse(dateValue.Substring(2, 2)), Int32.Parse(dateValue.Substring(4, 2)));
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
using CsvHelper.Configuration;
using Schaad.Finance.Api.Datasets;
namespace Schaad.Finance.Formats.CreditCardProvider
{
class CembraMap : ClassMap<CreditCardTransaction>
{
public CembraMap()
{
Map(m => m.BookingDate).Name("Booking Date");
Map(m => m.TransactionDate).Name("Transaction Date");
Map(m => m.Transaction).Name("Transaction");
Map(m => m.TransactionType).Name("Type");
Map(m => m.Amount).Name("Amount (CHF)");
}
}
}

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="12.1.2" />
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="System.Xml.XmlDocument" Version="4.3.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
<PackageReference Include="System.Runtime.Serialization.Formatters" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Schaad.Finance.Api\Schaad.Finance.Api.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Schaad.Finance.Api;
using Schaad.Finance.Api.AccountStatements;
using Schaad.Finance.Formats.AccountStatements;
namespace Schaad.Finance.Services
{
public class AccountStatementService : IAccountStatementService
{
public List<AccountStatementResult> ReadFile(string filePath, Encoding encoding)
{
IAccountStatementParsingService accountStatementParsingService = null;
var extension = Path.GetExtension(filePath).ToLower();
switch (extension)
{
case ".xml":
accountStatementParsingService = new CAMT053() as IAccountStatementParsingService;
break;
case ".sta":
accountStatementParsingService = new MT940() as IAccountStatementParsingService;
break;
default:
throw new NotImplementedException($"Unknwon ending {extension}");
}
var accountStatementResults = new List<AccountStatementResult>();
var accountStatements = accountStatementParsingService.ReadFile(filePath, encoding);
foreach (var accountStatement in accountStatements)
{
var error = accountStatementParsingService.ValidateAccountStatement(accountStatement);
var accountStatementResult = new AccountStatementResult(accountStatement, error);
accountStatementResults.Add(accountStatementResult);
}
return accountStatementResults;
}
}
}

View file

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using CsvHelper;
using Schaad.Finance.Api;
using Schaad.Finance.Api.Datasets;
namespace Schaad.Finance.Services
{
public class CreditCardStatementService : ICreditCardStatementService
{
public IReadOnlyList<CreditCardTransaction> ReadFile(CreditCardProvider creditCardProvider, string filePath, Encoding encoding)
{
switch (creditCardProvider)
{
case CreditCardProvider.Cembra:
return ParseCembra(filePath, encoding);
default:
throw new NotImplementedException($"Unnknown procider {creditCardProvider}");
}
}
private IReadOnlyList<CreditCardTransaction> ParseCembra(string filePath, Encoding encoding)
{
using (var reader = new StreamReader(filePath, encoding))
{
using (var csv = new CsvReader(reader))
{
csv.Configuration.RegisterClassMap<Formats.CreditCardProvider.CembraMap>();
var records = csv.GetRecords<CreditCardTransaction>();
return records.ToList();
}
}
}
}
}

View file

@ -0,0 +1,81 @@
using Newtonsoft.Json.Linq;
using Schaad.Finance.Api;
using System;
using System.Collections.Generic;
using System.Net;
namespace Schaad.Finance.Services
{
public class FxService : IFxService
{
private Dictionary<string, decimal> fxRateCache;
public FxService()
{
fxRateCache = new Dictionary<string, decimal>();
}
public decimal ConvertCurrency(decimal amount, string fromCurrency, string toCurrency, string fixerIoApiKey)
{
if (fromCurrency == toCurrency)
{
return amount;
}
var fxRate = GetFxRate(fromCurrency, toCurrency, DateTime.Now, fixerIoApiKey);
return Math.Round(fxRate * amount, 2);
}
public decimal GetFxRate(string fromCurrency, string toCurrency, DateTime date, string fixerIoApiKey)
{
if (fromCurrency == toCurrency)
{
return 1.0M;
}
var key = GetKey(fromCurrency, toCurrency, date);
if (fxRateCache.ContainsKey(key) == false)
{
// Base Currency ist EUR
var euroToFrom = GetFx(GetUrl(fromCurrency, date, fixerIoApiKey), fromCurrency);
var euroToTo = GetFx(GetUrl(toCurrency, date, fixerIoApiKey), toCurrency);
fxRateCache[key] = euroToTo / euroToFrom;
}
return fxRateCache[key];
}
private string GetUrl(string currency, DateTime date, string fixerIoApiKey)
{
var url = $"http://data.fixer.io/api/{date:yyyy-MM-dd}?access_key={fixerIoApiKey}&symbols={currency}";
return url;
}
private decimal GetFx(string url, string currency)
{
var response = HttpGet(url);
dynamic stuff = JObject.Parse(response);
return stuff.rates[currency];
}
private string GetKey(string fromCurrency, string toCurrency, DateTime date)
{
return $"{fromCurrency}_{toCurrency}_{date:yyyyMMdd}";
}
private string HttpGet(string url)
{
var request = (HttpWebRequest)WebRequest.Create(url);
var response = request.GetResponseAsync().Result;
string myResponse = "";
using (var sr = new System.IO.StreamReader(response.GetResponseStream()))
{
myResponse = sr.ReadToEnd();
return myResponse;
}
}
}
}