diff --git a/.gitignore b/.gitignore index 1ea8c10..61eaa0b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ bin obj .vs -OfxClientIntegrationTests.cs \ No newline at end of file +OfxClientIntegrationTests.cs +coverage.cobertura.xml +.codecov \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..74f63a7 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,12 @@ + + + + false + cobertura + true + **/OFX_XSD_Generated.cs + + line + total + + \ No newline at end of file diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000..db7db88 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,14 @@ + + + + + all + + + + + + + + + \ No newline at end of file diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..bd80024 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,30 @@ +assembly-versioning-scheme: Major +mode: ContinuousDeployment +next-version: 2.0.0 +increment: Patch +legacy-semver-padding: 1 +build-metadata-padding: 1 +commits-since-version-source-padding: 1 +continuous-delivery-fallback-tag: 'ci' +branches: + master: + regex: master + mode: ContinuousDeployment + tag: '' + increment: inherit + prevent-increment-of-merged-branch-version: true + tag-number-pattern: '[/-](?\d+)[-/]' + pull-request: + regex: (pull|pull\-requests|pr)[/-] + mode: ContinuousDeployment + tag: "dev" + increment: Patch + tag-number-pattern: '[/-](?\d+)[-/]' + develop: + regex: (!master)? + mode: ContinuousDeployment + tag: useBranchName + increment: Patch +ignore: + sha: [] +merge-message-formats: {} diff --git a/README.md b/README.md index d7883fb..5c4d11d 100644 --- a/README.md +++ b/README.md @@ -1 +1,8 @@ -# ofx \ No newline at end of file +# ofx + +[![Nuget](https://img.shields.io/nuget/vpre/Mocoding.Ofx)](https://www.nuget.org/packages/Mocoding.Ofx) +[![Build Status](https://dev.azure.com/mocoding/GitHub/_apis/build/status/mocoding-software.ofx?branchName=master)](https://dev.azure.com/mocoding/GitHub/_build/latest?definitionId=83&branchName=master) +[![Code Coverage](https://img.shields.io/azure-devops/coverage/mocoding/GitHub/83/master)](https://dev.azure.com/mocoding/GitHub/_build?definitionId=83) +![Nuget Downloads](https://img.shields.io/nuget/dt/Mocoding.Ofx) + + diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..759c81d --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,93 @@ +# ASP.NET Core +# Build and test ASP.NET Core projects targeting .NET Core. +# Add steps that run tests, create a NuGet package, deploy, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core + +# name: $(majorVersion).$(minorVersion).$(patchVersion)$(channelVersion)$(buildVersion)$(Rev:.r) + +trigger: + branches: + include: + - master + tags: + include: + - v* + +pool: + vmImage: "ubuntu-18.04" + +variables: + buildConfiguration: "Release" + projectName: "Mocoding.Ofx" + solutionFile: "ofx.sln" + # majorVersion: 2 + # minorVersion: 0 + # patchVersion: 0 + # channelVersion: "-rc" + # buildVersion: $[format('-{0}.build', variables['Build.SourceBranchName'])] + +steps: + - task: UseGitVersion@5 + inputs: + versionSpec: '5.0.0' + useConfigFile: true + configFilePath: 'GitVersion.yml' + + - task: DotNetCoreCLI@2 + displayName: "dotnet restore" + inputs: + command: restore + projects: $(solutionFile) + + - task: DotNetCoreCLI@2 + displayName: "dotnet build" + inputs: + command: build + projects: $(solutionFile) + + - task: DotNetCoreCLI@2 + displayName: "dotnet test" + inputs: + command: test + projects: 'test/**/*.Tests.csproj' + arguments: "--no-build /p:SkipCodeCoverageReport=true /p:Threshold=80 /p:CoverletOutput=$(Agent.TempDirectory)/" + + - task: PublishCodeCoverageResults@1 + displayName: "publish code coverage" + inputs: + codeCoverageTool: "Cobertura" + summaryFileLocation: "$(Agent.TempDirectory)/*.xml" + condition: succeededOrFailed() + + - task: DotNetCoreCLI@2 + displayName: "dotnet pack $(projectName)" + inputs: + command: pack + packagesToPack: "src/$(projectName)/$(projectName).csproj" + configuration: $(buildConfiguration) + packDirectory: "$(Build.StagingDirectory)/$(projectName)" + buildProperties: "Version=$(Build.BuildNumber)" + + - task: DotNetCoreCLI@2 + displayName: "dotnet pack $(projectName).Client" + inputs: + command: pack + packagesToPack: "src/$(projectName).Client/$(projectName).Client.csproj" + configuration: $(buildConfiguration) + packDirectory: "$(Build.StagingDirectory)/$(projectName)" + buildProperties: "Version=$(Build.BuildNumber)" + + - task: DotNetCoreCLI@2 + displayName: "dotnet pack $(projectName).Client.Discover" + inputs: + command: pack + packagesToPack: "src/$(projectName).Client.Discover/$(projectName).Client.Discover.csproj" + configuration: $(buildConfiguration) + packDirectory: "$(Build.StagingDirectory)/$(projectName)" + buildProperties: "Version=$(Build.BuildNumber)" + + - task: PublishBuildArtifacts@1 + displayName: "Publish Artifact: nupkg" + inputs: + PathtoPublish: "$(Build.StagingDirectory)" + ArtifactName: nupkg diff --git a/docker-compose.yaml b/docker-compose.yaml index c1912ce..25df264 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,11 +1,14 @@ version: "2.4" services: - devbox: + devbox: &devbox image: mocoding/ofx command: /bin/sh build: context: ./ dockerfile: docker/Dockerfile volumes: - - .:/app \ No newline at end of file + - .:/app + + test: + <<: *devbox \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 81ed80a..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM microsoft/dotnet:2.2-sdk AS builder -COPY . /app -WORKDIR app -RUN dotnet restore \ No newline at end of file diff --git a/docs/mocoding.ofx.puml b/docs/mocoding.ofx.puml new file mode 100644 index 0000000..feb2169 --- /dev/null +++ b/docs/mocoding.ofx.puml @@ -0,0 +1,64 @@ +@startuml + +skinparam componentStyle uml2 + +' hide class circle +' hide interface circle +' hide abstract circle +' hide enum circle + +' hide fields + +hide interface fields +hide class fields +hide abstract fields +hide enum methods + + +enum OfxVersionEnum { + Version1x= 1, + Version2x= 2, +} + +interface IOfxSerializer +{ + Serialize(model: OFX) : string + Deserialize(intputString: string) : OFX +} + +interface IOfxSerializerFactory { + IOfxSerializer Create(version: OfxVersionEnum) +} + +abstract class BaseSerializer { + + {abstract} Serialize(model:OFX) : string + + {abstract} Deserialize(inputString:string) : OFX + + # SerializeInternal(request:OFX) : string + # DeserializeInternal(input:string) : OFX +} + +class XmlSerializer { + + <> Serialize(model:OFX) : string + + <> Deserialize(inputString:string) : OFX +} + +class SgmlSerializer { + + <> Serialize(model:OFX) : string + + <> Deserialize(inputString:string) : OFX +} + +class DefaultOfxSerializerFactory { + + Create(version:OfxVersionEnum) : IOfxSerializer +} + +IOfxSerializer <|--- BaseSerializer +IOfxSerializerFactory <|--- DefaultOfxSerializerFactory +BaseSerializer <|--- XmlSerializer +BaseSerializer <|--- SgmlSerializer + +IOfxSerializerFactory -right-> IOfxSerializer + +center footer @ Mocoding 2019 + +@enduml \ No newline at end of file diff --git a/ofx.sln b/ofx.sln index 436dac9..4e1d021 100644 --- a/ofx.sln +++ b/ofx.sln @@ -1,13 +1,14 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2010 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29409.12 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{52EC7D92-4F1F-45CD-A25C-ABFC7E46759A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{313ABF6C-A107-4046-A2E0-455E5B681E57}" ProjectSection(SolutionItems) = preProject _stylecop\StyleCop.ruleset = _stylecop\StyleCop.ruleset + test.runsettings = test.runsettings EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{94A1D6B3-9112-4D1B-AA64-1AC6088E8E23}" @@ -16,9 +17,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mocoding.Ofx", "src\Mocodin EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mocoding.Ofx.Tests", "test\Mocoding.Ofx.Tests\Mocoding.Ofx.Tests.csproj", "{DB558166-A4A7-4E32-8285-75D4CC1593AC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mocoding.Ofx.Client", "src\Mocoding.Ofx.Client\Mocoding.Ofx.Client.csproj", "{06F2F625-5121-46CE-A65C-963BB4E9EA3B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mocoding.Ofx.Client", "src\Mocoding.Ofx.Client\Mocoding.Ofx.Client.csproj", "{5D2B5B2B-3551-4E47-8BE8-88C516282F7B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mocoding.Ofx.Client.Tests", "test\Mocoding.Ofx.Client.Tests\Mocoding.Ofx.Client.Tests.csproj", "{2DD397E1-4408-4DE6-85D8-0DBFA8CCFB28}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mocoding.Ofx.Client.Discover", "src\Mocoding.Ofx.Client.Discover\Mocoding.Ofx.Client.Discover.csproj", "{ED991F3D-2F6E-496F-972A-F64B4284381C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -33,15 +34,14 @@ Global {DB558166-A4A7-4E32-8285-75D4CC1593AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DB558166-A4A7-4E32-8285-75D4CC1593AC}.Debug|Any CPU.Build.0 = Debug|Any CPU {DB558166-A4A7-4E32-8285-75D4CC1593AC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DB558166-A4A7-4E32-8285-75D4CC1593AC}.Release|Any CPU.Build.0 = Release|Any CPU - {06F2F625-5121-46CE-A65C-963BB4E9EA3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {06F2F625-5121-46CE-A65C-963BB4E9EA3B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {06F2F625-5121-46CE-A65C-963BB4E9EA3B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {06F2F625-5121-46CE-A65C-963BB4E9EA3B}.Release|Any CPU.Build.0 = Release|Any CPU - {2DD397E1-4408-4DE6-85D8-0DBFA8CCFB28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2DD397E1-4408-4DE6-85D8-0DBFA8CCFB28}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2DD397E1-4408-4DE6-85D8-0DBFA8CCFB28}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2DD397E1-4408-4DE6-85D8-0DBFA8CCFB28}.Release|Any CPU.Build.0 = Release|Any CPU + {5D2B5B2B-3551-4E47-8BE8-88C516282F7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D2B5B2B-3551-4E47-8BE8-88C516282F7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D2B5B2B-3551-4E47-8BE8-88C516282F7B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D2B5B2B-3551-4E47-8BE8-88C516282F7B}.Release|Any CPU.Build.0 = Release|Any CPU + {ED991F3D-2F6E-496F-972A-F64B4284381C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED991F3D-2F6E-496F-972A-F64B4284381C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED991F3D-2F6E-496F-972A-F64B4284381C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED991F3D-2F6E-496F-972A-F64B4284381C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -49,8 +49,8 @@ Global GlobalSection(NestedProjects) = preSolution {F2FB00D8-476F-41D8-9BBD-E706D3066C95} = {52EC7D92-4F1F-45CD-A25C-ABFC7E46759A} {DB558166-A4A7-4E32-8285-75D4CC1593AC} = {94A1D6B3-9112-4D1B-AA64-1AC6088E8E23} - {06F2F625-5121-46CE-A65C-963BB4E9EA3B} = {52EC7D92-4F1F-45CD-A25C-ABFC7E46759A} - {2DD397E1-4408-4DE6-85D8-0DBFA8CCFB28} = {94A1D6B3-9112-4D1B-AA64-1AC6088E8E23} + {5D2B5B2B-3551-4E47-8BE8-88C516282F7B} = {52EC7D92-4F1F-45CD-A25C-ABFC7E46759A} + {ED991F3D-2F6E-496F-972A-F64B4284381C} = {52EC7D92-4F1F-45CD-A25C-ABFC7E46759A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C40D6E6A-BEDD-4CB9-9E43-EB415BE6B7BD} diff --git a/src/Mocoding.Ofx.Client.Discover/DiscoverProtocolUtils.cs b/src/Mocoding.Ofx.Client.Discover/DiscoverProtocolUtils.cs new file mode 100644 index 0000000..2561b9a --- /dev/null +++ b/src/Mocoding.Ofx.Client.Discover/DiscoverProtocolUtils.cs @@ -0,0 +1,117 @@ +using System; +using System.IO; +using System.Linq; +using System.Net.Security; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using Mocoding.Ofx.Client.Interfaces; + +[assembly: InternalsVisibleTo("Mocoding.Ofx.Tests")] + +namespace Mocoding.Ofx.Client.Discover +{ + /// + /// Discover Credit Card specific implementation of protocol utils. + /// + /// + public class DiscoverProtocolUtils : OfxProtocolUtils + { + /// + /// Initializes a new instance of the class. + /// + /// The requests. + public DiscoverProtocolUtils(IOfxRequestLocator requests) : base(requests) + { + + } + + /// + /// Gets the client uid. + /// + /// The user identifier. + /// + /// Client ID + /// + public override string GetClientUid(string userId) + { + return null; + } + + /// + /// Creates and executes POST request to specified url with specified body content. + /// + /// The URL. + /// The content. + /// + public override async Task PostRequest(Uri url, string content) + { + var server = url.Host; + var httpRequest = PrepareRequest(server, url.Port, content); + StringBuilder httpResponse = new StringBuilder(); + using (var client = new TcpClient()) + { + await client.ConnectAsync(server, url.Port); + if (url.Scheme == "https") + { + using (var sslStream = new SslStream(client.GetStream(), true)) + { + await sslStream.AuthenticateAsClientAsync(server); + var toSend = Encoding.ASCII.GetBytes(httpRequest); + await sslStream.WriteAsync(toSend, 0, toSend.Length); + await sslStream.FlushAsync(); + await ReadResponse(client, sslStream, httpResponse); + } + } + else + { + using (var stream = client.GetStream()) + { + var toSend = Encoding.ASCII.GetBytes(httpRequest); + stream.Write(toSend, 0, toSend.Length); + await ReadResponse(client, stream, httpResponse); + } + } + } + var httpContent = httpResponse.ToString(); + var contentIndex = httpContent.IndexOf("\r\n\r\n", StringComparison.Ordinal) + 4; + var endIndex = httpContent.LastIndexOf(">", StringComparison.Ordinal); + return httpContent.Substring(contentIndex, endIndex - contentIndex + 1); + } + + private static async Task ReadResponse(TcpClient client, Stream sslStream, StringBuilder httpResponse) + { + var chunk = string.Empty; + if (sslStream.CanRead) + do + { + var received = new byte[client.ReceiveBufferSize]; + var count = await sslStream.ReadAsync(received, 0, client.ReceiveBufferSize); + chunk = Encoding.ASCII.GetString(received.Take(count).ToArray()); + httpResponse.Append(chunk); + } while (!chunk.Contains("")); + } + + internal static string PrepareRequest(string server, int port, string content) + { + var builder = new StringBuilder(); + + builder.AppendLine($"POST / HTTP/1.1"); + builder.AppendLine("Content-Type: application/x-ofx"); + builder.AppendLine($"Host: {server}:{port}"); + builder.AppendLine($"Content-Length: {content.Length}"); + builder.AppendLine("Connection: close"); + // builder.AppendLine("User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1 Safari/605.1.15"); + // builder.AppendLine("Accept: */*"); + // builder.AppendLine("Accept-Language: en-us"); + // builder.AppendLine("Cache-Control: no-cache"); + + builder.AppendLine(); + builder.Append(content); + + var httpRequest = builder.ToString(); + return httpRequest; + } + } +} diff --git a/src/Mocoding.Ofx.Client.Discover/DiscoverProtocolUtilsFactory.cs b/src/Mocoding.Ofx.Client.Discover/DiscoverProtocolUtilsFactory.cs new file mode 100644 index 0000000..0ff1530 --- /dev/null +++ b/src/Mocoding.Ofx.Client.Discover/DiscoverProtocolUtilsFactory.cs @@ -0,0 +1,26 @@ +using Mocoding.Ofx.Client.Defaults; +using Mocoding.Ofx.Client.Interfaces; + +namespace Mocoding.Ofx.Client.Discover +{ + /// + /// Factory implementation that includes discover specific protocol utils + /// + /// + public class DiscoverProtocolUtilsFactory : IProtocolUtilsFactory + { + /// + /// Creates based on specified Financial Institution ID. + /// + /// Financial Institution ID. + /// + /// Concrete protocol methods implementation. + /// + public IProtocolUtils Create(string fid) + { + return fid == "7101" // Discover Credit Card fid + ? new DiscoverProtocolUtils(new DefaultOfxRequestLocator()) + : new OfxProtocolUtils(new DefaultOfxRequestLocator()); + } + } +} diff --git a/src/Mocoding.Ofx.Client.Discover/Mocoding.Ofx.Client.Discover.csproj b/src/Mocoding.Ofx.Client.Discover/Mocoding.Ofx.Client.Discover.csproj new file mode 100644 index 0000000..66389ab --- /dev/null +++ b/src/Mocoding.Ofx.Client.Discover/Mocoding.Ofx.Client.Discover.csproj @@ -0,0 +1,23 @@ + + + + OFX HTTP Client Customization to reliable work with Discover Credit Card (Fid 7101) + netstandard1.3 + netstandadrd;ofx;qfx;money;expense manager;finance;parser;serializer;sgml;discover;treasure management + Refactoring and Redesign. + https://mocoding.blob.core.windows.net/resources/ofx/nugetIcon.png + https://github.com/mocoding-software/ofx + https://raw.githubusercontent.com/mocoding-software/ofx/master/LICENSE + git + https://github.com/mocoding-software/ofx + ..\..\_stylecop\StyleCop.ruleset + MOCODING LLC,Dennis Miasoutov + Full + true + + + + + + + diff --git a/src/Mocoding.Ofx.Client/Args/BankStatementArgs.cs b/src/Mocoding.Ofx.Client/Args/BankStatementArgs.cs new file mode 100644 index 0000000..30cadba --- /dev/null +++ b/src/Mocoding.Ofx.Client/Args/BankStatementArgs.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Mocoding.Ofx.Models; + +namespace Mocoding.Ofx.Client.Args +{ + /// + /// Arguments for fetching bank account statement. + /// + public class BankStatementArgs + { + /// + /// Initializes a new instance of the class. + /// Sets default time range to three months. + /// + public BankStatementArgs() + { + StartDate = DateTime.Now.Date.AddMonths(-3); + EndDate = DateTime.Now.Date; + } + + /// + /// Initializes a new instance of the class from . + /// + /// The account. + public BankStatementArgs(Account account) : this() + { + AccountNumber = account.Id; + RoutingNumber = account.BankId; + Type = account.Type; + + } + + /// + /// Gets or sets the account number. + /// + /// + /// The account number. + /// + public string AccountNumber { get; set; } + + /// + /// Gets or sets the routing number. + /// + /// + /// The routing number. + /// + public string RoutingNumber { get; set; } + + /// + /// Gets or sets the type. + /// + /// + /// The type. + /// + public AccountTypeEnum Type { get; set; } + + /// + /// Gets or sets the start date. + /// + /// + /// The start date. + /// + public DateTime StartDate { get;set; } + + /// + /// Gets or sets the end date. + /// + /// + /// The end date. + /// + public DateTime EndDate { get; set; } + } +} diff --git a/src/Mocoding.Ofx.Client/Args/CreditCardStatementArgs.cs b/src/Mocoding.Ofx.Client/Args/CreditCardStatementArgs.cs new file mode 100644 index 0000000..a82e46d --- /dev/null +++ b/src/Mocoding.Ofx.Client/Args/CreditCardStatementArgs.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Mocoding.Ofx.Models; + +namespace Mocoding.Ofx.Client.Args +{ + /// + /// Arguments for fetching credit card account statement. + /// + public class CreditCardStatementArgs + { + /// + /// Initializes a new instance of the class. + /// + public CreditCardStatementArgs() + { + StartDate = DateTime.Now.Date.AddMonths(-3); + EndDate = DateTime.Now.Date; + } + + /// + /// Initializes a new instance of the class from . + /// + /// The account. + public CreditCardStatementArgs(Account account) : this() + { + AccountNumber = account.Id; + } + + /// + /// Gets or sets the account number. + /// + /// + /// The account number. + /// + public string AccountNumber { get; set; } + + /// + /// Gets or sets the start date. + /// + /// + /// The start date. + /// + public DateTime StartDate { get;set; } + + /// + /// Gets or sets the end date. + /// + /// + /// The end date. + /// + public DateTime EndDate { get; set; } + } +} diff --git a/src/Mocoding.Ofx.Client/Components/TcpClientTransport.cs b/src/Mocoding.Ofx.Client/Components/TcpClientTransport.cs deleted file mode 100644 index f5ed091..0000000 --- a/src/Mocoding.Ofx.Client/Components/TcpClientTransport.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Security; -using System.Net.Sockets; -using System.Text; -using System.Threading.Tasks; -using Mocoding.Ofx.Client.Interfaces; - -namespace Mocoding.Ofx.Client.Components -{ - /// - /// The reason to have this class is that HttpClient for Linux is broken for our scenario - /// It is based on curl and curl send Expect header which is not handled by Chase endpoint. - /// As the result server responds with 417 Expectation Failed. - /// - /// This class do http POST request using SSL to specific endpoint with the specific content. Easy. - /// - /// - public class TcpClientTransport : IOfxClientTransport - { - public async Task PostRequest(Uri url, string content) - { - var server = url.Host; - var port = url.Port == 80 || url.Port == 443 ? string.Empty : ":" + url.Port; - - var builder = new StringBuilder(); - - builder.AppendLine($"POST {url} HTTP/1.1"); - builder.AppendLine("Content-Type: application/x-ofx"); - builder.AppendLine($"Host: {server}{port}"); - builder.AppendLine($"Content-Length: {content.Length}"); - builder.AppendLine("Connection: Keep-Alive"); - builder.AppendLine(); - builder.Append(content); - - var httpRequest = builder.ToString(); - StringBuilder httpResponse = new StringBuilder(); - using (var client = new TcpClient()) - { - await client.ConnectAsync(server, url.Port); - if (url.Scheme == "https") - { - using (var sslStream = new SslStream(client.GetStream())) - { - await sslStream.AuthenticateAsClientAsync(server); - var toSend = Encoding.ASCII.GetBytes(httpRequest); - await sslStream.WriteAsync(toSend, 0, toSend.Length); - await sslStream.FlushAsync(); - await ReadResponse(client, sslStream, httpResponse); - } - } - else - { - using (var stream = client.GetStream()) - { - var toSend = Encoding.ASCII.GetBytes(httpRequest); - stream.Write(toSend, 0, toSend.Length); - await ReadResponse(client, stream, httpResponse); - } - } - } - var httpContent = httpResponse.ToString(); - var contentIndex = httpContent.IndexOf("\r\n\r\n", StringComparison.Ordinal) + 4; - var endIndex = httpContent.LastIndexOf(">", StringComparison.Ordinal); - return httpContent.Substring(contentIndex, endIndex - contentIndex + 1); - } - - private static async Task ReadResponse(TcpClient client, Stream sslStream, StringBuilder httpResponse) - { - var chunk = string.Empty; - if (sslStream.CanRead) - do - { - var received = new byte[client.ReceiveBufferSize]; - var count = await sslStream.ReadAsync(received, 0, client.ReceiveBufferSize); - chunk = Encoding.ASCII.GetString(received.Take(count).ToArray()); - httpResponse.Append(chunk); - } while (!chunk.Contains("")); - } - } -} \ No newline at end of file diff --git a/src/Mocoding.Ofx.Client/Components/Utils.cs b/src/Mocoding.Ofx.Client/Components/Utils.cs deleted file mode 100644 index ff449da..0000000 --- a/src/Mocoding.Ofx.Client/Components/Utils.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using Mocoding.Ofx.Client.Interfaces; - -namespace Mocoding.Ofx.Client.Components -{ - class Utils : IUtils - { - public const string DateTimeFormat = "yyyyMMddHHmmss"; - - public string GetCurrentDateTime() - { - return DateTime.Now.ToString(DateTimeFormat); - } - - public string GenerateTransactionId() - { - return Guid.NewGuid().ToString(); - } - - public string DateToString(DateTime dateTime) - { - return dateTime.ToString(DateTimeFormat); - } - - public string GetClientUid(string userId) - { - var bytes = Encoding.ASCII.GetBytes(userId + "chasebanksucks!").Take(16).ToArray(); - return new Guid(bytes).ToString("N"); - } - } -} diff --git a/src/Mocoding.Ofx.Client/Components/WebClientTransport.cs b/src/Mocoding.Ofx.Client/Components/WebClientTransport.cs deleted file mode 100644 index acbccba..0000000 --- a/src/Mocoding.Ofx.Client/Components/WebClientTransport.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading.Tasks; -using Mocoding.Ofx.Client.Exceptions; -using Mocoding.Ofx.Client.Interfaces; - -namespace Mocoding.Ofx.Client.Components -{ - /// - /// Implementation of OfxClient using WebClient class. - /// - public class WebClientTransport : IOfxClientTransport - { - public async Task PostRequest(Uri url, string content) - { - string result = null; - - using (var client = new HttpClient() { }) - { - client.DefaultRequestHeaders.ExpectContinue = false; - var httpContent = new StringContent(content, Encoding.UTF8); - httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-ofx"); - var response = await client.PostAsync(url, httpContent); - if (response.IsSuccessStatusCode) - { - result = await response.Content.ReadAsStringAsync(); - } - else - throw new OfxTransportException("Failed to send request to " + url); - } - - return result; - } - } -} diff --git a/src/Mocoding.Ofx.Client/Defaults/DefaultOfxClientFactory.cs b/src/Mocoding.Ofx.Client/Defaults/DefaultOfxClientFactory.cs new file mode 100644 index 0000000..da506c4 --- /dev/null +++ b/src/Mocoding.Ofx.Client/Defaults/DefaultOfxClientFactory.cs @@ -0,0 +1,49 @@ +using Mocoding.Ofx.Client.Interfaces; +using Mocoding.Ofx.Interfaces; + +namespace Mocoding.Ofx.Client.Defaults +{ + /// + /// Default implementation of OFX Client Factory. + /// + /// + public class DefaultOfxClientFactory : IOfxClientFactory + { + private readonly IOfxSerializerFactory _serializerFactory; + private readonly IProtocolUtilsFactory _utilsFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The serializer factory. + /// The utils factory. + public DefaultOfxClientFactory(IOfxSerializerFactory serializerFactory, IProtocolUtilsFactory utilsFactory) + { + _serializerFactory = serializerFactory; + _utilsFactory = utilsFactory; + } + + /// + /// Creates based on options provided. + /// + /// The OFX options. + /// + /// Concrete OFX client implementation. + /// + public IOfxClient Create(OfxClientOptions options) + { + var serializer = _serializerFactory.Create(options.Version); + var utils = _utilsFactory.Create(options.BankFid); + + return new OfxClient(options, utils, serializer); + } + + /// + /// Default Factory Instance + /// + /// + /// The instance. + /// + public DefaultOfxClientFactory Instance => new DefaultOfxClientFactory(new DefaultOfxSerializerFactory(), new DefaultProtocolUtilsFactory()); + } +} diff --git a/src/Mocoding.Ofx.Client/Defaults/DefaultOfxRequestLocator.cs b/src/Mocoding.Ofx.Client/Defaults/DefaultOfxRequestLocator.cs new file mode 100644 index 0000000..4ba3e4f --- /dev/null +++ b/src/Mocoding.Ofx.Client/Defaults/DefaultOfxRequestLocator.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Mocoding.Ofx.Client.Interfaces; +using Mocoding.Ofx.Client.Requests; + +namespace Mocoding.Ofx.Client.Defaults +{ + /// + /// Default implementation of Service Locator for OFX. + /// Uses Dictionary to store all the request type mappings. + /// + /// + public class DefaultOfxRequestLocator : IOfxRequestLocator + { + private Dictionary> _requestBuildersDict; + + /// + /// Gets the request builder of the specific type. + /// + /// The type of the request builder. + /// + /// Request builder implementation. + /// + public TRequestBuilder GetRequestBuilder() where TRequestBuilder : IRequestBuilder + { + return (TRequestBuilder)RequestBuilders[typeof(TRequestBuilder)](); + } + + /// + /// Gets the request builders. + /// + /// + /// The request builders. + /// + protected Dictionary> RequestBuilders => _requestBuildersDict ?? (_requestBuildersDict = InitRequestBuilders()); + + /// + /// Initializes the request builders. + /// + /// + protected virtual Dictionary> InitRequestBuilders() + { + return new Dictionary>() + { + {typeof(AuthenticateRequestBuilder), () => new AuthenticateRequestBuilder()}, + {typeof(AccountsRequestBuilder), () => new AccountsRequestBuilder()}, + {typeof(CreditCardStatementRequestBuilder), () => new CreditCardStatementRequestBuilder()}, + {typeof(BankStatementRequestBuilder), () => new BankStatementRequestBuilder()}, + }; + } + } +} diff --git a/src/Mocoding.Ofx.Client/Defaults/DefaultProtocolUtilsFactory.cs b/src/Mocoding.Ofx.Client/Defaults/DefaultProtocolUtilsFactory.cs new file mode 100644 index 0000000..9785140 --- /dev/null +++ b/src/Mocoding.Ofx.Client/Defaults/DefaultProtocolUtilsFactory.cs @@ -0,0 +1,23 @@ +using Mocoding.Ofx.Client.Interfaces; + +namespace Mocoding.Ofx.Client.Defaults +{ + /// + /// Default implementation for protocol utils factory. + /// + /// + public class DefaultProtocolUtilsFactory : IProtocolUtilsFactory + { + /// + /// Creates based on specified Financial Institution ID. + /// + /// Financial Institution ID. + /// + /// Concrete protocol methods implementation. + /// + public IProtocolUtils Create(string fid) + { + return new OfxProtocolUtils(new DefaultOfxRequestLocator()); + } + } +} diff --git a/src/Mocoding.Ofx.Client/Exceptions/OfxServerException.cs b/src/Mocoding.Ofx.Client/Exceptions/OfxServerException.cs new file mode 100644 index 0000000..c7de23b --- /dev/null +++ b/src/Mocoding.Ofx.Client/Exceptions/OfxServerException.cs @@ -0,0 +1,15 @@ +using System; + +namespace Mocoding.Ofx.Client.Exceptions +{ + class OfxServerException : Exception + { + public OfxServerException(string code, string message) + : base(message) + { + Code = code; + } + + public string Code {get; private set;} + } +} diff --git a/src/Mocoding.Ofx.Client/Interfaces/IOfxClient.cs b/src/Mocoding.Ofx.Client/Interfaces/IOfxClient.cs new file mode 100644 index 0000000..b4d6586 --- /dev/null +++ b/src/Mocoding.Ofx.Client/Interfaces/IOfxClient.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using Mocoding.Ofx.Client.Args; +using Mocoding.Ofx.Models; + +namespace Mocoding.Ofx.Client.Interfaces +{ + /// + /// Contains method to exchange information with Financial Institution + /// using OFX protocol over the network. + /// + public interface IOfxClient + { + /// + /// Gets accounts ofx raw payload. + /// + /// Raw OFX Payload + Task GetAccountsOfx(); + + /// + /// Gets list of accounts. + /// + /// List of bank accounts + Task GetAccounts(); + + /// + /// Gets credit card statement ofx payload. + /// + /// Date range and account filter. + /// Raw OFX Payload + Task GetStatementOfx(CreditCardStatementArgs args); + + /// + /// Gets credit card statement. + /// + /// Date range and account filter. + /// Strongly typed deserialized statement model. + Task GetStatement(CreditCardStatementArgs args); + + /// + /// Gets bank statement ofx payload. + /// + /// Date range and account filter. + /// Raw OFX Payload + Task GetStatementOfx(BankStatementArgs args); + + /// + /// Gets bank statement ofx payload. + /// + /// Date range and account filter. + /// Strongly typed deserialized statement model. + Task GetStatement(BankStatementArgs args); + } +} diff --git a/src/Mocoding.Ofx.Client/Interfaces/IOfxClientFactory.cs b/src/Mocoding.Ofx.Client/Interfaces/IOfxClientFactory.cs new file mode 100644 index 0000000..dd418ff --- /dev/null +++ b/src/Mocoding.Ofx.Client/Interfaces/IOfxClientFactory.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Mocoding.Ofx.Client.Interfaces +{ + /// + /// Abstract factory for . + /// + public interface IOfxClientFactory + { + /// + /// Creates based on options provided. + /// + /// The OFX options. + /// Concrete OFX client implementation. + IOfxClient Create(OfxClientOptions options); + } +} diff --git a/src/Mocoding.Ofx.Client/Interfaces/IOfxRequestLocator.cs b/src/Mocoding.Ofx.Client/Interfaces/IOfxRequestLocator.cs new file mode 100644 index 0000000..2f45a04 --- /dev/null +++ b/src/Mocoding.Ofx.Client/Interfaces/IOfxRequestLocator.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Mocoding.Ofx.Client.Interfaces +{ + /// + /// Service locator pattern for accessing specific OFX request builder. + /// + /// + /// This pattern is used to customize request creation per financial institution. + /// + public interface IOfxRequestLocator + { + /// + /// Gets the request builder of the specific type. + /// + /// The type of the request builder. + /// Request builder implementation. + TRequestBuilder GetRequestBuilder() where TRequestBuilder : IRequestBuilder; + } +} diff --git a/src/Mocoding.Ofx.Client/Interfaces/IProtocolUtils.cs b/src/Mocoding.Ofx.Client/Interfaces/IProtocolUtils.cs new file mode 100644 index 0000000..0016ea7 --- /dev/null +++ b/src/Mocoding.Ofx.Client/Interfaces/IProtocolUtils.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading.Tasks; +using Mocoding.Ofx.Client.Requests; + +namespace Mocoding.Ofx.Client.Interfaces +{ + /// + /// Contains methods that are used in OFX protocol during request creation and execution. + /// + public interface IProtocolUtils + { + /// + /// Gets the current date time in specific OFX format + /// + /// DateTime formatted string. + string GetCurrentDateTime(); + + /// + /// Generates the OFX transaction identifier. + /// + /// Transaction ID + string GenerateTransactionId(); + + /// + /// Gets the client uid. + /// + /// The user identifier. + /// Client ID + string GetClientUid(string userId); + + /// + /// Gets the specific OFX date format + /// + /// + /// The date format. + /// + string DateFormat { get; } + + /// + /// Creates and executes POST request to specified url with specified body content. + /// + /// The URL. + /// The content. + /// + Task PostRequest(Uri url, string content); + + /// + /// Gets the service locator for request builders + /// + /// + /// The requests. + /// + IOfxRequestLocator Requests { get; } + } +} \ No newline at end of file diff --git a/src/Mocoding.Ofx.Client/Interfaces/IProtocolUtilsFactory.cs b/src/Mocoding.Ofx.Client/Interfaces/IProtocolUtilsFactory.cs new file mode 100644 index 0000000..b5f4bb0 --- /dev/null +++ b/src/Mocoding.Ofx.Client/Interfaces/IProtocolUtilsFactory.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Mocoding.Ofx.Client.Interfaces +{ + /// + /// Abstract factory for . + /// + public interface IProtocolUtilsFactory + { + /// + /// Creates based on specified Financial Institution ID. + /// + /// Financial Institution ID. + /// Concrete protocol methods implementation. + IProtocolUtils Create(string fid); + } +} diff --git a/src/Mocoding.Ofx.Client/Interfaces/IRequestBuilder.cs b/src/Mocoding.Ofx.Client/Interfaces/IRequestBuilder.cs new file mode 100644 index 0000000..6e9fced --- /dev/null +++ b/src/Mocoding.Ofx.Client/Interfaces/IRequestBuilder.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Mocoding.Ofx.Protocol; + +namespace Mocoding.Ofx.Client.Interfaces +{ + /// + /// Abstraction over builder that is responsible for build specific message set of OFX. + /// + /// Par of Builder Design Pattern for OFX request. + public interface IRequestBuilder + { + /// + /// Builds this instance. + /// + /// Message Set to be added to OFX request. + AbstractTopLevelMessageSet Build(); + } +} diff --git a/src/Mocoding.Ofx.Client/Interfaces/Interfaces.cs b/src/Mocoding.Ofx.Client/Interfaces/Interfaces.cs deleted file mode 100644 index 20af0a1..0000000 --- a/src/Mocoding.Ofx.Client/Interfaces/Interfaces.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Threading.Tasks; -using Mocoding.Ofx.Client.Models; - -namespace Mocoding.Ofx.Client.Interfaces -{ - public interface IOfxClient - { - Task GetTransactions(Account account, TransactionsFilter filter = null); - Task GetAccounts(); - } - - /// - /// Transport layer for Ofx Client. - /// - public interface IOfxClientTransport - { - /// - /// Creates and executes POST request to specified url with specified body content. - /// - /// The URL. - /// The content. - /// - Task PostRequest(Uri url, string content); - } - - - interface IUtils - { - string GetCurrentDateTime(); - - string GenerateTransactionId(); - - string DateToString(DateTime dateTime); - - string GetClientUid(string userId); - } -} diff --git a/src/Mocoding.Ofx.Client/Mocoding.Ofx.Client.csproj b/src/Mocoding.Ofx.Client/Mocoding.Ofx.Client.csproj index c586b7c..a43a548 100644 --- a/src/Mocoding.Ofx.Client/Mocoding.Ofx.Client.csproj +++ b/src/Mocoding.Ofx.Client/Mocoding.Ofx.Client.csproj @@ -3,27 +3,29 @@ OFX HTTP Client for .NET Standard. Supports Account and Transactions import from financial institution. netstandard1.3 - netstandadrd;ofx;qfx;money;expense manager;finance;parser;serializer;sgml - Updated Dependencies. + netstandadrd;ofx;qfx;money;expense manager;finance;parser;serializer;sgml;treasure management + Refactoring and Redesign. https://mocoding.blob.core.windows.net/resources/ofx/nugetIcon.png https://github.com/mocoding-software/ofx https://raw.githubusercontent.com/mocoding-software/ofx/master/LICENSE git https://github.com/mocoding-software/ofx ..\..\_stylecop\StyleCop.ruleset - MOCODING - + MOCODING LLC,Dennis Miasoutov + Full + true + - + - + diff --git a/src/Mocoding.Ofx.Client/Models/Accounts.cs b/src/Mocoding.Ofx.Client/Models/Accounts.cs deleted file mode 100644 index 4143be9..0000000 --- a/src/Mocoding.Ofx.Client/Models/Accounts.cs +++ /dev/null @@ -1,105 +0,0 @@ -namespace Mocoding.Ofx.Client.Models -{ - /// - /// Contains data about single user bank account. - /// - public class Account - { - /// - /// Initializes a new instance of the class. - /// - /// The account type. - /// The account identifier. - /// The bank identifier. - public Account(AccountTypeEnum type, string id, string bankId = null) - { - BankId = bankId; - Id = id; - Type = type; - } - - /// - /// Internal ctor - to construct object with all properties. - /// Initializes a new instance of the class. - /// - /// The account type. - /// The account identifier. - /// The bank identifier. - /// The description. - /// The phone. - /// Type of the sub. - /// The status. - internal Account(AccountTypeEnum type, string id, string bankId, string description, string phone, string subType, string status) - { - Status = status; - SubType = subType; - Description = description; - BankId = bankId; - Id = id; - Type = type; - Phone = phone; - } - - /// - /// Gets the account description. - /// - /// - /// The description. - /// - public string Description { get; private set; } - - /// - /// Gets the account description. - /// - /// - /// The description. - /// - public string Phone { get; private set; } - - /// - /// Gets the sub type of the account. - /// - /// - /// The type of the sub. - /// - public string SubType { get; private set; } - - /// - /// Gets the account type. - /// - /// - /// The type. - /// - public AccountTypeEnum Type { get; private set; } - - /// - /// Gets the account identifier. - /// - /// - /// The identifier. - /// - public string Id { get; private set; } - - /// - /// Gets the bank identifier. - /// - /// - /// The bank identifier. - /// - public string BankId { get; private set; } - - /// - /// Gets the account status. - /// - /// - /// The status. - /// - public string Status { get; private set; } - } - - public enum AccountTypeEnum - { - Checking = 1, - Credit = 2 - } -} diff --git a/src/Mocoding.Ofx.Client/Models/Transactions.cs b/src/Mocoding.Ofx.Client/Models/Transactions.cs deleted file mode 100644 index 25af159..0000000 --- a/src/Mocoding.Ofx.Client/Models/Transactions.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Mocoding.Ofx.Client.Models -{ - /// - /// Contains data about transactions of single bank account. - /// - public class AccountTransactions - { - public AccountTransactions(decimal currentBalance, IEnumerable collection) - { - CurrentBalance = currentBalance; - Items = collection.ToArray(); - } - public decimal CurrentBalance { get; private set; } - public Transaction[] Items { get; private set;} -} - - public class Transaction - { - public Transaction(string id, string type, decimal ammount, DateTime datePosted, string description, string memo) - { - Memo = memo; - Description = description; - DatePosted = datePosted; - Ammount = ammount; - Type = type; - Id = id; - } - - public string Id { get; private set; } - public string Type { get; private set; } - public decimal Ammount { get; private set; } - public DateTime DatePosted { get; private set; } - public string Description { get; private set; } - public string Memo { get; private set; } - } -} diff --git a/src/Mocoding.Ofx.Client/OfxClient.GetAccounts.cs b/src/Mocoding.Ofx.Client/OfxClient.GetAccounts.cs deleted file mode 100644 index d41c41f..0000000 --- a/src/Mocoding.Ofx.Client/OfxClient.GetAccounts.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Mocoding.Ofx.Client.Exceptions; -using Mocoding.Ofx.Client.Models; -using Mocoding.Ofx.Protocol; - -namespace Mocoding.Ofx.Client -{ - partial class OfxClient - { - public async Task GetAccounts() - { - var accountsRequest = new SignupRequestMessageSetV1() - { - Items = new AbstractRequest[] - { - new AccountInfoTransactionRequest() - { - TRNUID = _utils.GenerateTransactionId(), - CLTCOOKIE = "4", - ACCTINFORQ = new AccountInfoRequest(){DTACCTUP = "19900101000000"} - } - } - }; - - var messageSet = - await ExecuteRequest(accountsRequest); - var accountsResponse = - messageSet.Items.FirstOrDefault(_ => _ is AccountInfoTransactionResponse) as AccountInfoTransactionResponse; - - if (accountsResponse == null) - throw new OfxResponseException("Required response is not present in message set."); - - var result = new List(); - foreach (var accountInfo in accountsResponse.ACCTINFORS.ACCTINFO) - { - AccountTypeEnum type; - string subtype = null; - string status = null; - string accountId = null; - string bankId = null; - - var bankAccount = accountInfo.Items.FirstOrDefault(_ => _ is BankAccountInfo) as BankAccountInfo; - var ccAccount = - accountInfo.Items.FirstOrDefault(_ => _ is CreditCardAccountInfo) as CreditCardAccountInfo; - - if (bankAccount != null) - { - type = AccountTypeEnum.Checking; - accountId = bankAccount.BANKACCTFROM.ACCTID; - bankId = bankAccount.BANKACCTFROM.BANKID; - subtype = bankAccount.BANKACCTFROM.ACCTTYPE.ToString(); - status = bankAccount.SVCSTATUS.ToString(); - } - else if (ccAccount != null) - { - type = AccountTypeEnum.Credit; - accountId = ccAccount.CCACCTFROM.ACCTID; - status = ccAccount.SVCSTATUS.ToString(); - } - else - continue; - - var account = new Account(type, accountId, bankId, accountInfo.DESC, accountInfo.PHONE, subtype, - status); - result.Add(account); - } - return result.ToArray(); - } - } -} diff --git a/src/Mocoding.Ofx.Client/OfxClient.GetTransactions.cs b/src/Mocoding.Ofx.Client/OfxClient.GetTransactions.cs deleted file mode 100644 index 1510c52..0000000 --- a/src/Mocoding.Ofx.Client/OfxClient.GetTransactions.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using Mocoding.Ofx.Client.Models; -using Mocoding.Ofx; -using Mocoding.Ofx.Client.Components; -using Mocoding.Ofx.Client.Exceptions; -using Mocoding.Ofx.Protocol; - -namespace Mocoding.Ofx.Client -{ - partial class OfxClient - { - public async Task GetTransactions(Account account, TransactionsFilter filter = null) - { - if (filter == null) - filter = new TransactionsFilter(DateTime.Now.Date.AddMonths(-3), DateTime.Now.Date); - else - if (filter.StartDate > filter.EndDate) - throw new Exception("Invalid Filter!!! Start date can't be greater to end date."); - - switch (account.Type) - { - case AccountTypeEnum.Credit: - return await GetCreditCardTransactions(account, filter); - default: - return await GetBankAccountTransactions(account, filter); - } - } - - async Task GetCreditCardTransactions(Account account, TransactionsFilter filter) - { - var transactionsRequest = new CreditcardRequestMessageSetV1() - { - Items = new AbstractTransactionRequest[] - { - new CreditCardStatementTransactionRequest() - { - TRNUID = _utils.GenerateTransactionId(), - CCSTMTRQ = new CreditCardStatementRequest() - { - CCACCTFROM = new CreditCardAccount() - { - ACCTID = account.Id - }, - INCTRAN = new IncTransaction() - { - DTEND = _utils.DateToString(filter.EndDate), - DTSTART = _utils.DateToString(filter.StartDate), - INCLUDE = BooleanType.Y - } - } - } - } - }; - - var messageSet = - await ExecuteRequest(transactionsRequest); - var transactionsResponse = - messageSet.Items.FirstOrDefault(_ => _ is CreditCardStatementTransactionResponse) as - CreditCardStatementTransactionResponse; - - if (transactionsResponse == null) - throw new OfxResponseException("Required response is not present in message set."); - - var transList = transactionsResponse.CCSTMTRS.BANKTRANLIST.STMTTRN != null - ? transactionsResponse.CCSTMTRS.BANKTRANLIST.STMTTRN.Select(MapToModel).ToList() - : new List(); - - decimal amount; - if (!decimal.TryParse(transactionsResponse.CCSTMTRS.LEDGERBAL.BALAMT, out amount)) - amount = 0; - - return new AccountTransactions(amount, transList); - } - - async Task GetBankAccountTransactions(Account account, TransactionsFilter filter) - { - var transactionsRequest = new BankRequestMessageSetV1() - { - Items = new AbstractRequest[] - { - new StatementTransactionRequest() - { - TRNUID = _utils.GenerateTransactionId(), - STMTRQ = new StatementRequest() - { - BANKACCTFROM = new BankAccount() - { - ACCTID = account.Id, - BANKID = account.BankId, - ACCTTYPE = (AccountEnum)Enum.Parse(typeof(AccountEnum), account.Type.ToString(), true) - }, - INCTRAN = new IncTransaction() - { - DTEND = _utils.DateToString(filter.EndDate), - DTSTART = _utils.DateToString(filter.StartDate), - INCLUDE = BooleanType.Y - } - } - } - } - }; - - var messageSet = - await ExecuteRequest(transactionsRequest); - var transactionsResponse = - messageSet.Items.FirstOrDefault(_ => _ is StatementTransactionResponse) as - StatementTransactionResponse; - - if (transactionsResponse == null) - throw new OfxResponseException("Required response is not present in message set."); - - var transList = transactionsResponse.STMTRS.BANKTRANLIST.STMTTRN != null - ? transactionsResponse.STMTRS.BANKTRANLIST.STMTTRN.Select(MapToModel).ToList() - : new List(); - - decimal amount; - if (!decimal.TryParse(transactionsResponse.STMTRS.AVAILBAL.BALAMT, out amount)) - amount = 0; - - return new AccountTransactions(amount, transList); - } - - static Transaction MapToModel(StatementTransaction transactionDto) - { - decimal amount; - if (!decimal.TryParse(transactionDto.TRNAMT, out amount)) - throw new OfxResponseException("Amount of transaction can not be parsed. " + transactionDto.TRNAMT); - - DateTime datePosted; - const int targetLength = 14; - var truncatedValue = transactionDto.DTPOSTED.Length == targetLength - ? transactionDto.DTPOSTED - : transactionDto.DTPOSTED.Length > targetLength - ? transactionDto.DTPOSTED.Substring(0, targetLength) - : transactionDto.DTPOSTED + new string('0', targetLength - transactionDto.DTPOSTED.Length); - if (!DateTime.TryParseExact(truncatedValue, Utils.DateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out datePosted)) - throw new OfxResponseException("Date of transaction can not be parsed. " + transactionDto.DTPOSTED); - - var description = transactionDto.Item is Payee - ? (transactionDto.Item as Payee).NAME - : transactionDto.Item as string; - - return new Transaction( - transactionDto.FITID, - transactionDto.TRNTYPE.ToString(), - amount, - datePosted, - description, - transactionDto.MEMO); - } - } - - public class TransactionsFilter - { - public TransactionsFilter(DateTime startDate, DateTime endDate) - { - StartDate = startDate; - EndDate = endDate; - } - - public DateTime StartDate { get; private set; } - public DateTime EndDate { get; private set; } - } -} diff --git a/src/Mocoding.Ofx.Client/OfxClient.cs b/src/Mocoding.Ofx.Client/OfxClient.cs index 5d4d894..b5f92d8 100644 --- a/src/Mocoding.Ofx.Client/OfxClient.cs +++ b/src/Mocoding.Ofx.Client/OfxClient.cs @@ -3,107 +3,173 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; -using Mocoding.Ofx.Client.Components; +using Mocoding.Ofx.Client.Args; using Mocoding.Ofx.Client.Exceptions; using Mocoding.Ofx.Client.Interfaces; +using Mocoding.Ofx.Client.Requests; +using Mocoding.Ofx.Interfaces; +using Mocoding.Ofx.Models; using Mocoding.Ofx.Protocol; -[assembly:InternalsVisibleTo("Mocoding.Ofx.Client.Tests")] +[assembly: InternalsVisibleTo("Mocoding.Ofx.Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] namespace Mocoding.Ofx.Client { - public partial class OfxClient : IOfxClient + /// + /// OFX Client Implementation + /// + /// + public class OfxClient : IOfxClient { - readonly IOfxClientTransport _transport; - readonly OfxSerializer _serializer; - readonly IUtils _utils; - - public OfxClient(OfxClientOptions options) - : this(options, new TcpClientTransport(), new OfxSerializer(), new Utils()) + readonly IOfxSerializer _serializer; + readonly IProtocolUtils _utils; + private readonly OfxClientOptions _opts; + + /// + /// Initializes a new instance of the class. + /// + /// The opts. + /// The utils. + /// The serializer. + public OfxClient(OfxClientOptions opts, IProtocolUtils utils, IOfxSerializer serializer) { - Options = options; + _serializer = serializer; + _utils = utils; + _opts = opts; } - public OfxClient(OfxClientOptions options, IOfxClientTransport transport) - : this(options, transport, new OfxSerializer(), new Utils()) + /// + /// Gets accounts ofx raw payload. + /// + /// + /// Raw OFX Payload + /// + public async Task GetAccountsOfx() { + var request = PrepareAccountsOfxRequest(); + var response = await _utils.PostRequest(_opts.ApiUrl, request); + + return response; } - internal OfxClient(OfxClientOptions options, IOfxClientTransport transport, IUtils utils) - : this(options, transport, new OfxSerializer(), utils) + /// + /// Gets list of accounts. + /// + /// + /// List of bank accounts + /// + public async Task GetAccounts() { + var response = await GetAccountsOfx(); + var ofxPayload = _serializer.Deserialize(response); + // check for code + + return OfxAccountsParser.Parse(ofxPayload).ToArray(); } - private OfxClient(OfxClientOptions options, IOfxClientTransport transport, OfxSerializer serializer, IUtils utils) + /// + /// Gets credit card statement ofx payload. + /// + /// Date range and account filter. + /// + /// Raw OFX Payload + /// + public async Task GetStatementOfx(CreditCardStatementArgs args) { - Options = options; - _transport = transport; - _serializer = serializer; - _utils = utils; - } + var request = PrepareCreditCardStatementOfxRequest(args); + var response = await _utils.PostRequest(_opts.ApiUrl, request); - public OfxClientOptions Options { get;} + return response; + } - List CreatedRequest() + /// + /// Gets credit card statement. + /// + /// Date range and account filter. + /// + /// Strongly typed deserialized statement model. + /// + public async Task GetStatement(CreditCardStatementArgs args) { - return new List() - { - new SignonRequestMessageSetV1() - { - SONRQ = new SignonRequest() - { - CLIENTUID = _utils.GetClientUid(Options.UserId), - DTCLIENT = _utils.GetCurrentDateTime(), - LANGUAGE = LanguageEnum.ENG, - FI = - new FinancialInstitution() - { - FID = Options.BankFid, - ORG = Options.BankOrg - }, - APPID = "QWIN", - APPVER = "2500", - Items = new[] {Options.UserId, Options.Password}, - ItemsElementName = new[] {ItemsChoiceType.USERID, ItemsChoiceType.USERPASS} - } - } - }; + var response = await GetStatementOfx(args); + var ofxPayload = _serializer.Deserialize(response); + // check for code + return OfxStatementParser.Parse(ofxPayload); } - async Task ExecuteRequest(TRequestMessage accountListRequest) - where TRequestMessage : AbstractRequestMessageSet - where TResponseMessage : AbstractResponseMessageSet + /// + /// Gets bank statement ofx payload. + /// + /// Date range and account filter. + /// + /// Raw OFX Payload + /// + public async Task GetStatementOfx(BankStatementArgs args) { + var request = PrepareBankStatementOfxRequest(args); + var response = await _utils.PostRequest(_opts.ApiUrl, request); - var request = CreatedRequest(); - request.Add(accountListRequest); - - var ofxRequest = new OFX() { Items = request.ToArray() }; - var requestBody = _serializer.Serialize(ofxRequest); - if(requestBody == null) - throw new OfxSerializationException("Request serialization error."); + return response; + } - var responseBody = await _transport.PostRequest(Options.ApiUrl, requestBody); - var ofxResponse = _serializer.Deserialize(responseBody); - if (ofxResponse == null) - throw new OfxSerializationException("Response deserialization error."); + /// + /// Gets bank statement ofx payload. + /// + /// Date range and account filter. + /// + /// Strongly typed deserialized statement model. + /// + public async Task GetStatement(BankStatementArgs args) + { + var response = await GetStatementOfx(args); + var ofxPayload = _serializer.Deserialize(response); + // check for code + return OfxStatementParser.Parse(ofxPayload); + } - var signInResponse = ofxResponse.Items.FirstOrDefault(_ => _ is SignonResponseMessageSetV1) as SignonResponseMessageSetV1; - if (signInResponse == null) - throw new OfxResponseException("SIGNONRESPONSEMESSAGESETV1 is not present in response."); + internal string PrepareAccountsOfxRequest() + { + return PrepareOfxRequest(AuthRequest(), GetAccountsRequest()); + } - if (signInResponse.SONRS.STATUS.CODE != "0") - throw new OfxResponseException(signInResponse.SONRS.STATUS.MESSAGE); + internal string PrepareCreditCardStatementOfxRequest(CreditCardStatementArgs args) + { + return PrepareOfxRequest(AuthRequest(), GetCreditCardStatementRequest(args)); + } + internal string PrepareBankStatementOfxRequest(BankStatementArgs args) + { + return PrepareOfxRequest(AuthRequest(), GetBankStatementRequest(args)); + } - var messageSet = ofxResponse.Items.FirstOrDefault(_ => _ is TResponseMessage) as TResponseMessage; + internal string PrepareOfxRequest(params IRequestBuilder[] builders) + { + var ofxRequest = new OFX() { Items = builders.Select(_ => _.Build()).ToArray() }; + var content = _serializer.Serialize(ofxRequest); - if (messageSet == null) - throw new OfxResponseException("Requested message set " + typeof(TResponseMessage).Name.ToUpper() + " is not present in response."); - - return messageSet; + return content; } + + internal IRequestBuilder AuthRequest() => _utils.Requests.GetRequestBuilder() + .ClientId(_utils.GetClientUid(_opts.UserId)) + .Timestamp(_utils.GetCurrentDateTime()) + .Bank(_opts.BankFid, _opts.BankOrg) + .Credentials(_opts.UserId, _opts.Password) + .AppVersion(_opts.AppName, _opts.AppVersion); + + internal IRequestBuilder GetAccountsRequest() => _utils.Requests.GetRequestBuilder() + .TransactionId(_utils.GenerateTransactionId()); + + internal IRequestBuilder GetCreditCardStatementRequest(CreditCardStatementArgs args) => _utils.Requests.GetRequestBuilder() + .Account(args.AccountNumber) + .Filter(args.StartDate.ToString(_utils.DateFormat), args.EndDate.ToString(_utils.DateFormat)) + .TransactionId(_utils.GenerateTransactionId()); + + internal IRequestBuilder GetBankStatementRequest(BankStatementArgs args) => _utils.Requests.GetRequestBuilder() + .Account(args.AccountNumber, args.RoutingNumber, args.Type.ToString()) + .Filter(args.StartDate.ToString(_utils.DateFormat), args.EndDate.ToString(_utils.DateFormat)) + .TransactionId(_utils.GenerateTransactionId()); } } diff --git a/src/Mocoding.Ofx.Client/OfxClientOptions.cs b/src/Mocoding.Ofx.Client/OfxClientOptions.cs index 78ec767..65d96c3 100644 --- a/src/Mocoding.Ofx.Client/OfxClientOptions.cs +++ b/src/Mocoding.Ofx.Client/OfxClientOptions.cs @@ -2,9 +2,7 @@ namespace Mocoding.Ofx.Client { - /// - /// Options to initialize URL. - /// + /// Options to initialize URL. public class OfxClientOptions { /// @@ -15,13 +13,19 @@ public class OfxClientOptions /// The bank fid. /// The user identifier. /// The password. - public OfxClientOptions(Uri apiUrl, string bankOrg, string bankFid, string userId, string password) + /// OFX Version. + /// Application Name + /// Application Version + public OfxClientOptions(Uri apiUrl, string bankOrg, string bankFid, string userId, string password, OfxVersionEnum version = OfxVersionEnum.Version1x, string appName = "QWIN", string appVersion = "2500") { Password = password; UserId = userId; BankFid = bankFid; BankOrg = bankOrg; ApiUrl = apiUrl; + Version = version; + AppName = appName; + AppVersion = appVersion; } /// @@ -30,7 +34,7 @@ public OfxClientOptions(Uri apiUrl, string bankOrg, string bankFid, string userI /// /// The API endpoint. /// - public Uri ApiUrl { get; private set; } + public Uri ApiUrl { get; } /// /// Gets the bank org. @@ -38,7 +42,7 @@ public OfxClientOptions(Uri apiUrl, string bankOrg, string bankFid, string userI /// /// The bank org in UPPER register. /// - public string BankOrg { get; private set; } + public string BankOrg { get; } /// /// Gets the bank fid. @@ -46,7 +50,7 @@ public OfxClientOptions(Uri apiUrl, string bankOrg, string bankFid, string userI /// /// The bank fid. /// - public string BankFid { get; private set; } + public string BankFid { get; } /// /// Gets the user login. @@ -54,7 +58,7 @@ public OfxClientOptions(Uri apiUrl, string bankOrg, string bankFid, string userI /// /// The user identifier. /// - public string UserId { get; private set; } + public string UserId { get; } /// /// Gets the password. @@ -62,6 +66,22 @@ public OfxClientOptions(Uri apiUrl, string bankOrg, string bankFid, string userI /// /// The password. /// - public string Password { get; private set; } + public string Password { get; } + + /// + /// Gets the password. + /// + /// + /// The password. + /// + public OfxVersionEnum Version { get; } + + /// Gets the name of the application. + /// The name of the application. + public string AppName { get; } + + /// Gets the application version + /// The application ver. + public string AppVersion { get; } } } diff --git a/src/Mocoding.Ofx.Client/OfxProtocolUtils.cs b/src/Mocoding.Ofx.Client/OfxProtocolUtils.cs new file mode 100644 index 0000000..d34d471 --- /dev/null +++ b/src/Mocoding.Ofx.Client/OfxProtocolUtils.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Mocoding.Ofx.Client.Exceptions; +using Mocoding.Ofx.Client.Interfaces; +using Mocoding.Ofx.Client.Requests; + +namespace Mocoding.Ofx.Client +{ + /// + /// Default OFX Protocol implementation + /// + /// + public class OfxProtocolUtils : IProtocolUtils + { + /// + /// The date time format + /// + public const string DateTimeFormat = "yyyyMMddHHmmss"; + + /// + /// Initializes a new instance of the class. + /// + /// The requests. + public OfxProtocolUtils(IOfxRequestLocator _requests) + { + this.Requests = _requests; + } + + /// + /// Gets the current date time in specific OFX format + /// + /// + /// DateTime formatted string. + /// + public virtual string GetCurrentDateTime() + { + return DateTime.Now.ToString(DateTimeFormat); + } + + /// + /// Generates the OFX transaction identifier. + /// + /// + /// Transaction ID + /// + public virtual string GenerateTransactionId() + { + return Guid.NewGuid().ToString(); + } + + /// + /// Gets the client uid. + /// + /// The user identifier. + /// + /// Client ID + /// + public virtual string GetClientUid(string userId) + { + var bytes = Encoding.ASCII.GetBytes(userId + "chasebanksucks!").Take(16).ToArray(); + return new Guid(bytes).ToString("N"); + } + + /// + /// Gets the specific OFX date format + /// + /// + /// The date format. + /// + public string DateFormat => DateTimeFormat; + + /// + /// Creates and executes POST request to specified url with specified body content. + /// + /// The URL. + /// The content. + /// + /// Failed to send request to " + url + public virtual async Task PostRequest(Uri url, string content) + { + string result = null; + + using (var client = new HttpClient(new HttpClientHandler() { AllowAutoRedirect = false }) { }) + { + client.DefaultRequestHeaders.ExpectContinue = false; + var httpContent = new StringContent(content, Encoding.UTF8); + + httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-ofx"); + var response = await client.PostAsync(url, httpContent); + if (response.IsSuccessStatusCode) + { + result = await response.Content.ReadAsStringAsync(); + } + else + throw new OfxTransportException("Failed to send request to " + url); + } + + return result; + } + + /// + /// Gets the service locator for request builders + /// + /// + /// The requests. + /// + public IOfxRequestLocator Requests { get; } + } +} diff --git a/src/Mocoding.Ofx.Client/Requests/AbstractTransactionRequestBuilder.cs b/src/Mocoding.Ofx.Client/Requests/AbstractTransactionRequestBuilder.cs new file mode 100644 index 0000000..703acfb --- /dev/null +++ b/src/Mocoding.Ofx.Client/Requests/AbstractTransactionRequestBuilder.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Mocoding.Ofx.Protocol; + +namespace Mocoding.Ofx.Client.Requests +{ + /// + /// Builds request that requires transaction Id. + /// + /// The type of the request. + /// + public abstract class AbstractTransactionRequestBuilder : RequestBuilder where TRequest : AbstractTransactionRequest, new() + { + /// + /// Sets transaction Id. + /// + /// The identifier. + /// This instance. + public AbstractTransactionRequestBuilder TransactionId(string id) + { + Request.TRNUID = id; + + return this; + } + } +} diff --git a/src/Mocoding.Ofx.Client/Requests/AccountsRequestBuilder.cs b/src/Mocoding.Ofx.Client/Requests/AccountsRequestBuilder.cs new file mode 100644 index 0000000..8a17bc0 --- /dev/null +++ b/src/Mocoding.Ofx.Client/Requests/AccountsRequestBuilder.cs @@ -0,0 +1,40 @@ +using Mocoding.Ofx.Protocol; + +namespace Mocoding.Ofx.Client.Requests +{ + /// + /// Creates request to fetch list of accounts. + /// + /// + public class AccountsRequestBuilder : AbstractTransactionRequestBuilder + { + + /// + /// Creates the request with defaults. + /// + /// This instance. + protected override AccountInfoTransactionRequest CreateRequest() + { + return new AccountInfoTransactionRequest() + { + CLTCOOKIE = "4", + ACCTINFORQ = new AccountInfoRequest() {DTACCTUP = "19900101"} + }; + } + + /// + /// Builds the message set to be added to the OFX top level message set collection. + /// + /// This instance. + public override AbstractTopLevelMessageSet Build() + { + return new SignupRequestMessageSetV1() + { + Items = new AbstractRequest[] + { + Request + } + }; + } + } +} diff --git a/src/Mocoding.Ofx.Client/Requests/AuthenticateRequestBuilder.cs b/src/Mocoding.Ofx.Client/Requests/AuthenticateRequestBuilder.cs new file mode 100644 index 0000000..ea1bc74 --- /dev/null +++ b/src/Mocoding.Ofx.Client/Requests/AuthenticateRequestBuilder.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using Mocoding.Ofx.Client.Interfaces; +using Mocoding.Ofx.Protocol; + +namespace Mocoding.Ofx.Client.Requests +{ + /// + /// Creates authentication request. + /// + /// + public class AuthenticateRequestBuilder : RequestBuilder + { + /// + /// Sets client identifier. + /// + /// The client identifier. + /// This instance. + public virtual AuthenticateRequestBuilder ClientId(string clientId) + { + Request.CLIENTUID = clientId; + return this; + } + + /// + /// Sets timestamp. + /// + /// The timestamp. + /// This instance. + public virtual AuthenticateRequestBuilder Timestamp(string timestamp) + { + Request.DTCLIENT = timestamp; + return this; + } + + /// + /// Sets bank information - fid and org. + /// + /// The fid. + /// The org. + /// This instance. + public virtual AuthenticateRequestBuilder Bank(string fid, string org) + { + Request.FI = new FinancialInstitution() + { + FID = fid, + ORG = org + }; + return this; + } + + /// + /// Sets credentials. + /// + /// The username. + /// The password. + /// This instance. + public virtual AuthenticateRequestBuilder Credentials(string username, string password) + { + Request.Items = new[] {username, password}; + Request.ItemsElementName = new[] {ItemsChoiceType.USERID, ItemsChoiceType.USERPASS}; + + return this; + } + + /// + /// Sets application version. + /// + /// The name. + /// The version. + /// This instance. + public virtual AuthenticateRequestBuilder AppVersion(string name, string version) + { + Request.APPID = name; + Request.APPVER = version; + + return this; + } + + /// + /// Creates the request with defaults. + /// + /// + protected override SignonRequest CreateRequest() + { + return new SignonRequest() + { + LANGUAGE = LanguageEnum.ENG // should we expose this as well? + }; + } + + /// + /// Builds the message set to be added to the OFX top level message set collection. + /// + /// + public override AbstractTopLevelMessageSet Build() + { + return new SignonRequestMessageSetV1() + { + SONRQ = Request + }; + } + } +} diff --git a/src/Mocoding.Ofx.Client/Requests/BankStatementRequestBuilder.cs b/src/Mocoding.Ofx.Client/Requests/BankStatementRequestBuilder.cs new file mode 100644 index 0000000..d7256e9 --- /dev/null +++ b/src/Mocoding.Ofx.Client/Requests/BankStatementRequestBuilder.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Mocoding.Ofx.Protocol; + +namespace Mocoding.Ofx.Client.Requests +{ + /// + /// Builds request to fetch bank account statement. + /// + /// + public class BankStatementRequestBuilder : AbstractTransactionRequestBuilder + { + + /// + /// Sets account information. + /// + /// The account number. + /// The routing. + /// The type. + /// This instance. + public BankStatementRequestBuilder Account(string accountNumber, string routing, string type) + { + Request.STMTRQ.BANKACCTFROM = new BankAccount() + { + ACCTID = accountNumber, + BANKID = routing, + ACCTTYPE = (AccountEnum)Enum.Parse(typeof(AccountEnum), type, true) + }; + + return this; + } + + /// + /// Sets date filter. + /// + /// The start date. + /// The end date. + /// This instance. + public BankStatementRequestBuilder Filter(string startDate, string endDate) + { + Request.STMTRQ.INCTRAN = new IncTransaction() + { + DTSTART = startDate, + DTEND = endDate, + INCLUDE = BooleanType.Y + }; + + return this; + } + + + /// + /// Creates the request with defaults. + /// + /// + protected override StatementTransactionRequest CreateRequest() + { + return new StatementTransactionRequest() + { + STMTRQ = new StatementRequest() + }; + } + + /// + /// Builds the message set to be added to the OFX top level message set collection. + /// + /// + public override AbstractTopLevelMessageSet Build() + { + return new BankRequestMessageSetV1() + { + Items = new AbstractRequest[] + { + Request + } + }; + } + } +} diff --git a/src/Mocoding.Ofx.Client/Requests/CreditCardStatementRequestBuilder.cs b/src/Mocoding.Ofx.Client/Requests/CreditCardStatementRequestBuilder.cs new file mode 100644 index 0000000..ba57f20 --- /dev/null +++ b/src/Mocoding.Ofx.Client/Requests/CreditCardStatementRequestBuilder.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Mocoding.Ofx.Protocol; + +namespace Mocoding.Ofx.Client.Requests +{ + /// + /// Builds request to fetch bank account statement. + /// + /// + public class CreditCardStatementRequestBuilder : AbstractTransactionRequestBuilder + { + + /// + /// Sets account information. + /// + /// The identifier. + /// This instance. + public CreditCardStatementRequestBuilder Account(string id) + { + Request.CCSTMTRQ.CCACCTFROM = new CreditCardAccount() + { + ACCTID = id + }; + + return this; + } + /// + /// Sets date filter. + /// + /// The start date. + /// The end date. + /// This instance. + public CreditCardStatementRequestBuilder Filter(string startDate, string endDate) + { + Request.CCSTMTRQ.INCTRAN = new IncTransaction() + { + DTSTART = startDate, + DTEND = endDate, + INCLUDE = BooleanType.Y + }; + + return this; + } + + /// + /// Creates the request with defaults. + /// + /// + protected override CreditCardStatementTransactionRequest CreateRequest() + { + return new CreditCardStatementTransactionRequest() + { + CCSTMTRQ = new CreditCardStatementRequest() + }; + } + + /// + /// Builds the message set to be added to the OFX top level message set collection. + /// + /// + public override AbstractTopLevelMessageSet Build() + { + return new CreditcardRequestMessageSetV1() + { + Items = new AbstractTransactionRequest[] + { + Request + } + }; + } + } +} diff --git a/src/Mocoding.Ofx.Client/Requests/RequestBuilder.cs b/src/Mocoding.Ofx.Client/Requests/RequestBuilder.cs new file mode 100644 index 0000000..6f9d848 --- /dev/null +++ b/src/Mocoding.Ofx.Client/Requests/RequestBuilder.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Mocoding.Ofx.Client.Interfaces; +using Mocoding.Ofx.Protocol; + +namespace Mocoding.Ofx.Client.Requests +{ + /// Builder Pattern base class. + /// + /// + public abstract class RequestBuilder : IRequestBuilder where T : class + { + private T _requestMessageSet; + + /// + /// Creates the request with defaults. + /// + /// + protected abstract T CreateRequest(); + + /// + /// Gets current request instance. + /// + /// + /// The request. + /// + protected T Request => _requestMessageSet ?? (_requestMessageSet = CreateRequest()); + + /// + /// Builds the message set to be added to the OFX top level message set collection. + /// + /// + public abstract AbstractTopLevelMessageSet Build(); + } +} diff --git a/src/Mocoding.Ofx/DefaultOfxSerializerFactory.cs b/src/Mocoding.Ofx/DefaultOfxSerializerFactory.cs new file mode 100644 index 0000000..56b648c --- /dev/null +++ b/src/Mocoding.Ofx/DefaultOfxSerializerFactory.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Mocoding.Ofx.Interfaces; +using Mocoding.Ofx.Serializers; + +namespace Mocoding.Ofx +{ + /// + /// Default implementation for OFX Serializer factory. + /// + /// + public class DefaultOfxSerializerFactory : IOfxSerializerFactory + { + /// + /// Creates the specified OFX Serializer based on OFX Version. + /// + /// The OFX version. + /// + /// Serializer implementation. + /// + /// Version {version} is not supported. Supported versions are '1' - sgml and '2' - xml. + public IOfxSerializer Create(OfxVersionEnum version) + { + switch (version) + { + case OfxVersionEnum.Version1x: + return new OfxSgmlSerializer(); + case OfxVersionEnum.Version2x: + return new OfxXmlSerializer(); + default: + throw new NotSupportedException($"Version {version} is not supported. Supported versions are '1' - sgml and '2' - xml"); + } + } + } +} diff --git a/src/Mocoding.Ofx/Interfaces/IOfxSerializer.cs b/src/Mocoding.Ofx/Interfaces/IOfxSerializer.cs new file mode 100644 index 0000000..1a84abf --- /dev/null +++ b/src/Mocoding.Ofx/Interfaces/IOfxSerializer.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Mocoding.Ofx.Protocol; + +namespace Mocoding.Ofx.Interfaces +{ + /// + /// Abstraction over serializing and deserializing model in various protocol versions. + /// + public interface IOfxSerializer + { + /// + /// Serializes the model. + /// + /// The model. + /// Serialized representation. + string Serialize(OFX model); + + /// + /// Deserializes into model. + /// + /// The input string. + /// Parsed result - Model. + OFX Deserialize(string inputString); + } +} diff --git a/src/Mocoding.Ofx/Interfaces/IOfxSerializerFactory.cs b/src/Mocoding.Ofx/Interfaces/IOfxSerializerFactory.cs new file mode 100644 index 0000000..4f05623 --- /dev/null +++ b/src/Mocoding.Ofx/Interfaces/IOfxSerializerFactory.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Mocoding.Ofx.Protocol; + +namespace Mocoding.Ofx.Interfaces +{ + /// + /// Abstract factory for OFX Serializer based on version. + /// + public interface IOfxSerializerFactory + { + /// + /// Creates the specified OFX Serializer based on OFX Version. + /// + /// The OFX version. + /// Serializer implementation. + IOfxSerializer Create(OfxVersionEnum version); + } +} diff --git a/src/Mocoding.Ofx/Mocoding.Ofx.csproj b/src/Mocoding.Ofx/Mocoding.Ofx.csproj index 8a64a92..a6f28e9 100644 --- a/src/Mocoding.Ofx/Mocoding.Ofx.csproj +++ b/src/Mocoding.Ofx/Mocoding.Ofx.csproj @@ -1,25 +1,27 @@  - OFX Schema Object, OFX Parser and Sgml Parser for .NET Standard. + OFX Schema Object, OFX Parser and Sgml Parser for .NET Standard. Also includes Statement Parser for QFX files. netstandard1.0 - netstandadrd;ofx;qfx;money;expense manager;finance;parser;serializer;sgml - Made stylecop as private assets. + netstandadrd;ofx;qfx;money;expense manager;finance;parser;serializer;sgml;treasure management + Refactoring and Redesign. https://mocoding.blob.core.windows.net/resources/ofx/nugetIcon.png https://github.com/mocoding-software/ofx https://raw.githubusercontent.com/mocoding-software/ofx/master/LICENSE git https://github.com/mocoding-software/ofx ..\..\_stylecop\StyleCop.ruleset - MOCODING + MOCODING LLC,Dennis Miasoutov + Full + true - + - + all - 1.1.0-beta006 + 1.1.118 diff --git a/src/Mocoding.Ofx/Models/Account.cs b/src/Mocoding.Ofx/Models/Account.cs new file mode 100644 index 0000000..7a2c8e0 --- /dev/null +++ b/src/Mocoding.Ofx/Models/Account.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Mocoding.Ofx.Models +{ + /// + /// Type of the Account. + /// + public enum AccountTypeEnum + { + /// The checking account + Checking = 1, + + /// The credit account + Credit = 2, + } + + /// + /// Contains properties of the bank or credit card account retrieved from OFX. + /// + public class Account + { + /// + /// Gets or sets the account description. + /// + /// + /// The description. + /// + public string Description { get; set; } + + /// + /// Gets or sets the account description. + /// + /// + /// The description. + /// + public string Phone { get; set; } + + /// + /// Gets or sets the sub type of the account. + /// + /// + /// The type of the sub. + /// + public string SubType { get; set; } + + /// + /// Gets or sets the account type. + /// + /// + /// The type. + /// + public AccountTypeEnum Type { get; set; } + + /// + /// Gets or sets the account identifier. + /// + /// + /// The identifier. + /// + public string Id { get; set; } + + /// + /// Gets or sets the bank identifier. + /// + /// + /// The bank identifier. + /// + public string BankId { get; set; } + + /// + /// Gets or sets the account status. + /// + /// + /// The status. + /// + public string Status { get; set; } + } +} \ No newline at end of file diff --git a/src/Mocoding.Ofx/Models/Statement.cs b/src/Mocoding.Ofx/Models/Statement.cs new file mode 100644 index 0000000..c8092ef --- /dev/null +++ b/src/Mocoding.Ofx/Models/Statement.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Mocoding.Ofx.Models +{ + /// + /// Contains data about transactions of single bank account. + /// + public class Statement + { + /// + /// Gets or sets the account number. + /// + /// + /// The account number. + /// + public string AccountNumber { get; set; } + + /// + /// Gets or sets the currency. + /// + /// + /// The currency. + /// + public string Currency { get; set; } + + /// + /// Gets or sets the ledger balance. + /// + /// + /// The ledger balance. + /// + public decimal LedgerBalance { get; set; } + + /// + /// Gets or sets the available balance. + /// + /// + /// The available balance. + /// + public decimal AvailableBalance { get; set; } + + /// + /// Gets or sets the transactions. + /// + /// + /// The transactions. + /// + public Transaction[] Transactions { get; set; } + } +} diff --git a/src/Mocoding.Ofx/Models/Transaction.cs b/src/Mocoding.Ofx/Models/Transaction.cs new file mode 100644 index 0000000..d3296ff --- /dev/null +++ b/src/Mocoding.Ofx/Models/Transaction.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Mocoding.Ofx.Models +{ + /// + /// Contains data about single transactions. + /// + public class Transaction + { + /// + /// Gets or sets the identifier. + /// + /// + /// The identifier. + /// + public string Id { get; set; } + + /// + /// Gets or sets the type. + /// + /// + /// The type. + /// + public string Type { get; set; } + + /// + /// Gets or sets the amount. + /// + /// + /// The amount. + /// + public decimal Amount { get; set; } + + /// + /// Gets or sets the date posted. + /// + /// + /// The date posted. + /// + public DateTime DatePosted { get; set; } + + /// + /// Gets or sets the description. + /// + /// + /// The description. + /// + public string Description { get; set; } + + /// + /// Gets or sets the memo. + /// + /// + /// The memo. + /// + public string Memo { get; set; } + } +} diff --git a/src/Mocoding.Ofx/OfxAccountsParser.cs b/src/Mocoding.Ofx/OfxAccountsParser.cs new file mode 100644 index 0000000..673eb09 --- /dev/null +++ b/src/Mocoding.Ofx/OfxAccountsParser.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Mocoding.Ofx.Interfaces; +using Mocoding.Ofx.Models; +using Mocoding.Ofx.Protocol; + +namespace Mocoding.Ofx +{ + /// + /// Contains methods to parse OFX payload to get account information. + /// + public static class OfxAccountsParser + { + /// + /// Parses the ofx payload to get account information. + /// + /// The ofx payload. + /// Array of accounts. + public static IEnumerable Parse(OFX ofxPayload) + { + var messageSet = + ofxPayload.Items.FirstOrDefault(_ => _ is SignupResponseMessageSetV1) as SignupResponseMessageSetV1; + var accountsResponse = + messageSet?.Items.FirstOrDefault(_ => _ is AccountInfoTransactionResponse) as AccountInfoTransactionResponse; + + if (accountsResponse?.ACCTINFORS?.ACCTINFO == null) + yield break; + + foreach (var accountInfoWrap in accountsResponse.ACCTINFORS?.ACCTINFO) + { + foreach (var accountInfo in accountInfoWrap.Items) + { + switch (accountInfo) + { + case CreditCardAccountInfo cc: + yield return ParseCreditCardAccount(cc); + break; + case BankAccountInfo bank: + yield return ParseBankAccount(bank); + break; + default: + continue; + } + } + } + } + + private static Account ParseBankAccount(BankAccountInfo bank) + { + return new Account() + { + Type = AccountTypeEnum.Checking, + Id = bank.BANKACCTFROM.ACCTID, + BankId = bank.BANKACCTFROM.BANKID, + SubType = bank.BANKACCTFROM.ACCTTYPE.ToString(), + Status = bank.SVCSTATUS.ToString(), + }; + } + + private static Account ParseCreditCardAccount(CreditCardAccountInfo cc) + { + return new Account() + { + Type = AccountTypeEnum.Credit, + Id = cc.CCACCTFROM.ACCTID, + Status = cc.SVCSTATUS.ToString(), + }; + } + } +} diff --git a/src/Mocoding.Ofx/OfxSerializer.cs b/src/Mocoding.Ofx/OfxSerializer.cs deleted file mode 100644 index fdb57e5..0000000 --- a/src/Mocoding.Ofx/OfxSerializer.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using Mocoding.Ofx.Protocol; - -namespace Mocoding.Ofx -{ - public class OfxSerializer - { - public const string Default103Header = - @"OFXHEADER:100 -DATA:OFXSGML -VERSION:103 -SECURITY:NONE -ENCODING:USASCII -CHARSET:1252 -COMPRESSION:NONE -OLDFILEUID:NONE -NEWFILEUID:NONE - -"; - - private readonly SgmlSerializer _sgmlSerializer; - - public OfxSerializer() - { - _sgmlSerializer = new SgmlSerializer(); - } - - public string Serialize(OFX request) - { - var sgml = _sgmlSerializer.Serialize(request); - return Default103Header + sgml; - } - - public OFX Deserialize(string responseBody) - { - var ofxDataStartIndex = responseBody.IndexOf("", StringComparison.OrdinalIgnoreCase); - if (ofxDataStartIndex == -1) - throw new FormatException(" element is not present in the response body"); - var sgml = responseBody.Substring(ofxDataStartIndex); - - var result = _sgmlSerializer.Deserialize(sgml); - - return result; - } - } -} diff --git a/src/Mocoding.Ofx/OfxStatementParser.cs b/src/Mocoding.Ofx/OfxStatementParser.cs new file mode 100644 index 0000000..32106b6 --- /dev/null +++ b/src/Mocoding.Ofx/OfxStatementParser.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Mocoding.Ofx.Interfaces; +using Mocoding.Ofx.Models; +using Mocoding.Ofx.Protocol; + +namespace Mocoding.Ofx +{ + /// + /// Contains methods to parse OFX payload to get statement and transaction information. + /// + public static class OfxStatementParser + { + private const string DateTimeFormat = "yyyyMMddHHmmss"; + + /// + /// Parses the specified ofx payload and converts it to statement. + /// Accepts both credit card and bank OFX strings. + /// + /// The ofx payload. + /// Returns parsed statement. + /// Can't find Credit Card or Bank Statement'. + public static Statement Parse(OFX ofxPayload) + { + foreach (var messageSet in ofxPayload.Items) + { + switch (messageSet) + { + case CreditcardResponseMessageSetV1 cc: + return ParseCreditCardStatement(cc.Items.FirstOrDefault() as CreditCardStatementTransactionResponse); + case BankResponseMessageSetV1 bank: + return ParseBankStatement(bank.Items.FirstOrDefault() as StatementTransactionResponse); + default: + continue; + } + } + + throw new Exception("Can't find Credit Card or Bank Statement'"); + } + + private static Statement ParseBankStatement(StatementTransactionResponse bankStatement) + { + var transactions = ParseTransactions(bankStatement.STMTRS.BANKTRANLIST); + return new Statement() + { + AccountNumber = bankStatement.STMTRS.BANKACCTFROM?.ACCTID, + Currency = bankStatement.STMTRS.CURDEF, + AvailableBalance = ParseBalance(bankStatement.STMTRS.AVAILBAL?.BALAMT), + LedgerBalance = ParseBalance(bankStatement.STMTRS.LEDGERBAL?.BALAMT), + Transactions = transactions, + }; + } + + private static decimal ParseBalance(string balance) + { + if (string.IsNullOrEmpty(balance) || !decimal.TryParse(balance, out var amount)) + amount = 0; + + return amount; + } + + private static Statement ParseCreditCardStatement(CreditCardStatementTransactionResponse creditCardStatement) + { + var transactions = ParseTransactions(creditCardStatement.CCSTMTRS.BANKTRANLIST); + return new Statement() + { + AccountNumber = creditCardStatement.CCSTMTRS.CCACCTFROM?.ACCTID, + Currency = creditCardStatement.CCSTMTRS.CURDEF, + AvailableBalance = ParseBalance(creditCardStatement.CCSTMTRS.AVAILBAL?.BALAMT), + LedgerBalance = ParseBalance(creditCardStatement.CCSTMTRS.LEDGERBAL?.BALAMT), + Transactions = transactions, + }; + } + + private static Transaction[] ParseTransactions(BankTransactionList transactionsList) + { + return transactionsList?.STMTTRN == null ? new Transaction[0] : transactionsList.STMTTRN.Select(MapToModel).ToArray(); + } + + private static Transaction MapToModel(StatementTransaction transactionDto) + { + var amount = decimal.Parse(transactionDto.TRNAMT); + + var truncatedValue = transactionDto.DTPOSTED.Length == DateTimeFormat.Length + ? transactionDto.DTPOSTED + : transactionDto.DTPOSTED.Length > DateTimeFormat.Length + ? transactionDto.DTPOSTED.Substring(0, DateTimeFormat.Length) + : transactionDto.DTPOSTED + new string('0', DateTimeFormat.Length - transactionDto.DTPOSTED.Length); + var datePosted = DateTime.ParseExact(truncatedValue, DateTimeFormat, CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal); + + var description = transactionDto.Item is Payee payee + ? payee.NAME + : transactionDto.Item as string; + + return new Transaction() + { + Memo = transactionDto.MEMO, + Description = description, + DatePosted = datePosted, + Amount = amount, + Type = transactionDto.TRNTYPE.ToString(), + Id = transactionDto.FITID, + }; + } + } +} diff --git a/src/Mocoding.Ofx/OfxVersionEnum.cs b/src/Mocoding.Ofx/OfxVersionEnum.cs new file mode 100644 index 0000000..db834f4 --- /dev/null +++ b/src/Mocoding.Ofx/OfxVersionEnum.cs @@ -0,0 +1,18 @@ +namespace Mocoding.Ofx +{ + /// + /// OFX Version Definitions. + /// + public enum OfxVersionEnum + { + /// + /// The version1x + /// + Version1x = 1, + + /// + /// The version2x + /// + Version2x = 2, + } +} \ No newline at end of file diff --git a/src/Mocoding.Ofx/OFX_XSD_Generated.cs b/src/Mocoding.Ofx/Protocol/OFX_XSD_Generated.cs similarity index 99% rename from src/Mocoding.Ofx/OFX_XSD_Generated.cs rename to src/Mocoding.Ofx/Protocol/OFX_XSD_Generated.cs index d9cecff..9ca4986 100644 --- a/src/Mocoding.Ofx/OFX_XSD_Generated.cs +++ b/src/Mocoding.Ofx/Protocol/OFX_XSD_Generated.cs @@ -24456,7 +24456,7 @@ public string URL public partial class CreditCardStatementResponse { - private CurrencyEnum cURDEFField; + //private CurrencyEnum cURDEFField; private CreditCardAccount cCACCTFROMField; @@ -24470,19 +24470,23 @@ public partial class CreditCardStatementResponse private string mKTGINFOField; + // DM: Do not want to hard code Currency Symbol. Why? In case of unkown currency - may fail. /// [System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)] - public CurrencyEnum CURDEF - { - get - { - return this.cURDEFField; - } - set - { - this.cURDEFField = value; - } - } + public string CURDEF { get; set; } + + //[System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)] + //public CurrencyEnum CURDEF + //{ + // get + // { + // return this.cURDEFField; + // } + // set + // { + // this.cURDEFField = value; + // } + //} /// [System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)] @@ -30216,7 +30220,7 @@ public Closing[] CLOSING public partial class StatementResponse { - private CurrencyEnum cURDEFField; + //private CurrencyEnum cURDEFField; private BankAccount bANKACCTFROMField; @@ -30230,19 +30234,23 @@ public partial class StatementResponse private string mKTGINFOField; - /// - [System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)] - public CurrencyEnum CURDEF - { - get - { - return this.cURDEFField; - } - set - { - this.cURDEFField = value; - } - } + // DM: Do not want to hard code Currency Symbol. Why? In case of unkown currency - may fail. + /// + [System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)] + public string CURDEF { get; set; } + + //[System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)] + //public CurrencyEnum CURDEF + //{ + // get + // { + // return this.cURDEFField; + // } + // set + // { + // this.cURDEFField = value; + // } + //} /// [System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)] diff --git a/src/Mocoding.Ofx/Serializers/BaseSerializer.cs b/src/Mocoding.Ofx/Serializers/BaseSerializer.cs new file mode 100644 index 0000000..bc92a75 --- /dev/null +++ b/src/Mocoding.Ofx/Serializers/BaseSerializer.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using Mocoding.Ofx.Interfaces; +using Mocoding.Ofx.Protocol; + +namespace Mocoding.Ofx.Serializers +{ + /// + /// Base class for OFX Serializer. + /// + /// + public abstract class BaseSerializer : IOfxSerializer + { + /// + /// The default XML header. + /// + public const string XmlHeader = @""; + + private readonly XmlSerializer _serializer; + + /// + /// Initializes a new instance of the class. + /// Instantiates internal class. + /// + protected BaseSerializer() + { + _serializer = new XmlSerializer(typeof(OFX)); + } + + /// + /// Serializes the model. + /// + /// The model. + /// + /// Serialized representation. + /// + public abstract string Serialize(OFX model); + + /// + /// Deserializes into model. + /// + /// The input string. + /// Parsed result - Model. + public abstract OFX Deserialize(string inputString); + + /// + /// Uses to serialize OFX Model into xml. + /// + /// The request. + /// Xml string. + protected string SerializeInternal(OFX request) + { + var ns = new XmlSerializerNamespaces(); + ns.Add(string.Empty, string.Empty); + + var writer = new StringWriter(); + using (var xmlWriter = XmlWriter.Create(writer, new XmlWriterSettings { Indent = false, OmitXmlDeclaration = true })) + _serializer.Serialize(xmlWriter, (object)request, ns); + + var xml = writer.ToString(); + + return xml; + } + + /// + /// Uses to serialize OFX Model from xml. + /// + /// This method cuts everything in front of OFX declaration. + /// The input XML string. + /// Returns deserialized model. + /// OFX element is not present in the response body. + protected OFX DeserializeInternal(string input) + { + // getting root + var ofxDataStartIndex = input.IndexOf("", StringComparison.OrdinalIgnoreCase); + if (ofxDataStartIndex == -1) + throw new FormatException(" element is not present in the response body."); + var ofxBody = input.Substring(ofxDataStartIndex); + + // appending xml header + var xml = XmlHeader + ofxBody; + + // xml part + var reader = new StringReader(xml); + var result = (OFX)_serializer.Deserialize(reader); + return result; + } + } +} diff --git a/src/Mocoding.Ofx/Serializers/OfxSgmlSerializer.cs b/src/Mocoding.Ofx/Serializers/OfxSgmlSerializer.cs new file mode 100644 index 0000000..f591a7b --- /dev/null +++ b/src/Mocoding.Ofx/Serializers/OfxSgmlSerializer.cs @@ -0,0 +1,57 @@ +using System.Text.RegularExpressions; +using Mocoding.Ofx.Protocol; + +namespace Mocoding.Ofx.Serializers +{ + /// + /// Contains Sgml serialization/deserialization logic for OFX Version 1.x + /// Uses XML serialization and some regex magic to get desired result. + /// + /// + public class OfxSgmlSerializer : BaseSerializer + { + private readonly string[] _default102Headers = new[] + { + "OFXHEADER:100", + "DATA:OFXSGML", + "VERSION:102", + "SECURITY:NONE", + "ENCODING:USASCII", + "CHARSET:1252", + "COMPRESSION:NONE", + "OLDFILEUID:NONE", + "NEWFILEUID:NONE", + }; + + /// + /// Serializes the model. + /// + /// The model. + /// + /// Serialized representation. + /// + public override string Serialize(OFX model) + { + var xml = SerializeInternal(model); + + var sgml = Regex.Replace(xml, @"<([A-Za-z0-9_\-\.]+)>([^<]+)", "<$1>$2"); + var result = Regex.Replace(sgml, @"<[A-Za-z0-9_\-]+ />", string.Empty); + + return string.Join("\n", string.Join("\n", _default102Headers), string.Empty, result); + } + + /// + /// Deserializes into model. + /// + /// The input string. + /// Parsed result - Model. + public override OFX Deserialize(string inputString) + { + // convert sgml to xml using Regex + var xml = Regex.Replace(inputString, @"<([A-Za-z0-9_\-\.]+)>([^<]+)", "<$1>$2"); + var result = DeserializeInternal(xml); + + return result; + } + } +} diff --git a/src/Mocoding.Ofx/Serializers/OfxXmlSerializer.cs b/src/Mocoding.Ofx/Serializers/OfxXmlSerializer.cs new file mode 100644 index 0000000..8004342 --- /dev/null +++ b/src/Mocoding.Ofx/Serializers/OfxXmlSerializer.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Mocoding.Ofx.Protocol; + +namespace Mocoding.Ofx.Serializers +{ + /// + /// Contains XML serialization/deserialization logic for OFX Version 2.x. + /// + /// + public class OfxXmlSerializer : BaseSerializer + { + private const string Default211Header = @""; + + /// + /// Serializes the model. + /// + /// The model. + /// + /// Serialized representation. + /// + public override string Serialize(OFX model) + { + var xml = SerializeInternal(model); + return string.Join("\n", XmlHeader, Default211Header, xml); + } + + /// + /// Deserializes into model. + /// + /// The input string. + /// Parsed result - Model. + public override OFX Deserialize(string inputString) + { + return DeserializeInternal(inputString); + } + } +} diff --git a/src/Mocoding.Ofx/SgmlSerializer.cs b/src/Mocoding.Ofx/SgmlSerializer.cs deleted file mode 100644 index 1f1b724..0000000 --- a/src/Mocoding.Ofx/SgmlSerializer.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.IO; -using System.Text.RegularExpressions; -using System.Xml; -using System.Xml.Serialization; - -namespace Mocoding.Ofx -{ - public class SgmlSerializer - { - private readonly XmlSerializer _serializer; - - public SgmlSerializer() - { - _serializer = new XmlSerializer(typeof(T)); - } - - public string Serialize(T request) - { - var ns = new XmlSerializerNamespaces(); - ns.Add(string.Empty, string.Empty); - - // Making xml first - var writer = new StringWriter(); - using (var xmlWriter = XmlWriter.Create(writer, new XmlWriterSettings { Indent = false, OmitXmlDeclaration = true })) - _serializer.Serialize(xmlWriter, (object)request, ns); - - var xml = writer.ToString(); - - // super HACK! :) converting to sgml by removing closing tags for elements with simple value. - var sgml = Regex.Replace(xml, @"<([A-Za-z0-9_\-\.]+)>([^<]+)", "<$1>$2"); - var result = Regex.Replace(sgml, @"<[A-Za-z0-9_\-]+ />", string.Empty); - - return result; - } - - public T Deserialize(string sgml) - { - const string xmlDeclaration = @""; - - // converting to xml by adding closing tags for elements with simple value. - var xml = xmlDeclaration + Regex.Replace(sgml, @"<([A-Za-z0-9_\-\.]+)>([^<]+)", "<$1>$2"); - - // xml part - var reader = new StringReader(xml); - var result = (T)_serializer.Deserialize(reader); - return result; - } - } -} diff --git a/test/Mocoding.Ofx.Client.Tests/Mocoding.Ofx.Client.Tests.csproj b/test/Mocoding.Ofx.Client.Tests/Mocoding.Ofx.Client.Tests.csproj deleted file mode 100644 index 3ea68ac..0000000 --- a/test/Mocoding.Ofx.Client.Tests/Mocoding.Ofx.Client.Tests.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - netcoreapp2.0 - - - - - - - - - - - - - - - - diff --git a/test/Mocoding.Ofx.Client.Tests/OfxClientTests.cs b/test/Mocoding.Ofx.Client.Tests/OfxClientTests.cs deleted file mode 100644 index b7ebc2f..0000000 --- a/test/Mocoding.Ofx.Client.Tests/OfxClientTests.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.Collections.Immutable; -using System.Threading.Tasks; -using Mocoding.Ofx.Client.Exceptions; -using Mocoding.Ofx.Client.Interfaces; -using Mocoding.Ofx.Client.Models; -using Mocoding.Ofx.Tests; -using NSubstitute; -using Xunit; - -namespace Mocoding.Ofx.Client.Tests -{ - public class OfxClientTests - { - readonly Uri ApiUrl = new Uri("http://localhost:5000/api/ofx"); - - [Fact] - public async Task AccountListTest() - { - - var expectedRequest = - EmbeddedResourceReader.ReadRequestAsString("accountList.sgml"); - var expectedResponse = - EmbeddedResourceReader.ReadResponseAsString("accountList.sgml"); - - var options = new OfxClientOptions(ApiUrl, "HAN", "5959", "testUserAccount", "testUserPassword"); - var transportMock = Substitute.For(); - var utilsMock = Substitute.For(); - - transportMock.PostRequest(Arg.Any(), Arg.Any()).Returns(Task.FromResult(expectedResponse)); - - utilsMock.GenerateTransactionId().Returns("0000000000"); - utilsMock.GetCurrentDateTime().Returns("20150127131257"); - utilsMock.GetClientUid(Arg.Is(val => val == "testUserAccount")).Returns("SomeGuidHere"); - - var client = new OfxClient(options, transportMock, utilsMock); - var result = await client.GetAccounts(); - var account = result; - - Assert.NotEqual(ImmutableArray.Empty, account); - Assert.Equal(2, account.Length); - } - - [Fact] - public async Task CreditCardTransactionsListTest() - { - - var expectedRequest = - EmbeddedResourceReader.ReadRequestAsString("creditCardTransactions.sgml"); - var expectedResponse = - EmbeddedResourceReader.ReadResponseAsString("creditCardTransactions.sgml"); - - var options = new OfxClientOptions(ApiUrl, "HAN", "5959", "testUserAccount", "testUserPassword"); - var transportMock = Substitute.For(); - var utilsMock = Substitute.For(); - - transportMock.PostRequest(Arg.Any(), Arg.Any()).Returns(Task.FromResult(expectedResponse)); - - utilsMock.GenerateTransactionId().Returns("0000000000"); - utilsMock.GetCurrentDateTime().Returns("XXXXXXXXXXXXXX"); - - var client = new OfxClient(options, transportMock, utilsMock); - var result = await client.GetTransactions(new Account(AccountTypeEnum.Credit, "XXXXXXXXXXXX3158")); - var transactions = result; - - Assert.NotNull(transactions); - Assert.Equal(2, transactions.Items.Length); - } - - [Fact] - public async Task BankTransactionsListTest() - { - - var expectedRequest = - EmbeddedResourceReader.ReadRequestAsString("bankTransactions.sgml"); - var expectedResponse = - EmbeddedResourceReader.ReadResponseAsString("bankTransactions.sgml"); - - var options = new OfxClientOptions(ApiUrl, "HAN", "5959", "testUserAccount", "testUserPassword"); - var transportMock = Substitute.For(); - var utilsMock = Substitute.For(); - - transportMock.PostRequest(Arg.Any(), Arg.Any()).Returns(Task.FromResult(expectedResponse)); - - utilsMock.GenerateTransactionId().Returns("0000000000"); - utilsMock.GetCurrentDateTime().Returns("XXXXXXXXXXXXXX"); - - var client = new OfxClient(options, transportMock, utilsMock); - var result = await client.GetTransactions(new Account(AccountTypeEnum.Checking, "YYYYYYYY1924", "XXXXXXXXX")); - var transactions = result; - - Assert.NotNull(transactions); - Assert.Equal(2, transactions.Items.Length); - } - - [Fact] - public async Task FailedRequestTest() - { - var expectedResponse = - EmbeddedResourceReader.ReadResponseAsString("error.sgml"); - - var options = new OfxClientOptions(ApiUrl, "HAN", "5959", "testUserAccount", "testUserPassword"); - var transportMock = Substitute.For(); - var utilsMock = Substitute.For(); - - utilsMock.GenerateTransactionId().Returns("0000000000"); - utilsMock.GetCurrentDateTime().Returns("XXXXXXXXXXXXXX"); - - transportMock.PostRequest(Arg.Any(), Arg.Any()).Returns(Task.FromResult(expectedResponse)); - - var client = new OfxClient(options, transportMock, utilsMock); - var ex = await Assert.ThrowsAsync(() => client.GetAccounts()); - Assert.Equal("An incorrect username/password combination has been entered. Please try again.", ex.Message); - } - - [Fact] - public async Task ErrorRequestTest() - { - var options = new OfxClientOptions(ApiUrl, "HAN", "5959", "testUserAccount", "testUserPassword"); - var transportMock = Substitute.For(); - var utilsMock = Substitute.For(); - - utilsMock.GenerateTransactionId().Returns("0000000000"); - utilsMock.GetCurrentDateTime().Returns("XXXXXXXXXXXXXX"); - - transportMock.PostRequest(Arg.Any(), Arg.Any()).Returns(Task.FromResult(string.Empty)); - - var client = new OfxClient(options, transportMock, utilsMock); - var ex = await Assert.ThrowsAsync(() => client.GetAccounts()); - - Assert.Equal(" element is not present in the response body", ex.Message); - } - } -} diff --git a/test/Mocoding.Ofx.Tests/Client/DiscoverProtocolUtilsTests.cs b/test/Mocoding.Ofx.Tests/Client/DiscoverProtocolUtilsTests.cs new file mode 100644 index 0000000..6d0fcac --- /dev/null +++ b/test/Mocoding.Ofx.Tests/Client/DiscoverProtocolUtilsTests.cs @@ -0,0 +1,74 @@ +using Mocoding.Ofx.Client; +using Mocoding.Ofx.Client.Defaults; +using Mocoding.Ofx.Client.Discover; +using Xunit; + +namespace Mocoding.Ofx.Tests.Client +{ + public class DiscoverProtocolUtilsTests + { + [Fact] + public void PrepareDiscoverRequestTest() + { + // arrange + var expected = @"POST / HTTP/1.1 +Content-Type: application/x-ofx +Host: server:443 +Content-Length: 7 +Connection: close + +content"; + + // act + var actual = DiscoverProtocolUtils.PrepareRequest("server", 443, "content"); + + // assert + Assert.Equal(expected, actual); + + } + + [Fact] + public void GetClientUidTest() + { + // arrange + var utils = new DiscoverProtocolUtils(new DefaultOfxRequestLocator()); + + // act + var actual = utils.GetClientUid("test"); + + // assert + Assert.Null(actual); + + } + + [Fact] + public void DiscoverFactoryTest() + { + // arrange + var factory = new DiscoverProtocolUtilsFactory(); + var expected = typeof(DiscoverProtocolUtils); + + // act + var actual = factory.Create("7101"); + + // assert + Assert.IsType(expected, actual); + + } + + [Fact] + public void DefaultDiscoverFactoryTest() + { + // arrange + var factory = new DiscoverProtocolUtilsFactory(); + var expected = typeof(DiscoverProtocolUtils); + + // act + var actual = factory.Create("test"); + + // assert + Assert.IsNotType(expected, actual); + + } + } +} diff --git a/test/Mocoding.Ofx.Tests/Client/OfxClientTests.cs b/test/Mocoding.Ofx.Tests/Client/OfxClientTests.cs new file mode 100644 index 0000000..0d2fb52 --- /dev/null +++ b/test/Mocoding.Ofx.Tests/Client/OfxClientTests.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Mocoding.Ofx.Client; +using Mocoding.Ofx.Client.Args; +using Mocoding.Ofx.Client.Defaults; +using Mocoding.Ofx.Client.Interfaces; +using Mocoding.Ofx.Models; +using Mocoding.Ofx.Serializers; +using NSubstitute; +using Xunit; + +namespace Mocoding.Ofx.Tests.Client +{ + public class OfxClientTests + { + readonly Uri ApiUrl = new Uri("http://localhost:5000/api/ofx"); + + [Fact] + public async Task AccountListTest() + { + var expectedResponse = + EmbeddedResourceReader.ReadResponseAsString("accountList.sgml"); + + var options = new OfxClientOptions(ApiUrl, "HAN", "5959", "testUserAccount", "testUserPassword"); + var utilsMock = Substitute.For(); + + utilsMock.Requests.Returns(new DefaultOfxRequestLocator()); + utilsMock.PostRequest(Arg.Any(), Arg.Any()).Returns(Task.FromResult(expectedResponse)); + + + utilsMock.GenerateTransactionId().Returns("0000000000"); + utilsMock.GetCurrentDateTime().Returns("20150127131257"); + utilsMock.GetClientUid(Arg.Is(val => val == "testUserAccount")).Returns("SomeGuidHere"); + + var client = new OfxClient(options, utilsMock, new OfxSgmlSerializer()); + var result = await client.GetAccounts(); + var account = result; + + Assert.NotEqual(ImmutableArray.Empty, account); + Assert.Equal(2, account.Length); + } + + [Fact] + public async Task CreditCardTransactionsListTest() + { + var expectedResponse = + EmbeddedResourceReader.ReadResponseAsString("creditCardTransactions.sgml"); + + var options = new OfxClientOptions(ApiUrl, "HAN", "5959", "testUserAccount", "testUserPassword"); + var utilsMock = Substitute.For(); + + utilsMock.Requests.Returns(new DefaultOfxRequestLocator()); + utilsMock.PostRequest(Arg.Any(), Arg.Any()).Returns(Task.FromResult(expectedResponse)); + + utilsMock.GenerateTransactionId().Returns("0000000000"); + utilsMock.GetCurrentDateTime().Returns("XXXXXXXXXXXXXX"); + + var client = new OfxClient(options, utilsMock, new OfxSgmlSerializer()); + var statement = await client.GetStatement(new CreditCardStatementArgs() { AccountNumber = "XXXXXXXXXXXX3158" }); + + Assert.NotNull(statement); + Assert.Equal(2, statement.Transactions.Length); + } + + [Fact] + public async Task BankTransactionsListTest() + { + var expectedResponse = + EmbeddedResourceReader.ReadResponseAsString("bankTransactions.sgml"); + + var options = new OfxClientOptions(ApiUrl, "HAN", "5959", "testUserAccount", "testUserPassword"); + var utilsMock = Substitute.For(); + + utilsMock.Requests.Returns(new DefaultOfxRequestLocator()); + utilsMock.PostRequest(Arg.Any(), Arg.Any()).Returns(Task.FromResult(expectedResponse)); + + utilsMock.GenerateTransactionId().Returns("0000000000"); + utilsMock.GetCurrentDateTime().Returns("XXXXXXXXXXXXXX"); + + var client = new OfxClient(options, utilsMock, new OfxSgmlSerializer()); + var statement = await client.GetStatement(new BankStatementArgs() + { + AccountNumber = "YYYYYYYY3158", RoutingNumber = "XXXXXXXXX", Type = AccountTypeEnum.Checking + + }); + + Assert.NotNull(statement); + Assert.Equal(2, statement.Transactions.Length); + } + + //[Fact] + //public async Task FailedRequestTest() + //{ + // var expectedResponse = + // EmbeddedResourceReader.ReadResponseAsString("error.sgml"); + + // var options = new OfxClientOptions(ApiUrl, "HAN", "5959", "testUserAccount", "testUserPassword"); + // var transportMock = Substitute.For(); + // var utilsMock = Substitute.For(); + + // utilsMock.GenerateTransactionId().Returns("0000000000"); + // utilsMock.GetCurrentDateTime().Returns("XXXXXXXXXXXXXX"); + + // transportMock.PostRequest(Arg.Any(), Arg.Any()).Returns(Task.FromResult(expectedResponse)); + + // var client = new OfxClient(options, transportMock, utilsMock); + // var ex = await Assert.ThrowsAsync(() => client.GetAccounts()); + // Assert.Equal("An incorrect username/password combination has been entered. Please try again.", ex.Message); + //} + + //[Fact] + //public async Task ErrorRequestTest() + //{ + // var options = new OfxClientOptions(ApiUrl, "HAN", "5959", "testUserAccount", "testUserPassword"); + // var transportMock = Substitute.For(); + // var utilsMock = Substitute.For(); + + // utilsMock.GenerateTransactionId().Returns("0000000000"); + // utilsMock.GetCurrentDateTime().Returns("XXXXXXXXXXXXXX"); + + // transportMock.PostRequest(Arg.Any(), Arg.Any()).Returns(Task.FromResult(string.Empty)); + + // var client = new OfxClient(options, transportMock, utilsMock); + // var ex = await Assert.ThrowsAsync(() => client.GetAccounts()); + + // Assert.Equal(" element is not present in the response body", ex.Message); + //} + + //[Fact] + //public async Task ParseCreditCardStatementTest() + //{ + // var statement = + // EmbeddedResourceReader.ReadResponseAsString("creditCardStatement", OfxVersionEnum.Version1x); + + // var transactions = OfxClient.ParseCreditCardStatement(statement); + + // Assert.NotNull(transactions); + // Assert.Equal(1, transactions.Items.Length); + // Assert.Equal(-20.43m, transactions.CurrentBalance); + //} + + } +} diff --git a/test/Mocoding.Ofx.Tests/Client/OfxRequestsTests.cs b/test/Mocoding.Ofx.Tests/Client/OfxRequestsTests.cs new file mode 100644 index 0000000..94a0189 --- /dev/null +++ b/test/Mocoding.Ofx.Tests/Client/OfxRequestsTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Mocoding.Ofx.Client; +using Mocoding.Ofx.Client.Args; +using Mocoding.Ofx.Client.Defaults; +using Mocoding.Ofx.Client.Interfaces; +using Mocoding.Ofx.Models; +using Mocoding.Ofx.Serializers; +using Mocoding.Ofx.Tests; +using NSubstitute; +using Xunit; + +namespace Mocoding.Ofx.Tests.Client +{ + public class OfxRequestsTests + { + readonly Uri ApiUrl = new Uri("http://localhost:5000/api/ofx"); + + [Fact] + public void CreditCardStatementTest() + { + var expectedRequest = + EmbeddedResourceReader.ReadRequestAsString("creditCardTransactions.sgml"); + + var options = new OfxClientOptions(ApiUrl, "HAN", "5959", "testUserAccount", "testUserPassword"); + var utilsMock = Substitute.For(); + + utilsMock.Requests.Returns(new DefaultOfxRequestLocator()); + + utilsMock.GenerateTransactionId().Returns("0000000000"); + utilsMock.GetCurrentDateTime().Returns("XXXXXXXXXXXXXX"); + utilsMock.DateFormat.Returns(OfxProtocolUtils.DateTimeFormat); + utilsMock.GetClientUid(Arg.Is("testUserAccount")).Returns("XXXXXXXXX"); + + var client = new OfxClient(options, utilsMock, new OfxSgmlSerializer()); + var startDate = new DateTime(2019, 1, 1); + var endDate = new DateTime(2019, 3, 1); + + + var statement = client.PrepareCreditCardStatementOfxRequest(new CreditCardStatementArgs() + { + AccountNumber = "XXXXXXXXXXXX3158", + StartDate = startDate, + EndDate = endDate + }); + + Assert.Equal(expectedRequest, statement); + } + + [Fact] + public void BankStatementTest() + { + var expectedRequest = + EmbeddedResourceReader.ReadRequestAsString("bankTransactions.sgml"); + + var options = new OfxClientOptions(ApiUrl, "HAN", "5959", "testUserAccount", "testUserPassword"); + var utilsMock = Substitute.For(); + + utilsMock.Requests.Returns(new DefaultOfxRequestLocator()); + + utilsMock.GenerateTransactionId().Returns("0000000000"); + utilsMock.GetCurrentDateTime().Returns("XXXXXXXXXXXXXX"); + utilsMock.DateFormat.Returns(OfxProtocolUtils.DateTimeFormat); + utilsMock.GetClientUid(Arg.Is("testUserAccount")).Returns("XXXXXXXXX"); + + var client = new OfxClient(options, utilsMock, new OfxSgmlSerializer()); + var startDate = new DateTime(2019, 1, 1); + var endDate = new DateTime(2019, 3, 1); + + + var statement = client.PrepareBankStatementOfxRequest(new BankStatementArgs() + { + AccountNumber = "YYYYYYYY3158", + RoutingNumber = "XXXXXXXXX", + Type = AccountTypeEnum.Checking, + StartDate = startDate, + EndDate = endDate + }); + + Assert.Equal(expectedRequest, statement); + } + } +} diff --git a/test/Mocoding.Ofx.Tests/Client/ProtocolUtilsTests.cs b/test/Mocoding.Ofx.Tests/Client/ProtocolUtilsTests.cs new file mode 100644 index 0000000..11692ba --- /dev/null +++ b/test/Mocoding.Ofx.Tests/Client/ProtocolUtilsTests.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using Mocoding.Ofx.Client; +using Mocoding.Ofx.Client.Defaults; +using Mocoding.Ofx.Client.Discover; +using Mocoding.Ofx.Client.Interfaces; +using Xunit; + +namespace Mocoding.Ofx.Tests.Client +{ + public class ProtocolUtilsTests + { + private IProtocolUtils CreateUtils() => new OfxProtocolUtils(new DefaultOfxRequestLocator()); + + [Fact] + public void NotNullTests() + { + // arrange + var utils = CreateUtils(); + + // act & assert + Assert.NotNull(utils.GetCurrentDateTime()); + Assert.NotNull(utils.GenerateTransactionId()); + Assert.NotNull(utils.Requests); + } + + [Fact] + public void ClientIdTest() + { + // arrange + var utils = CreateUtils(); + var expectedLength = 32; + + // act + var actualLength = utils.GetClientUid("test").Length; + + // assert + Assert.Equal(expectedLength, actualLength); + } + } +} diff --git a/test/Mocoding.Ofx.Tests/EmbeddedResourceReader.cs b/test/Mocoding.Ofx.Tests/EmbeddedResourceReader.cs index 5fd8012..4c27bd2 100644 --- a/test/Mocoding.Ofx.Tests/EmbeddedResourceReader.cs +++ b/test/Mocoding.Ofx.Tests/EmbeddedResourceReader.cs @@ -16,18 +16,23 @@ public static string ReadAsString(string resourceName) var resourceStream = assembly.GetManifestResourceStream($"Mocoding.Ofx.Tests.TestData.{resourceName}"); using (var reader = new StreamReader(resourceStream, Encoding.UTF8)) - return reader.ReadToEnd(); + return reader.ReadToEnd().Replace("\r\n", "\n"); } - public static string ReadRequestAsString(string resourceName) - { - return ReadAsString($"Request.{resourceName}"); - } + public static string ReadRequestAsString(string resourceName) => + ReadAsString($"Request.{resourceName}"); - public static string ReadResponseAsString(string resourceName) - { - return ReadAsString($"Response.{resourceName}"); - } + public static string ReadRequestAsString(string resourceName, OfxVersionEnum version) => + ReadRequestAsString($"{resourceName}.{GetExetension(version)}"); + + public static string ReadResponseAsString(string resourceName) => + ReadAsString($"Response.{resourceName}"); + + public static string ReadResponseAsString(string resourceName, OfxVersionEnum version) => + ReadResponseAsString($"{resourceName}.{GetExetension(version)}"); + + public static string GetExetension(OfxVersionEnum version) => + version == OfxVersionEnum.Version1x ? "sgml" : "xml"; } } diff --git a/test/Mocoding.Ofx.Tests/Mocoding.Ofx.Tests.csproj b/test/Mocoding.Ofx.Tests/Mocoding.Ofx.Tests.csproj index 60e520f..7f20623 100644 --- a/test/Mocoding.Ofx.Tests/Mocoding.Ofx.Tests.csproj +++ b/test/Mocoding.Ofx.Tests/Mocoding.Ofx.Tests.csproj @@ -1,24 +1,42 @@  - Mocoding.Ofx.Tests Class Library - Mocoding - netcoreapp2.0 + MOCODING LLC, Dennis Miasoutov + netcoreapp3.0 - + + + + + + + + + + - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/test/Mocoding.Ofx.Tests/OfxSerializerTests.cs b/test/Mocoding.Ofx.Tests/OfxSerializerTests.cs index de4c4f5..df888c1 100644 --- a/test/Mocoding.Ofx.Tests/OfxSerializerTests.cs +++ b/test/Mocoding.Ofx.Tests/OfxSerializerTests.cs @@ -3,36 +3,43 @@ using System.Linq; using System.Runtime.InteropServices.ComTypes; using System.Threading.Tasks; +using Mocoding.Ofx.Interfaces; using Xunit; namespace Mocoding.Ofx.Tests { public class OfxSerializerTests { - private OfxSerializer _serializer; + private readonly IOfxSerializerFactory _factory; public OfxSerializerTests() { - _serializer = new OfxSerializer(); - } + _factory = new DefaultOfxSerializerFactory(); + } - [Fact] - public void AccountListRequestTest() + [Theory] + [InlineData(OfxVersionEnum.Version1x)] + [InlineData(OfxVersionEnum.Version2x)] + public void AccountListRequestTest(OfxVersionEnum version) { - var response = EmbeddedResourceReader.ReadRequestAsString("accountList.sgml"); - var ofx = _serializer.Deserialize(response); - var serizlied = _serializer.Serialize(ofx); + var response = EmbeddedResourceReader.ReadRequestAsString("accountList", version); + var serializer = _factory.Create(version); + var ofx = serializer.Deserialize(response); + var serialized = serializer.Serialize(ofx); - Assert.Equal(response, serizlied); + Assert.Equal(response, serialized); } - [Fact] - public void AccountListResponseTest() + [Theory] + [InlineData(OfxVersionEnum.Version1x)] + [InlineData(OfxVersionEnum.Version2x)] + public void AccountListResponseTest(OfxVersionEnum version) { - var response = EmbeddedResourceReader.ReadResponseAsString("accountList.sgml"); - var ofx = _serializer.Deserialize(response); - var serizlied = _serializer.Serialize(ofx); + var response = EmbeddedResourceReader.ReadResponseAsString("accountList", version); + var serializer = _factory.Create(version); + var ofx = serializer.Deserialize(response); + var serialized = serializer.Serialize(ofx); - Assert.Equal(response, serizlied); - } + Assert.Equal(response, serialized); + } } } diff --git a/test/Mocoding.Ofx.Tests/OfxStatetementParserTests.cs b/test/Mocoding.Ofx.Tests/OfxStatetementParserTests.cs new file mode 100644 index 0000000..6fefd84 --- /dev/null +++ b/test/Mocoding.Ofx.Tests/OfxStatetementParserTests.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Mocoding.Ofx.Tests +{ + public class OfxStatetementParserTests + { + [Fact] + public void CreditCardAccount() + { + // Arrange + var creditCardStatement = + EmbeddedResourceReader.ReadResponseAsString("creditCardTransactions", OfxVersionEnum.Version1x); + var serializer = new DefaultOfxSerializerFactory().Create(OfxVersionEnum.Version1x); + var ofxPayload = serializer.Deserialize(creditCardStatement); + + // Act + var statement = OfxStatementParser.Parse(ofxPayload); + + // Assert + Assert.Equal("XXXXXXXXXXXX3158", statement.AccountNumber); + Assert.Equal("USD", statement.Currency); + Assert.Equal(0.00m, statement.AvailableBalance); + Assert.Equal(0.00m, statement.LedgerBalance); + Assert.Equal(2, statement.Transactions.Length); + } + + [Fact] + public void BankAccount() + { + // Arrange + var creditCardStatement = + EmbeddedResourceReader.ReadResponseAsString("bankTransactions", OfxVersionEnum.Version1x); + var serializer = new DefaultOfxSerializerFactory().Create(OfxVersionEnum.Version1x); + var ofxPayload = serializer.Deserialize(creditCardStatement); + + // Act + var statement = OfxStatementParser.Parse(ofxPayload); + + // Assert + Assert.Equal("0000000000003158", statement.AccountNumber); + Assert.Equal("USD", statement.Currency); + Assert.Equal(1322.42m, statement.AvailableBalance); + Assert.Equal(1327.42m, statement.LedgerBalance); + Assert.Equal(2, statement.Transactions.Length); + } + } + +} diff --git a/test/Mocoding.Ofx.Tests/SgmlSerizlierTests.cs b/test/Mocoding.Ofx.Tests/SgmlSerizlierTests.cs deleted file mode 100644 index 84b69de..0000000 --- a/test/Mocoding.Ofx.Tests/SgmlSerizlierTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Xunit; - -namespace Mocoding.Ofx.Tests -{ - public class Data - { - public string User { get; set; } - public int Age { get; set; } - } - - public class SgmlSerizlierTests - { - [Fact] - public void SerializeTest() - { - var serializer = new SgmlSerializer(); - - var data = new Data(){ - User = "Some", - Age = 13 - }; - - var expected = "Some13"; - var actual = serializer.Serialize(data); - - Assert.Equal(expected, actual); - } - - [Fact] - public void DeserializeTest() - { - var serializer = new SgmlSerializer(); - - var actual = serializer.Deserialize("Some13"); - - Assert.Equal("Some", actual.User); - Assert.Equal(13, actual.Age); - } - } -} diff --git a/test/Mocoding.Ofx.Tests/TestData/Request/accountList.sgml b/test/Mocoding.Ofx.Tests/TestData/Request/accountList.sgml index 6046ea6..38409c0 100644 --- a/test/Mocoding.Ofx.Tests/TestData/Request/accountList.sgml +++ b/test/Mocoding.Ofx.Tests/TestData/Request/accountList.sgml @@ -1,6 +1,6 @@ OFXHEADER:100 DATA:OFXSGML -VERSION:103 +VERSION:102 SECURITY:NONE ENCODING:USASCII CHARSET:1252 diff --git a/test/Mocoding.Ofx.Tests/TestData/Request/accountList.xml b/test/Mocoding.Ofx.Tests/TestData/Request/accountList.xml new file mode 100644 index 0000000..63801f2 --- /dev/null +++ b/test/Mocoding.Ofx.Tests/TestData/Request/accountList.xml @@ -0,0 +1,3 @@ + + +20150127131257testUserPassword1234ENGHAN5959OTHER9999SomeGuidHere000000000019900101000000 \ No newline at end of file diff --git a/test/Mocoding.Ofx.Tests/TestData/Request/bankTransactions.sgml b/test/Mocoding.Ofx.Tests/TestData/Request/bankTransactions.sgml index c6736c9..3fb0fe3 100644 --- a/test/Mocoding.Ofx.Tests/TestData/Request/bankTransactions.sgml +++ b/test/Mocoding.Ofx.Tests/TestData/Request/bankTransactions.sgml @@ -1,6 +1,6 @@ OFXHEADER:100 DATA:OFXSGML -VERSION:103 +VERSION:102 SECURITY:NONE ENCODING:USASCII CHARSET:1252 @@ -8,4 +8,4 @@ COMPRESSION:NONE OLDFILEUID:NONE NEWFILEUID:NONE -XXXXXXXXXXXXXXtestUserAccounttestUserPasswordENGHAN5959QWIN25000000000000XXXXXXXXXYYYYYYYY1924CHECKINGY \ No newline at end of file +XXXXXXXXXXXXXXtestUserAccounttestUserPasswordENGHAN5959QWIN2500XXXXXXXXX0000000000XXXXXXXXXYYYYYYYY3158CHECKING2019010100000020190301000000Y \ No newline at end of file diff --git a/test/Mocoding.Ofx.Tests/TestData/Request/creditCardTransactions.sgml b/test/Mocoding.Ofx.Tests/TestData/Request/creditCardTransactions.sgml index 30304a0..faa016c 100644 --- a/test/Mocoding.Ofx.Tests/TestData/Request/creditCardTransactions.sgml +++ b/test/Mocoding.Ofx.Tests/TestData/Request/creditCardTransactions.sgml @@ -1,6 +1,6 @@ OFXHEADER:100 DATA:OFXSGML -VERSION:103 +VERSION:102 SECURITY:NONE ENCODING:USASCII CHARSET:1252 @@ -8,4 +8,4 @@ COMPRESSION:NONE OLDFILEUID:NONE NEWFILEUID:NONE -XXXXXXXXXXXXXXtestUserAccounttestUserPasswordENGHAN5959QWIN25000000000000XXXXXXXXXXXX3158Y \ No newline at end of file +XXXXXXXXXXXXXXtestUserAccounttestUserPasswordENGHAN5959QWIN2500XXXXXXXXX0000000000XXXXXXXXXXXX31582019010100000020190301000000Y \ No newline at end of file diff --git a/test/Mocoding.Ofx.Tests/TestData/Response/accountList.sgml b/test/Mocoding.Ofx.Tests/TestData/Response/accountList.sgml index d9732b3..e0c5f6c 100644 --- a/test/Mocoding.Ofx.Tests/TestData/Response/accountList.sgml +++ b/test/Mocoding.Ofx.Tests/TestData/Response/accountList.sgml @@ -1,6 +1,6 @@ OFXHEADER:100 DATA:OFXSGML -VERSION:103 +VERSION:102 SECURITY:NONE ENCODING:USASCII CHARSET:1252 @@ -8,4 +8,4 @@ COMPRESSION:NONE OLDFILEUID:NONE NEWFILEUID:NONE -0INFOSUCCESS20150103023446ENG20131012020000HAN59590101010101010101010101014099273390INFO20150103023447BankAmericard0000000000003158YYNACTIVEBOFA CORE CHECKING0110001380000000000001924CHECKINGYYYACTIVE010101010000000001924CHECKINGACTIVE \ No newline at end of file +0INFOSUCCESS20150103023446ENG20131012020000HAN59590101010101010101010101014099273390INFO20150103023447BankAmericard0000000000003158YYNACTIVEBOFA CORE CHECKING0110001380000000000003158CHECKINGYYYACTIVE010101010000000003158CHECKINGACTIVE \ No newline at end of file diff --git a/test/Mocoding.Ofx.Tests/TestData/Response/accountList.xml b/test/Mocoding.Ofx.Tests/TestData/Response/accountList.xml new file mode 100644 index 0000000..cf54c62 --- /dev/null +++ b/test/Mocoding.Ofx.Tests/TestData/Response/accountList.xml @@ -0,0 +1,3 @@ + + +0INFOSUCCESS20150103023446ENG20131012020000HAN59590101010101010101010101014099273390INFO20150103023447BankAmericard0000000000003158YYNACTIVEBOFA CORE CHECKING0110001380000000000003158CHECKINGYYYACTIVE010101010000000003158CHECKINGACTIVE \ No newline at end of file diff --git a/test/Mocoding.Ofx.Tests/TestData/Response/bankTransactions.sgml b/test/Mocoding.Ofx.Tests/TestData/Response/bankTransactions.sgml index acc5fb3..0b84d15 100644 --- a/test/Mocoding.Ofx.Tests/TestData/Response/bankTransactions.sgml +++ b/test/Mocoding.Ofx.Tests/TestData/Response/bankTransactions.sgml @@ -1,6 +1,6 @@ OFXHEADER:100 DATA:OFXSGML -VERSION:103 +VERSION:102 SECURITY:NONE ENCODING:USASCII CHARSET:1252 @@ -8,4 +8,4 @@ COMPRESSION:NONE OLDFILEUID:NONE NEWFILEUID:NONE -0INFOSUCCESS20150209014309ENG20131012020000HAN59590101010101010101010101018296313240INFOUSD0110001380000000000001924CHECKING2014011119000020150206190000DEBIT20150206190000-9.9500094320206-9.95015020613276.42ONLINE BANKING VIA QUICKENCREDIT201502051900002598.75000902335012598.75015020515221.67DIRECTPAY+1327.4220150208204311+1322.4220150208204311 \ No newline at end of file +0INFOSUCCESS20150209014309ENG20131012020000HAN59590101010101010101010101018296313240INFOUSD0110001380000000000003158CHECKING2014011119000020150206190000DEBIT20150206190000-9.9500094320206-9.95015020613276.42ONLINE BANKING VIA QUICKENCREDIT201502051900002598.75000902335012598.75015020515221.67DIRECTPAY+1327.4220150208204311+1322.4220150208204311 \ No newline at end of file diff --git a/test/Mocoding.Ofx.Tests/TestData/Response/creditCardTransactions.sgml b/test/Mocoding.Ofx.Tests/TestData/Response/creditCardTransactions.sgml index 00f2726..f539fe8 100644 --- a/test/Mocoding.Ofx.Tests/TestData/Response/creditCardTransactions.sgml +++ b/test/Mocoding.Ofx.Tests/TestData/Response/creditCardTransactions.sgml @@ -1,6 +1,6 @@ OFXHEADER:100 DATA:OFXSGML -VERSION:103 +VERSION:102 SECURITY:NONE ENCODING:USASCII CHARSET:1252 diff --git a/test/Mocoding.Ofx.Tests/TestData/Response/error.sgml b/test/Mocoding.Ofx.Tests/TestData/Response/error.sgml index 2a633a0..e2d9637 100644 --- a/test/Mocoding.Ofx.Tests/TestData/Response/error.sgml +++ b/test/Mocoding.Ofx.Tests/TestData/Response/error.sgml @@ -1,6 +1,6 @@ OFXHEADER:100 DATA:OFXSGML -VERSION:103 +VERSION:102 SECURITY:NONE ENCODING:USASCII CHARSET:1252