diff --git a/Schaad.Finance.Api/CreditCardProvider.cs b/Schaad.Finance.Api/CreditCardProvider.cs index cac7bc3..6a862be 100644 --- a/Schaad.Finance.Api/CreditCardProvider.cs +++ b/Schaad.Finance.Api/CreditCardProvider.cs @@ -2,6 +2,7 @@ { public enum CreditCardProvider { - Cembra + CembraCsv, + CembraPdf } } diff --git a/Schaad.Finance.Api/IPdfParsingService.cs b/Schaad.Finance.Api/IPdfParsingService.cs new file mode 100644 index 0000000..d4fb9e7 --- /dev/null +++ b/Schaad.Finance.Api/IPdfParsingService.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace Schaad.Finance.Api +{ + public interface IPdfParsingService + { + IReadOnlyList ExtractText(string file, int page); + } +} \ No newline at end of file diff --git a/Schaad.Finance.Tests/CreditCardStatementServiceTest.cs b/Schaad.Finance.Tests/CreditCardStatementServiceTest.cs index 2bc3618..0e1171b 100644 --- a/Schaad.Finance.Tests/CreditCardStatementServiceTest.cs +++ b/Schaad.Finance.Tests/CreditCardStatementServiceTest.cs @@ -1,6 +1,8 @@ -using NUnit.Framework; +using Moq; +using NUnit.Framework; using Schaad.Finance.Api; using Schaad.Finance.Services; +using System.Collections.Generic; using System.Linq; namespace Schaad.Finance.Tests @@ -13,16 +15,34 @@ namespace Schaad.Finance.Tests [SetUp] public void Startup() { - service = new CreditCardStatementService(); + var pdfParsingServiceMock = new Mock(); + pdfParsingServiceMock.Setup(t => t.ExtractText(It.IsAny(), It.IsAny())).Returns(() => new List() + { + " Einkaufs-Datum", + " 13.07.2019 14.07.2019 Clay Schaad 13.70", + " 13.07.2019 14.07.2019 Clay Schaad2 23.50", + " dummy test", + " Saldo per " + }); + service = new CreditCardStatementService(pdfParsingServiceMock.Object); } [Test] - public void ReadFile_Cembra() + public void ReadFile_CembraCsv() { - var transactions = service.ReadFile(CreditCardProvider.Cembra, @"Data\cembra.csv", System.Text.Encoding.UTF8); + var transactions = service.ReadFile(CreditCardProvider.CembraCsv, @"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)); } + + [Test] + public void ReadFile_CembraPdf() + { + var transactions = service.ReadFile(CreditCardProvider.CembraPdf, "", System.Text.Encoding.UTF8); + + Assert.That(transactions.Count, Is.EqualTo(2)); + Assert.That(transactions.Sum(t => t.Amount), Is.EqualTo(37.20m)); + } } } diff --git a/Schaad.Finance.Tests/Schaad.Finance.Tests.csproj b/Schaad.Finance.Tests/Schaad.Finance.Tests.csproj index f1f376e..93fb855 100644 --- a/Schaad.Finance.Tests/Schaad.Finance.Tests.csproj +++ b/Schaad.Finance.Tests/Schaad.Finance.Tests.csproj @@ -8,6 +8,7 @@ + @@ -22,15 +23,6 @@ - - - ..\dlls\Schaad.Web.dll - - - ..\dlls\Schaad.Web.Api.dll - - - Always diff --git a/Schaad.Finance/Schaad.Finance.csproj b/Schaad.Finance/Schaad.Finance.csproj index 7fb32c6..ba289fe 100644 --- a/Schaad.Finance/Schaad.Finance.csproj +++ b/Schaad.Finance/Schaad.Finance.csproj @@ -5,6 +5,7 @@ + diff --git a/Schaad.Finance/Services/CreditCardStatementService.cs b/Schaad.Finance/Services/CreditCardStatementService.cs index c9750a7..21bf9f0 100644 --- a/Schaad.Finance/Services/CreditCardStatementService.cs +++ b/Schaad.Finance/Services/CreditCardStatementService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -11,19 +12,29 @@ namespace Schaad.Finance.Services { public class CreditCardStatementService : ICreditCardStatementService { + private readonly IPdfParsingService pdfParsingService; + + public CreditCardStatementService(IPdfParsingService pdfParsingService) + { + this.pdfParsingService = pdfParsingService; + } + public IReadOnlyList ReadFile(CreditCardProvider creditCardProvider, string filePath, Encoding encoding) { switch (creditCardProvider) { - case CreditCardProvider.Cembra: - return ParseCembra(filePath, encoding); + case CreditCardProvider.CembraCsv: + return ParseCembraCsv(filePath, encoding); + + case CreditCardProvider.CembraPdf: + return ParseCembraPdf(filePath); default: throw new NotImplementedException($"Unnknown procider {creditCardProvider}"); } } - private IReadOnlyList ParseCembra(string filePath, Encoding encoding) + private IReadOnlyList ParseCembraCsv(string filePath, Encoding encoding) { using (var reader = new StreamReader(filePath, encoding)) { @@ -35,5 +46,129 @@ namespace Schaad.Finance.Services } } } + + private IReadOnlyList ParseCembraPdf(string filePath) + { + var text = pdfParsingService.ExtractText(filePath, 2); + + var transactionList = new List(); + + var startReading = false; + foreach (var line in text) + { + var trimmedLine = line.Trim(); + if (trimmedLine.StartsWith("Einkaufs-Datum")) + { + startReading = true; + continue; + } + + if (trimmedLine.StartsWith("Saldo per")) + { + break; + } + + if (startReading) + { + var transaction = ParseLine(trimmedLine); + if (transaction != null) + { + transactionList.Add(transaction); + } + } + } + + return transactionList; + } + + private CreditCardTransaction ParseLine(string line) + { + var nextPart = GetNextPart(line, 10); + + if (nextPart.part == null) + { + return null; + } + + var purchaseDate = ParseDateTime(nextPart.part); + + if (purchaseDate.HasValue == false) + { + return null; + } + + nextPart = GetNextPart(nextPart.rest, 10); + var bookingDate = ParseDateTime(nextPart.part); + + if (bookingDate.HasValue == false) + { + return null; + } + + nextPart = GetNextPartUntilSpace(nextPart.rest, 2); + var transactionText = nextPart.part; + + nextPart = GetNextPartUntilSpace(nextPart.rest, 2); + var amount = nextPart.part; + + return new CreditCardTransaction + { + BookingDate = bookingDate.Value, + TransactionDate = purchaseDate.Value, + Amount = decimal.Parse(amount), + Transaction = transactionText + }; + } + + private (string part, string rest) GetNextPart(string line, int length) + { + if (line.Length < length) + { + return (null, null); + } + + var part = line.Substring(0, length); + var rest = line.Substring(length, line.Length - length).TrimStart(); + return (part, rest); + } + + private (string part, string rest) GetNextPartUntilSpace(string line, int howManySpaces) + { + var part = ""; + var spaceCount = 0; + foreach (var c in line) + { + if (c == ' ') + { + spaceCount++; + } + else + { + spaceCount = 0; + } + + part += c; + + if (spaceCount == howManySpaces) + { + part = part.Trim(); + break; + } + } + + var rest = line.Substring(part.Length, line.Length - part.Length).TrimStart(); + return (part, rest); + } + + private DateTime? ParseDateTime(string dateString) + { + var deCH = new CultureInfo("de-CH"); + DateTime purchaseDate; + if (DateTime.TryParseExact(dateString, "dd.MM.yyyy", deCH, DateTimeStyles.None, out purchaseDate)) + { + return purchaseDate; + } + return null; + } } } diff --git a/Schaad.Finance/Services/PdfParsingService.cs b/Schaad.Finance/Services/PdfParsingService.cs new file mode 100644 index 0000000..2a1b389 --- /dev/null +++ b/Schaad.Finance/Services/PdfParsingService.cs @@ -0,0 +1,18 @@ +using Schaad.Finance.Api; +using Spire.Pdf; +using System.Collections.Generic; +using System.Linq; + +namespace Schaad.Finance.Services +{ + public class PdfParsingService : IPdfParsingService + { + public IReadOnlyList ExtractText(string file, int page) + { + var document = new PdfDocument(); + document.LoadFromFile(file); + var text = document.Pages[page - 1].ExtractText(); + return text.Split('\n').ToList(); + } + } +}