diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8494bc2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,161 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results + +[Dd]ebug/ +[Rr]elease/ +x64/ +build/ +[Bb]in/ +[Oo]bj/ + +# Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets +!packages/*/build/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.log +*.scc + +*.exe + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.Publish.xml + +# NuGet Packages Directory +## TODO: If you have NuGet Package Restore enabled, uncomment the next line +packages/ + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +#sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.[Pp]ublish.xml +*.pfx +*.publishsettings + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + + +#LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml + +# ========================= +# Windows detritus +# ========================= + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Mac desktop service store files +.DS_Store diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..081f197 --- /dev/null +++ b/nuget.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/pack.cmd b/pack.cmd new file mode 100644 index 0000000..1defcf6 --- /dev/null +++ b/pack.cmd @@ -0,0 +1,4 @@ +source\.nuget\nuget.exe pack source\OFXSharper\OFXSharper.csproj -Prop Configuration=Release +echo # update Project Url: https://github.com/Habanerio/OFXSharper +echo # update License Url: http://opensource.org/licenses/MIT +pause \ No newline at end of file diff --git a/source/.editorconfig b/source/.editorconfig new file mode 100644 index 0000000..2c73003 --- /dev/null +++ b/source/.editorconfig @@ -0,0 +1,273 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# All files +[*] +charset = utf-8 +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_readonly_field = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Expression-level preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:suggestion + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false:silent +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:suggestion +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:warning + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = no_change +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case +csharp_style_namespace_declarations = file_scoped:suggestion +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion + +# Constants are UPPERCASE +dotnet_naming_rule.constants_should_be_upper_case.severity = suggestion +dotnet_naming_rule.constants_should_be_upper_case.symbols = constants +dotnet_naming_rule.constants_should_be_upper_case.style = constant_style + +dotnet_naming_symbols.constants.applicable_kinds = field, local +dotnet_naming_symbols.constants.required_modifiers = const + +dotnet_naming_style.constant_style.capitalization = all_upper + +[*.cs] +dotnet_diagnostic.MA0053.severity = suggestion + +# Report public classes without inheritors (default: false) +MA0053.public_class_should_be_sealed = true + +# Report class without inheritors even if there is virtual members (default: false) +MA0053.class_with_virtual_member_shoud_be_sealed = true diff --git a/source/.nuget/NuGet.Config b/source/.nuget/NuGet.Config new file mode 100644 index 0000000..67f8ea0 --- /dev/null +++ b/source/.nuget/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/source/.nuget/NuGet.targets b/source/.nuget/NuGet.targets new file mode 100644 index 0000000..593e44a --- /dev/null +++ b/source/.nuget/NuGet.targets @@ -0,0 +1,151 @@ + + + + $(MSBuildProjectDirectory)\..\ + + + false + + + false + + + true + + + true + + + + + + + + + + + $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) + + + + + $(SolutionDir).nuget + + + + packages.$(MSBuildProjectName.Replace(' ', '_')).config + + + + + + $(PackagesProjectConfig) + + + + + packages.config + + + + + + + $(NuGetToolsPath)\NuGet.exe + @(PackageSource) + + "$(NuGetExePath)" + mono --runtime=v4.0.30319 $(NuGetExePath) + + $(TargetDir.Trim('\\')) + + -RequireConsent + -NonInteractive + + "$(SolutionDir) " + "$(SolutionDir)" + + + $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) + $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols + + + + RestorePackages; + $(BuildDependsOn); + + + + + $(BuildDependsOn); + BuildPackage; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/.unitTestGeneratorConfig b/source/.unitTestGeneratorConfig new file mode 100644 index 0000000..084b10c --- /dev/null +++ b/source/.unitTestGeneratorConfig @@ -0,0 +1,76 @@ +[GenerationOptions] +AutoDetectFrameworkTypes=False +FrameworkType=XUnit +MockingFrameworkType=MoqAutoMock +AllowGenerationWithoutTargetProject=True +TestProjectNaming={0}.Tests +TestFileNaming={0}Tests +TestTypeNaming={0}Tests +UseFluentAssertions=False +UseShouldly=False +UseAutoFixture=True +UseAutoFixtureForMocking=True +UseFieldForAutoFixture=True +EmitUsingsOutsideNamespace=True +PartialGenerationAllowed=True +EmitTestsForInternals=False +AutomaticallyConfigureMocks=True +EmitSubclassForProtectedMethods=True +ArrangeComment=Arrange +ActComment=Act +AssertComment=Assert +UserInterfaceMode=WhenTargetNotFound +FallbackTargetFinding=TypeInCorrectNamespace +PrefixFieldReferencesWithThis=False +EmitXmlDocumentation=False +UseMockBehaviorStrict=False +CreateTargetAssets=True +TestTypeBaseClass= +TestTypeBaseClassNamespace= +GenerateFileScopedNamespaces=True +PlaceSystemUsingDirectivesFirst=True +SkipInternalTypesOnMultipleGeneration=False +DefaultFailureMessage=Create or modify test +EmitMultilinePocoInitializers=True +UseFieldsForConstructorParameterTests=True + +[NamingOptions] +CanConstructNamingPattern=Can_Construct +CannotConstructWithNullNamingPattern=Cannot_Construct_WithNull_{parameterName} +CannotConstructWithInvalidNamingPattern=Cannot_Construct_WithInvalid_{parameterName} +CanInitializeNamingPattern=Can_Initialize +CannotInitializeWithNullNamingPattern=Cannot_Initialize_WithNull_{memberName} +CannotInitializeWithInvalidNamingPattern=Cannot_Initialize_WithInvalid_{memberName} +CanGetNamingPattern=CanGet_{memberName} +CanSetAndGetNamingPattern=CanSet_And_Get_{memberName} +CanSetNamingPattern=CanSet_{memberName} +IsInitializedCorrectlyNamingPattern={memberName}_IsInitialized_Correctly +ImplementsIEnumerableNamingPattern=ImplementsI_Enumerable_{typeParameters} +ImplementsIComparableNamingPattern=ImplementsI_Comparable_{typeParameters} +ImplementsIEquatableNamingPattern=ImplementsI_Equatable_{typeParameters} +CanCallNamingPattern=CanCall_{memberName} +PerformsMappingNamingPattern={memberName}_PerformsMapping +CannotCallWithNullNamingPattern=CannotCall_{memberName}_WithNull_{parameterName} +CannotCallWithInvalidNamingPattern=CannotCall_{memberName}_WithInvalid_{parameterName} +CanCallOperatorNamingPattern=CanCall_{memberName}_Operator +CannotCallOperatorWithNullNamingPattern=CannotCall_{memberName}_Operator_WithNull_{parameterName} +TargetFieldName=_testClass +DependencyFieldName=_{parameterName:camel} +MockDependencyFieldName= +AutoFixtureFieldName=_fixture +ForceAsyncSuffix=False + +[StrategyOptions] +ConstructorChecksAreEnabled=True +ConstructorParameterChecksAreEnabled=True +InitializerChecksAreEnabled=True +InitializerPropertyChecksAreEnabled=True +MethodCallChecksAreEnabled=True +MappingMethodChecksAreEnabled=True +MethodParameterChecksAreEnabled=True +IndexerChecksAreEnabled=True +PropertyChecksAreEnabled=True +InitializedPropertyChecksAreEnabled=True +OperatorChecksAreEnabled=True +OperatorParameterChecksAreEnabled=True +InterfaceImplementationChecksAreEnabled=True diff --git a/source/OFXSharper.Tests/CanParser.cs b/source/OFXSharper.Tests/CanParser.cs new file mode 100644 index 0000000..7e133c3 --- /dev/null +++ b/source/OFXSharper.Tests/CanParser.cs @@ -0,0 +1,117 @@ +using System; +using System.IO; +using Habanerio.OFXSharper.Types; +using Xunit; + +namespace Habanerio.OFXSharper.Tests +{ + public class CanParser + { + [Fact] + public void CanParserBankTransactions() + { + var parser = new OFXDocumentParser(); + var ofxDocument = parser.Import(new FileStream(@"bankTransactions.sgml", FileMode.Open)); + + Assert.NotNull(ofxDocument); + Assert.NotNull(ofxDocument.Account); + + Assert.Equal(AccountType.BANK, ofxDocument.AccType); + + Assert.Equal("0000000000003158", ofxDocument.Account.AccountID); + Assert.Equal("3158", ofxDocument.Account.AccountKey); + Assert.Equal(AccountType.BANK, ofxDocument.Account.AccountType); + Assert.Equal(BankAccountType.CHECKING, ofxDocument.Account.BankAccountType); + Assert.Equal("011000138", ofxDocument.Account.BankID); + Assert.Equal("003", ofxDocument.Account.BranchID); + + Assert.NotNull(ofxDocument.Balance); + Assert.Equal(1327.42M, ofxDocument.Balance.AvailableBalance); + Assert.Equal(new DateTime(2024, 02, 08, 0, 0, 0 + , DateTimeKind.Unspecified), ofxDocument.Balance.AvailableBalanceDate); + + Assert.Equal(1327.42M, ofxDocument.Balance.LedgerBalance); + Assert.Equal(new DateTime(2024, 02, 08, 0, 0, 0 + , DateTimeKind.Unspecified), ofxDocument.Balance.LedgerBalanceDate); + + Assert.Equal("USD", ofxDocument.Currency); + + Assert.NotNull(ofxDocument.SignOn); + Assert.Equal(new DateTime(2024, 02, 09, 0, 0, 0 + , DateTimeKind.Unspecified), ofxDocument.SignOn.DTServer); + Assert.Equal("", ofxDocument.SignOn.IntuBid); + Assert.Equal("ENG", ofxDocument.SignOn.Language); + Assert.Equal(0, ofxDocument.SignOn.StatusCode); + Assert.Equal("INFO", ofxDocument.SignOn.StatusSeverity); + + Assert.Equal(new DateTime(2024, 01, 11, 0, 0, 0 + , DateTimeKind.Unspecified), ofxDocument.StatementStart); + Assert.Equal(new DateTime(2024, 02, 06, 0, 0, 0 + , DateTimeKind.Unspecified), ofxDocument.StatementEnd); + } + + [Fact] + public void CanParserCreditCardTransactions() + { + var parser = new OFXDocumentParser(); + var ofxDocument = parser.Import(new FileStream(@"creditCardTransactions.sgml", FileMode.Open)); + + Assert.NotNull(ofxDocument); + Assert.NotNull(ofxDocument.Account); + + Assert.Equal(AccountType.CC, ofxDocument.AccType); + + Assert.Equal("XXXXXXXXXXXX3158", ofxDocument.Account.AccountID); + Assert.Equal(string.Empty, ofxDocument.Account.AccountKey); + Assert.Equal(AccountType.CC, ofxDocument.Account.AccountType); + Assert.Equal(BankAccountType.NA, ofxDocument.Account.BankAccountType); + Assert.Null(ofxDocument.Account.BankID); + Assert.Null(ofxDocument.Account.BranchID); + + Assert.NotNull(ofxDocument.Balance); + Assert.Equal(12000.00M, ofxDocument.Balance.AvailableBalance); + Assert.Equal(new DateTime(2024, 01, 04, 0, 0, 0, + DateTimeKind.Unspecified), ofxDocument.Balance.AvailableBalanceDate); + + Assert.Equal(345m, ofxDocument.Balance.LedgerBalance); + Assert.Equal(new DateTime(2024, 01, 04, 0, 0, 0, + DateTimeKind.Unspecified), ofxDocument.Balance.LedgerBalanceDate); + + Assert.Equal("USD", ofxDocument.Currency); + + Assert.NotNull(ofxDocument.SignOn); + Assert.Equal(new DateTime(2024, 01, 05, 0, 0, 0, + DateTimeKind.Unspecified), ofxDocument.SignOn.DTServer); + Assert.Equal("", ofxDocument.SignOn.IntuBid); + Assert.Equal("ENG", ofxDocument.SignOn.Language); + Assert.Equal(0, ofxDocument.SignOn.StatusCode); + Assert.Equal("INFO", ofxDocument.SignOn.StatusSeverity); + + // TODO: Fix this + //Assert.Equal(new DateTime(2024, 01, 04, 0, 0, 0, + // DateTimeKind.Unspecified), ofxDocument.StatementStart); + //Assert.Equal(new DateTime(2024, 02, 06, 0, 0, 0, + // DateTimeKind.Unspecified), ofxDocument.StatementEnd); + } + + [Fact] + public void CanParserItau() + { + var parser = new OFXDocumentParser(); + var ofxDocument = parser.Import(new FileStream(@"itau.ofx", FileMode.Open)); + + Assert.NotNull(ofxDocument); + Assert.NotNull(ofxDocument.Account); + } + + [Fact] + public void CanParserSantander() + { + var parser = new OFXDocumentParser(); + var ofxDocument = parser.Import(new FileStream(@"santander.ofx", FileMode.Open)); + + Assert.NotNull(ofxDocument); + Assert.NotNull(ofxDocument.Account); + } + } +} diff --git a/source/OFXSharper.Tests/OFXSharper.Tests.csproj b/source/OFXSharper.Tests/OFXSharper.Tests.csproj new file mode 100644 index 0000000..4ec9bb0 --- /dev/null +++ b/source/OFXSharper.Tests/OFXSharper.Tests.csproj @@ -0,0 +1,70 @@ + + + + net6.0 + OFXSharper + Habanerio.OFXSharper.Tests + Habanerio.OFXSharper.Tests + + false + + + + + + + + + + + + + + PreserveNewest + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/source/OFXSharper.Tests/accountList.sgml b/source/OFXSharper.Tests/accountList.sgml new file mode 100644 index 0000000..396289e --- /dev/null +++ b/source/OFXSharper.Tests/accountList.sgml @@ -0,0 +1,75 @@ +OFXHEADER:100 +DATA:OFXSGML +VERSION:103 +SECURITY:NONE +ENCODING:USASCII +CHARSET:1252 +COMPRESSION:NONE +OLDFILEUID:NONE +NEWFILEUID:NONE + + + + + + 0 + INFO + SUCCESS + + 20150103023446 + ENG + 20131012020000 + + HAN< + FID>5959 + + 010101010101010101010101 + + + + + 409927339 + + 0 + INFO + + + 20150103023447 + + BankAmericard + + + 0000000000003158 + + Y + Y + N + ACTIVE + + + + BOFA CORE CHECKING + + + 011000138 + 0000000000003158 + CHECKING + + Y + Y + Y + ACTIVE + + + + 010101010 + 000000003158 + CHECKING + + ACTIVE + + + + + + \ No newline at end of file diff --git a/source/OFXSharper.Tests/accountList.xml b/source/OFXSharper.Tests/accountList.xml new file mode 100644 index 0000000..cf54c62 --- /dev/null +++ b/source/OFXSharper.Tests/accountList.xml @@ -0,0 +1,3 @@ + + +0INFOSUCCESS20150103023446ENG20131012020000HAN59590101010101010101010101014099273390INFO20150103023447BankAmericard0000000000003158YYNACTIVEBOFA CORE CHECKING0110001380000000000003158CHECKINGYYYACTIVE010101010000000003158CHECKINGACTIVE \ No newline at end of file diff --git a/source/OFXSharper.Tests/bankTransactions.sgml b/source/OFXSharper.Tests/bankTransactions.sgml new file mode 100644 index 0000000..8dfe75f --- /dev/null +++ b/source/OFXSharper.Tests/bankTransactions.sgml @@ -0,0 +1,74 @@ +OFXHEADER:100 +DATA:OFXSGML +VERSION:103 +SECURITY:NONE +ENCODING:USASCII +CHARSET:1252 +COMPRESSION:NONE +OLDFILEUID:NONE +NEWFILEUID:NONE + + + + + + 0 + INFO + SUCCESS + + 20240209014309 + ENG + 20231012020000 + + HAN + 5959 + + 010101010101010101010101 + + + + + 829631324 + + 0 + INFO + + + USD + + 011000138 + 003 + 0000000000003158 + 3158 + CHECKING + + + 20240111190000 + 20240206190000 + + DEBIT + 20240206190000 + -9.95 + 00094320206-9.95015020613276.42 + ONLINE BANKING VIA QUICKEN + + + CREDIT + 20240205190000 + 2598.75 + 000902335012598.75015020515221.67 + DIRECTPAY + + + + +1327.42 + 20240208204311 + + + +1327.42 + 20240208204311 + + + + + \ No newline at end of file diff --git a/source/OFXSharper.Tests/creditCardTransactions.sgml b/source/OFXSharper.Tests/creditCardTransactions.sgml new file mode 100644 index 0000000..e9e5588 --- /dev/null +++ b/source/OFXSharper.Tests/creditCardTransactions.sgml @@ -0,0 +1,71 @@ +OFXHEADER:100 +DATA:OFXSGML +VERSION:103 +SECURITY:NONE +ENCODING:USASCII +CHARSET:1252 +COMPRESSION:NONE +OLDFILEUID:NONE +NEWFILEUID:NONE + + + + + + 0 + INFO + SUCCESS + + 20240105043851 + ENG + 20131012020000 + + HAN + 5959 + + 010101010101010101010101 + + + + + 257568705 + + 0 + INFO + + + USD + + XXXXXXXXXXXX3158 + + + 20230104190000 + 20240103190000 + + DEBIT + 20240103190000 + -9.62 + B27K7JG8PBN5VNGD + REBECCAS CAFE INC + CAMBRIDGE MA + + + CREDIT + 20231230190000 + 835.58 + B27JSWPVPWSSVPD8 + PAYMENT - THANK YOU + + + + 345.00 + 20240104233852 + + + 12000.00 + 20240104233852 + + + + + \ No newline at end of file diff --git a/source/OFXSharper.Tests/itau.ofx b/source/OFXSharper.Tests/itau.ofx new file mode 100644 index 0000000..61b285b --- /dev/null +++ b/source/OFXSharper.Tests/itau.ofx @@ -0,0 +1,71 @@ +OFXHEADER:100 +DATA:OFXSGML +VERSION:102 +SECURITY:NONE +ENCODING:USASCII +CHARSET:1252 +COMPRESSION:NONE +OLDFILEUID:NONE +NEWFILEUID:NONE + + + + + +0 +INFO + +20140304100000[-03:EST] +POR + + + + +1001 + +0 +INFO + + +BRL + +0341 +9999999999 +CHECKING + + +20131205100000[-03:EST] +20140228100000[-03:EST] + +DEBIT +20131209100000[-03:EST] +-666.66 +20131209001 +20131209001 +RSHOP + + +CREDIT +20131209100000[-03:EST] +99.99 +20131209002 +20131209002 +REND PAGO APLIC AUT MAIS + + +DEBIT +20131210100000[-03:EST] +-77.77 +20131210001 +20131210001 +SISDEB + + + +-9999.99 +20140304100000[-03:EST] + + + + + diff --git a/source/OFXSharper.Tests/santander.ofx b/source/OFXSharper.Tests/santander.ofx new file mode 100644 index 0000000..071eab8 --- /dev/null +++ b/source/OFXSharper.Tests/santander.ofx @@ -0,0 +1,78 @@ +OFXHEADER:100 +DATA:OFXSGML +VERSION:102 +SECURITY:NONE +ENCODING:USASCII +CHARSET:1252 +COMPRESSION:NONE +OLDFILEUID:NONE +NEWFILEUID:NONE + + + + + + 0 + INFO + + 20140203182251[-3:GMT] + ENG + + SANTANDER + SANTANDER + + + + + + 1 + + 0 + INFO + + + BRL + + 033 + 9999999999999 + CHECKING + + + 20140203182251[-3:GMT] + 20140203182251[-3:GMT] + + OTHER + 20131107000000[-3:GMT] + -11.11 + 00510367 + 00510367 + 0 + DEBITO VISA ELECTRON BRASIL + + + OTHER + 20131107000000[-3:GMT] + -222,22 + 00000105 + 00000105 + 0 + COMPENSACAO INTERNA DE CHEQUE + + + OTHER + 20131108000000[-3:GMT] + -333.33 + 00330867 + 00330867 + 0 + DEBITO VISA ELECTRON BRASIL + + + + 9999.99 + 20140203182251[-3:GMT] + + + + + \ No newline at end of file diff --git a/source/OFXSharper.sln b/source/OFXSharper.sln new file mode 100644 index 0000000..3bf270d --- /dev/null +++ b/source/OFXSharper.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.34825.169 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OFXSharper", "OFXSharper\OFXSharper.csproj", "{C89D7C48-DE17-4EB3-BC65-CCA3D8A375F9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OFXSharper.Tests", "OFXSharper.Tests\OFXSharper.Tests.csproj", "{72CCB407-4984-45B3-8326-5263B8CB4AAD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C89D7C48-DE17-4EB3-BC65-CCA3D8A375F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C89D7C48-DE17-4EB3-BC65-CCA3D8A375F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C89D7C48-DE17-4EB3-BC65-CCA3D8A375F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C89D7C48-DE17-4EB3-BC65-CCA3D8A375F9}.Release|Any CPU.Build.0 = Release|Any CPU + {72CCB407-4984-45B3-8326-5263B8CB4AAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72CCB407-4984-45B3-8326-5263B8CB4AAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72CCB407-4984-45B3-8326-5263B8CB4AAD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72CCB407-4984-45B3-8326-5263B8CB4AAD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {64987655-AB4A-4038-A051-47C71E6C4113} + EndGlobalSection +EndGlobal diff --git a/source/OFXSharper/Exceptions/OFXException.cs b/source/OFXSharper/Exceptions/OFXException.cs new file mode 100644 index 0000000..cb9deb3 --- /dev/null +++ b/source/OFXSharper/Exceptions/OFXException.cs @@ -0,0 +1,28 @@ +using System; +using System.Runtime.Serialization; + +namespace Habanerio.OFXSharper.Exceptions +{ + [Serializable] + public class OFXException : Exception + { + public OFXException() + { + } + + public OFXException(string message) : base(message) + { + } + + public OFXException(string message, Exception inner) : base(message, inner) + { + } + + protected OFXException( + SerializationInfo info, + StreamingContext context) + : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/source/OFXSharper/Exceptions/OFXParseException.cs b/source/OFXSharper/Exceptions/OFXParseException.cs new file mode 100644 index 0000000..3165543 --- /dev/null +++ b/source/OFXSharper/Exceptions/OFXParseException.cs @@ -0,0 +1,28 @@ +using System; +using System.Runtime.Serialization; + +namespace Habanerio.OFXSharper.Exceptions +{ + [Serializable] + public class OFXParseException : OFXException + { + public OFXParseException() + { + } + + public OFXParseException(string message) : base(message) + { + } + + public OFXParseException(string message, Exception inner) : base(message, inner) + { + } + + protected OFXParseException( + SerializationInfo info, + StreamingContext context) + : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/source/OFXSharper/ModelDiagram.cd b/source/OFXSharper/ModelDiagram.cd new file mode 100644 index 0000000..c8217a6 --- /dev/null +++ b/source/OFXSharper/ModelDiagram.cd @@ -0,0 +1,55 @@ + + + + + + AACBAAAAAAAAAAAEAAgAAAABCAAAAAAAAAAAAAAAAEI= + OFXDocument.cs + + + + + + + + + + + + + + + AAAAAAAAAAAAAAAAAAIIAAAAAAAAAAEAAAAAAAAAAAQ= + Balance.cs + + + + + + BACAAIAAIQAAAAAAAgAAIAAAAAAAAAAAACAAQAAAAAA= + Account.cs + + + + + + AAAAAAAACAAAAAAAAAAAAABAAAAABAAAIAQAAAAAAAA= + SignOn.cs + + + + + + AAAAAgAAAACAAQAAAAAEAgSgCBABFAUDgAAAAAAAIAA= + Transaction.cs + + + + + + AAAAAAIAAAAAAAAQAAAAAAEgAAAAAgAAAAAAAAAAAAA= + AccountType.cs + + + + \ No newline at end of file diff --git a/source/OFXSharper/Models/Account.cs b/source/OFXSharper/Models/Account.cs new file mode 100644 index 0000000..69d128f --- /dev/null +++ b/source/OFXSharper/Models/Account.cs @@ -0,0 +1,95 @@ +using System.Xml; +using Habanerio.OFXSharper.Exceptions; +using Habanerio.OFXSharper.Types; + +namespace Habanerio.OFXSharper.Models +{ + public class Account + { + public string AccountID { get; set; } + public string AccountKey { get; set; } + public AccountType AccountType { get; set; } + + #region Bank Only + + private BankAccountType _BankAccountType = BankAccountType.NA; + + public string BankID { get; set; } + + public string BranchID { get; set; } + + + public BankAccountType BankAccountType + { + get + { + if (AccountType == AccountType.BANK) + return _BankAccountType; + + return BankAccountType.NA; + } + set + { + _BankAccountType = AccountType == AccountType.BANK ? value : BankAccountType.NA; + } + } + + #endregion + + public Account(XmlNode node, AccountType type) + { + AccountType = type; + + AccountID = node.GetValue("//ACCTID"); + AccountKey = node.GetValue("//ACCTKEY"); + + switch (AccountType) + { + case AccountType.BANK: + InitializeBank(node); + break; + case AccountType.AP: + InitializeAP(node); + break; + case AccountType.AR: + InitializeAR(node); + break; + default: + break; + } + } + + /// + /// Initializes information specific to bank + /// + private void InitializeBank(XmlNode node) + { + BankID = node.GetValue("//BANKID"); + BranchID = node.GetValue("//BRANCHID"); + + //Get Bank Account Type from XML + string bankAccountType = node.GetValue("//ACCTTYPE"); + + //Check that it has been set + if (string.IsNullOrEmpty(bankAccountType)) + throw new OFXParseException("Bank Account type unknown"); + + //Set bank account enum + _BankAccountType = bankAccountType.GetBankAccountType(); + } + + #region Account types not supported + + private static void InitializeAP(XmlNode node) + { + throw new OFXParseException("AP Account type not supported"); + } + + private static void InitializeAR(XmlNode node) + { + throw new OFXParseException("AR Account type not supported"); + } + + #endregion + } +} \ No newline at end of file diff --git a/source/OFXSharper/Models/Balance.cs b/source/OFXSharper/Models/Balance.cs new file mode 100644 index 0000000..b433377 --- /dev/null +++ b/source/OFXSharper/Models/Balance.cs @@ -0,0 +1,64 @@ +using System; +using System.Globalization; +using System.Xml; +using Habanerio.OFXSharper.Exceptions; + +namespace Habanerio.OFXSharper.Models +{ + public class Balance + { + public decimal LedgerBalance { get; set; } + + public DateTime? LedgerBalanceDate { get; set; } + + public decimal AvailableBalance { get; set; } + + public DateTime? AvailableBalanceDate { get; set; } = null; + + public Balance(XmlNode ledgerNode, XmlNode availableNode) + { + var tempLedgerBalance = ledgerNode.GetValue("//LEDGERBAL//BALAMT"); + + if (!string.IsNullOrEmpty(tempLedgerBalance)) + { + // ***** Forced Invariant Culture. + // If you don't force it, it will use the computer's default (defined in windows control panel, regional settings) + // So, if the number format of the computer in use it's different from OFX standard (i suppose the english/invariant), + // the next line of could crash or (worse) the number would be wrongly interpreted. + // For example, my computer has a brazilian regional setting, with "." as thousand separator and "," as + // decimal separator, so the value "10.99" (ten 'dollars' (or whatever currency) and ninety-nine cents) would be interpreted as "1099" + // (one thousand and ninety-nine dollars - the "." would be ignored) + LedgerBalance = Convert.ToDecimal(tempLedgerBalance, CultureInfo.InvariantCulture); + } + else + { + throw new OFXParseException("Ledger balance has not been set"); + } + + // ***** OFX files from my bank don't have the 'availableNode' node, so i manage a null situation + if (availableNode == null) + { + AvailableBalance = 0; + } + else + { + // *** Need to prepend `//AVAILBAL` to the xpath to get the correct value. Or else it returns the value in `//LEDGERBAL//BALAMT` + var tempAvailableBalance = availableNode.GetValue("//AVAILBAL//BALAMT"); + + if (!string.IsNullOrEmpty(tempAvailableBalance)) + { + // ***** Forced Invariant Culture. (same comment as above) + AvailableBalance = Convert.ToDecimal(tempAvailableBalance, CultureInfo.InvariantCulture); + } + else + { + throw new OFXParseException("Available balance has not been set"); + } + + AvailableBalanceDate = availableNode.GetValue("//DTASOF").ToDate(); + } + + LedgerBalanceDate = ledgerNode.GetValue("//DTASOF")?.ToDate(); + } + } +} \ No newline at end of file diff --git a/source/OFXSharper/Models/SignOn.cs b/source/OFXSharper/Models/SignOn.cs new file mode 100644 index 0000000..396c451 --- /dev/null +++ b/source/OFXSharper/Models/SignOn.cs @@ -0,0 +1,27 @@ +using System; +using System.Xml; + +namespace Habanerio.OFXSharper.Models +{ + public class SignOn + { + public string StatusSeverity { get; set; } + + public DateTime? DTServer { get; set; } + + public int StatusCode { get; set; } + + public string Language { get; set; } + + public string IntuBid { get; set; } + + public SignOn(XmlNode node) + { + StatusCode = Convert.ToInt32(node.GetValue("//CODE")); + StatusSeverity = node.GetValue("//SEVERITY"); + DTServer = node.GetValue("//DTSERVER")?.ToDate(); + Language = node.GetValue("//LANGUAGE"); + IntuBid = node.GetValue("//INTU.BID"); + } + } +} \ No newline at end of file diff --git a/source/OFXSharper/Models/Transaction.cs b/source/OFXSharper/Models/Transaction.cs new file mode 100644 index 0000000..7a97083 --- /dev/null +++ b/source/OFXSharper/Models/Transaction.cs @@ -0,0 +1,139 @@ +using System; +using System.Globalization; +using System.Xml; +using Habanerio.OFXSharper.Exceptions; +using Habanerio.OFXSharper.Types; + +namespace Habanerio.OFXSharper.Models +{ + public class Transaction + { + public TransactionType TransType { get; set; } + + public DateTime? Date { get; set; } + + public decimal Amount { get; set; } + + public string TransactionID { get; set; } + + public string Name { get; set; } + + public DateTime? TransactionInitializationDate { get; set; } + + public DateTime? FundAvaliabilityDate { get; set; } + + public string Memo { get; set; } + + public string IncorrectTransactionID { get; set; } + + public TransactionCorrectionType TransactionCorrectionAction { get; set; } + + public string ServerTransactionID { get; set; } + + public string CheckNum { get; set; } + + public string ReferenceNumber { get; set; } + + public string Sic { get; set; } + + public string PayeeID { get; set; } + + public Account TransactionSenderAccount { get; set; } + + public string Currency { get; set; } + + public Transaction() + { + } + + public Transaction(XmlNode node, string currency) + { + TransType = GetTransactionType(node.GetValue(".//TRNTYPE")); + Date = node.GetValue(".//DTPOSTED").ToDate(); + TransactionInitializationDate = node.GetValue(".//DTUSER").ToDate(); + FundAvaliabilityDate = node.GetValue(".//DTAVAIL").ToDate(); + + try + { + Amount = Convert.ToDecimal(node.GetValue(".//TRNAMT"), CultureInfo.InvariantCulture); + } + catch (Exception ex) + { + throw new OFXParseException("Transaction Amount unknown", ex); + } + + try + { + TransactionID = node.GetValue(".//FITID"); + } + catch (Exception ex) + { + throw new OFXParseException("Transaction ID unknown", ex); + } + + IncorrectTransactionID = node.GetValue(".//CORRECTFITID"); + + + //If Transaction Correction Action exists, populate + var tempCorrectionAction = node.GetValue(".//CORRECTACTION"); + + TransactionCorrectionAction = !string.IsNullOrEmpty(tempCorrectionAction) + ? GetTransactionCorrectionType(tempCorrectionAction) + : TransactionCorrectionType.NA; + + ServerTransactionID = node.GetValue(".//SRVRTID"); + CheckNum = node.GetValue(".//CHECKNUM"); + ReferenceNumber = node.GetValue(".//REFNUM"); + Sic = node.GetValue(".//SIC"); + PayeeID = node.GetValue(".//PAYEEID"); + Name = node.GetValue(".//NAME"); + Memo = node.GetValue(".//MEMO"); + + //If different currency to CURDEF, populate currency + if (NodeExists(node, ".//CURRENCY")) + Currency = node.GetValue(".//CURRENCY"); + else if (NodeExists(node, ".//ORIGCURRENCY")) + Currency = node.GetValue(".//ORIGCURRENCY"); + //If currency not different, set to CURDEF + else + Currency = currency; + + //If senders bank/credit card details available, add + if (NodeExists(node, ".//BANKACCTTO")) + TransactionSenderAccount = new Account(node.SelectSingleNode(".//BANKACCTTO"), AccountType.BANK); + else if (NodeExists(node, ".//CCACCTTO")) + TransactionSenderAccount = new Account(node.SelectSingleNode(".//CCACCTTO"), AccountType.CC); + } + + /// + /// Returns TransactionType from string version + /// + /// string version of transaction type + /// Enum version of given transaction type string + private static TransactionType GetTransactionType(string transactionType) + { + return (TransactionType)Enum.Parse(typeof(TransactionType), transactionType); + } + + /// + /// Returns TransactionCorrectionType from string version + /// + /// string version of Transaction Correction Type + /// Enum version of given TransactionCorrectionType string + private static TransactionCorrectionType GetTransactionCorrectionType(string transactionCorrectionType) + { + return (TransactionCorrectionType)Enum.Parse(typeof(TransactionCorrectionType), transactionCorrectionType); + } + + /// + /// Checks if a node exists + /// + /// Node to search in + /// XPath to node you want to see if exists + /// + private static bool NodeExists(XmlNode node, string xpath) + { + return node.SelectSingleNode(xpath) != null; + } + } +} \ No newline at end of file diff --git a/source/OFXSharper/OFXDocument.cs b/source/OFXSharper/OFXDocument.cs new file mode 100644 index 0000000..091826d --- /dev/null +++ b/source/OFXSharper/OFXDocument.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using Habanerio.OFXSharper.Models; +using Habanerio.OFXSharper.Types; + +namespace Habanerio.OFXSharper +{ + public class OFXDocument + { + public DateTime? StatementStart { get; set; } + + public DateTime? StatementEnd { get; set; } + + public AccountType AccType { get; set; } + + public string Currency { get; set; } + + public SignOn SignOn { get; set; } + + public Account Account { get; set; } + + public Balance Balance { get; set; } + + public List Transactions { get; set; } + } +} \ No newline at end of file diff --git a/source/OFXSharper/OFXDocumentParser.cs b/source/OFXSharper/OFXDocumentParser.cs new file mode 100644 index 0000000..9e0bcf8 --- /dev/null +++ b/source/OFXSharper/OFXDocumentParser.cs @@ -0,0 +1,316 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Xml; +using Habanerio.OFXSharper.Exceptions; +using Habanerio.OFXSharper.Models; +using Habanerio.OFXSharper.Types; +using Sgml; + + +namespace Habanerio.OFXSharper +{ + public class OFXDocumentParser + { + public string Version { get; private set; } = "102"; + + public OFXDocument Import(FileStream stream) + { + using (var reader = new StreamReader(stream, Encoding.Default)) + { + return Import(reader.ReadToEnd()); + } + } + + public OFXDocument Import(string ofx) + { + return ParseOfxDocument(ofx); + } + + private OFXDocument ParseOfxDocument(string ofxString) + { + //If OFX file in SGML format, convert to XML + if (!IsXmlVersion(ofxString)) + { + ofxString = SGMLToXML(ofxString); + } + + return Parse(ofxString); + } + + private OFXDocument Parse(string ofxString) + { + var ofx = new OFXDocument { AccType = GetAccountType(ofxString) }; + + //Load into xml document + var doc = new XmlDocument(); + doc.Load(new StringReader(ofxString)); + + var currencyNode = doc.SelectSingleNode(GetXPath(ofx.AccType, OFXSection.CURRENCY)); + + if (currencyNode != null) + { + ofx.Currency = currencyNode.FirstChild.Value.Trim(); + } + else + { + throw new OFXParseException("Currency not found"); + } + + //Get sign on node from OFX file + var signOnNode = doc.SelectSingleNode(Resources.SignOn); + + //If exists, populate signon obj, else throw parse error + if (signOnNode != null) + { + ofx.SignOn = new SignOn(signOnNode); + } + else + { + throw new OFXParseException("Sign On information not found"); + } + + //Get Account information for ofx doc + var accountNode = doc.SelectSingleNode(GetXPath(ofx.AccType, OFXSection.ACCOUNTINFO)); + + //If account info present, populate account object + if (accountNode != null) + { + ofx.Account = new Account(accountNode, ofx.AccType); + } + else + { + throw new OFXParseException("Account information not found"); + } + + //Get list of transactions + ImportTransactions(ofx, doc); + + //Get balance info from ofx doc + var ledgerNode = doc.SelectSingleNode(GetXPath(ofx.AccType, OFXSection.BALANCE) + "/LEDGERBAL"); + var availableNode = doc.SelectSingleNode(GetXPath(ofx.AccType, OFXSection.BALANCE) + "/AVAILBAL"); + + //If balance info present, populate balance object + // ***** OFX files from my bank don't have the 'availableNode' node, so i manage a 'null' situation + if (ledgerNode != null) // && availableNode != null + { + ofx.Balance = new Balance(ledgerNode, availableNode); + } + else + { + throw new OFXParseException("Balance information not found"); + } + + return ofx; + } + + + /// + /// Returns the correct xpath to specified section for given account type + /// + /// Account type + /// Section of OFX document, e.g. Transaction Section + /// Thrown in account type not supported + private static string GetXPath(AccountType type, OFXSection section) + { + string xpath, accountInfo; + + switch (type) + { + case AccountType.BANK: + xpath = Resources.BankAccount; + accountInfo = "/BANKACCTFROM"; + break; + case AccountType.CC: + xpath = Resources.CCAccount; + accountInfo = "/CCACCTFROM"; + break; + default: + throw new OFXException("Account Type not supported. Account type " + type); + } + + switch (section) + { + case OFXSection.ACCOUNTINFO: + return xpath + accountInfo; + case OFXSection.BALANCE: + return xpath; + case OFXSection.TRANSACTIONS: + return xpath + "/BANKTRANLIST"; + case OFXSection.SIGNON: + return Resources.SignOn; + case OFXSection.CURRENCY: + return xpath + "/CURDEF"; + default: + throw new OFXException("Unknown section found when retrieving XPath. Section " + section); + } + } + + /// + /// Returns list of all transactions in OFX document + /// + /// OFX Document + /// XML document + /// List of transactions found in OFX document + private static void ImportTransactions(OFXDocument ofxDocument, XmlDocument doc) + { + var xpath = GetXPath(ofxDocument.AccType, OFXSection.TRANSACTIONS); + + ofxDocument.StatementStart = doc.GetValue(xpath + "//DTSTART")?.ToDate(); + ofxDocument.StatementEnd = doc.GetValue(xpath + "//DTEND")?.ToDate(); + + var transactionNodes = doc.SelectNodes(xpath + "//STMTTRN"); + + if (transactionNodes == null) + return; + + ofxDocument.Transactions = new List(); + + foreach (XmlNode node in transactionNodes) + ofxDocument.Transactions.Add(new Transaction(node, ofxDocument.Currency)); + } + + /// + /// Checks account type of supplied file + /// + /// OFX file want to check + /// Account type for account supplied in ofx file + private static AccountType GetAccountType(string file) + { + if (file.IndexOf("", StringComparison.InvariantCulture) != -1) + return AccountType.CC; + + if (file.IndexOf("", StringComparison.InvariantCulture) != -1) + return AccountType.BANK; + + throw new OFXException("Unsupported Account Type"); + } + + /// + /// Check if OFX file is in SGML or XML format + /// + /// + /// + private static bool IsXmlVersion(string file) + { + return file.IndexOf("OFXHEADER:100", StringComparison.InvariantCulture) == -1; + } + + /// + /// Converts SGML to XML + /// + /// OFX File (SGML Format) + /// OFX File in XML format + private string SGMLToXML(string file) + { + var sgmlReader = new SgmlReader(); + + //Initialize SGML reader + var fileReader = new StringReader(ParseHeader(file)); + + sgmlReader.DocType = "OFX"; + sgmlReader.InputStream = fileReader; + + // Newer implementation + var ofxDoc = new XmlDocument(); + ofxDoc.PreserveWhitespace = false; + ofxDoc.XmlResolver = null; + ofxDoc.Load(sgmlReader); + + return ofxDoc.OuterXml; + + // Original implementation + //var sw = new StringWriter(); + //var xml = new XmlTextWriter(sw); + + ////write output of sgml reader to xml text writer + //while (!sgmlReader.EOF) + // xml.WriteNode(sgmlReader, true); + + ////close xml text writer + //xml.Flush(); + //xml.Close(); + + //var temp = sw.ToString().Replace("\t", "").TrimStart().Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); + + //return string.Join("", temp); + } + + /// + /// Checks that the file is supported by checking the header. Removes the header. + /// + /// OFX file + /// File, without the header + private string ParseHeader(string file) + { + //Select header of file and split into array + //End of header worked out by finding first instance of '<' + //Array split based of new line & carrige return + var header = file.Substring(0, file.IndexOf('<')) + .Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); + + //Check that no errors in header + CheckHeader(header); + + //Remove header + return file.Substring(file.IndexOf('<')).Trim(); + } + + /// + /// Checks that all the elements in the header are supported + /// + /// Header of OFX file in array + private void CheckHeader(string[] header) + { + if (header[0] == "OFXHEADER:100DATA:OFXSGMLVERSION:102SECURITY:NONEENCODING:USASCIICHARSET:1252COMPRESSION:NONEOLDFILEUID:NONENEWFILEUID:NONE")//non delimited header + return; + + if (header[0] != "OFXHEADER:100") + throw new OFXParseException("Incorrect header format"); + + if (header[1] != "DATA:OFXSGML") + throw new OFXParseException("Data type unsupported: " + header[1] + ". OFXSGML required"); + + if (header[2].Contains("VERSION")) + { + Version = header[2].Split(':')[1]; + } + + //if (header[2] != "VERSION:102") + // throw new OFXParseException("OFX version unsupported. " + header[2]); + + // Do we care if the SECURITY is not NONE? + //if (header[3] != "SECURITY:NONE") + // throw new OFXParseException("OFX security unsupported"); + + if (header[4] != "ENCODING:USASCII") + throw new OFXParseException("ASCII Format unsupported:" + header[4]); + + if (header[5] != "CHARSET:1252") + throw new OFXParseException("Charecter set unsupported:" + header[5]); + + if (header[6] != "COMPRESSION:NONE") + throw new OFXParseException("Compression unsupported"); + + if (header[7] != "OLDFILEUID:NONE") + throw new OFXParseException("OLDFILEUID incorrect"); + } + + #region Nested type: OFXSection + + /// + /// Section of OFX Document + /// + private enum OFXSection + { + SIGNON, + ACCOUNTINFO, + TRANSACTIONS, + BALANCE, + CURRENCY + } + + #endregion + } +} \ No newline at end of file diff --git a/source/OFXSharper/OFXHelperMethods.cs b/source/OFXSharper/OFXHelperMethods.cs new file mode 100644 index 0000000..82166d0 --- /dev/null +++ b/source/OFXSharper/OFXHelperMethods.cs @@ -0,0 +1,72 @@ +using System; +using System.Xml; +using Habanerio.OFXSharper.Exceptions; +using Habanerio.OFXSharper.Types; + +namespace Habanerio.OFXSharper +{ + public static class OFXHelperMethods + { + /// + /// Converts string representation of AccountInfo to enum AccountInfo + /// + /// representation of AccountInfo + /// AccountInfo + public static BankAccountType GetBankAccountType(this string bankAccountType) + { + try + { + return (BankAccountType)Enum.Parse(typeof(BankAccountType), bankAccountType, true); + } + catch (Exception) + { + + return BankAccountType.NA; + } + } + + /// + /// Flips date from YYYYMMDD to DDMMYYYY + /// + /// Date in YYYYMMDD format + /// Date in format DDMMYYYY + public static DateTime? ToDate(this string date) + { + try + { + if (date.Length < 8) + { + return null; + } + + var dd = int.Parse(date.Substring(6, 2)); + var mm = int.Parse(date.Substring(4, 2)); + var yyyy = int.Parse(date.Substring(0, 4)); + + return new DateTime(yyyy, mm, dd, 0, 0, 0, DateTimeKind.Unspecified); + } + catch + { + throw new OFXParseException("Unable to parse date"); + } + } + + /// + /// Returns value of specified node + /// + /// Node to look for specified node + /// XPath for node you want + /// + public static string GetValue(this XmlNode node, string xpath) + { + var tempNode = node.SelectSingleNode(xpath); + + if (tempNode?.FirstChild != null) + { + return tempNode.FirstChild.Value.Trim(); + } + + return string.Empty; + } + } +} \ No newline at end of file diff --git a/source/OFXSharper/OFXSharper.csproj b/source/OFXSharper/OFXSharper.csproj new file mode 100644 index 0000000..9e3eca5 --- /dev/null +++ b/source/OFXSharper/OFXSharper.csproj @@ -0,0 +1,48 @@ + + + netstandard2.0 + OFXSharper + Habanerio.OFXSharper + Habanerio.OFXSharper + + Habaner.io, jhollingworth + Habaner.io + .Net Standard version of James Hollingworth's popular OFXSharp Library (https://github.com/jhollingworth/OFXSharp) + + Habanerio.OFXSharper + + 1.0.0 + 1.0.0 + 1.0.0 + + + http://opensource.org/licenses/MIT + https://github.com/Habanerio/OFXSharper + + OFX, OFX Parser, OFXSharp + + true + + + + True + True + Resources.resx + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/source/OFXSharper/Properties/AssemblyInfo.cs b/source/OFXSharper/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..09a9486 --- /dev/null +++ b/source/OFXSharper/Properties/AssemblyInfo.cs @@ -0,0 +1,13 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4dc4da7c-0cb3-455d-98d7-7a2ad77069ff")] diff --git a/source/OFXSharper/Resources.Designer.cs b/source/OFXSharper/Resources.Designer.cs new file mode 100644 index 0000000..16991d1 --- /dev/null +++ b/source/OFXSharper/Resources.Designer.cs @@ -0,0 +1,117 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Habanerio.OFXSharper { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Habanerio.OFXSharper.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to OFX/BANKMSGSRSV1/STMTTRNRS/. + /// + internal static string BankAccount { + get { + return ResourceManager.GetString("BankAccount", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OFX/CREDITCARDMSGSRSV1/CCSTMTTRNRS/. + /// + internal static string CCAccount { + get { + return ResourceManager.GetString("CCAccount", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There are insufficient funds to pay your {item}. We have allocated what you have evenly between the {item} but there is no room in the budget for anything else, sorry.... + /// + internal static string InsufficentFunds { + get { + return ResourceManager.GetString("InsufficentFunds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There are no funds for your {item} sorry.... + /// + internal static string NoFunds { + get { + return ResourceManager.GetString("NoFunds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are spending beyond your means so we have had to use your savings to create this budget. Perhaps you need to review your spending.... + /// + internal static string NoMoney { + get { + return ResourceManager.GetString("NoMoney", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OFX/SIGNONMSGSRSV1/SONRS. + /// + internal static string SignOn { + get { + return ResourceManager.GetString("SignOn", resourceCulture); + } + } + } +} diff --git a/source/OFXSharper/Resources.resx b/source/OFXSharper/Resources.resx new file mode 100644 index 0000000..1f57462 --- /dev/null +++ b/source/OFXSharper/Resources.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + OFX/BANKMSGSRSV1/STMTTRNRS/ + + + OFX/CREDITCARDMSGSRSV1/CCSTMTTRNRS/ + + + There are insufficient funds to pay your {item}. We have allocated what you have evenly between the {item} but there is no room in the budget for anything else, sorry... + + + There are no funds for your {item} sorry... + + + You are spending beyond your means so we have had to use your savings to create this budget. Perhaps you need to review your spending... + + + OFX/SIGNONMSGSRSV1/SONRS + + diff --git a/source/OFXSharper/Types/AccountType.cs b/source/OFXSharper/Types/AccountType.cs new file mode 100644 index 0000000..cbe8bbe --- /dev/null +++ b/source/OFXSharper/Types/AccountType.cs @@ -0,0 +1,17 @@ +using System.ComponentModel; + +namespace Habanerio.OFXSharper.Types +{ + public enum AccountType + { + [Description("Bank Account")] + BANK, + [Description("Credit Card")] + CC, + [Description("Accounts Payable")] + AP, + [Description("Accounts Recievable")] + AR, + NA, + } +} \ No newline at end of file diff --git a/source/OFXSharper/Types/BankAccountType.cs b/source/OFXSharper/Types/BankAccountType.cs new file mode 100644 index 0000000..d94a633 --- /dev/null +++ b/source/OFXSharper/Types/BankAccountType.cs @@ -0,0 +1,19 @@ +using System.ComponentModel; + +namespace Habanerio.OFXSharper.Types +{ + public enum BankAccountType + { + [Description("Checking Account")] + CHECKING, + [Description("Savings Account")] + SAVINGS, + [Description("Money Market Account")] + MONEYMRKT, + [Description("Line of Credit")] + CREDITLINE, + NA, + [Description("Home Loan")] + HOMELOAN, + } +} \ No newline at end of file diff --git a/source/OFXSharper/Types/TransactionCorrectionType.cs b/source/OFXSharper/Types/TransactionCorrectionType.cs new file mode 100644 index 0000000..db9032a --- /dev/null +++ b/source/OFXSharper/Types/TransactionCorrectionType.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; + +namespace Habanerio.OFXSharper.Types +{ + public enum TransactionCorrectionType + { + [Description("No correction needed")] + NA, + [Description("Replace this transaction with one referenced by CORRECTFITID")] + REPLACE, + [Description("Delete transaction")] + DELETE, + } +} \ No newline at end of file diff --git a/source/OFXSharper/Types/TransactionType.cs b/source/OFXSharper/Types/TransactionType.cs new file mode 100644 index 0000000..572af37 --- /dev/null +++ b/source/OFXSharper/Types/TransactionType.cs @@ -0,0 +1,41 @@ +using System.ComponentModel; + +namespace Habanerio.OFXSharper.Types +{ + public enum TransactionType + { + [Description("Basic Credit")] + CREDIT, + [Description("Basic Debit")] + DEBIT, + [Description("Interest")] + INT, + [Description("Dividend")] + DIV, + [Description("Fee")] + FEE, + [Description("Service Charge")] + SRVCHG, + [Description("Deposit")] + DEP, + [Description("ATM transfer")] + ATM, + [Description("Point of Sale transfer")] + POS, + [Description("Transfer")] + XFER, + [Description("Check")] + CHECK, + [Description("Payment")] + PAYMENT, + [Description("Cash Withdrawl")] + CASH, + [Description("Direct Deposit")] + DIRECTDEP, + [Description("Merchant Initiated Debit")] + DIRECTDEBIT, + [Description("Repeating Payment")] + REPEATPMT, + OTHER, + } +}