ofx_kit 0.1.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +42 -82
- data/lib/generators/{ofx → ofx_kit}/eject_generator.rb +5 -3
- data/lib/ofx_kit/balance.rb +25 -0
- data/lib/ofx_kit/bank_account.rb +25 -0
- data/lib/ofx_kit/{domain/bank_statement.rb → bank_statement.rb} +1 -0
- data/lib/ofx_kit/base/account.rb +3 -0
- data/lib/ofx_kit/base/builder.rb +8 -4
- data/lib/ofx_kit/base/document.rb +16 -4
- data/lib/ofx_kit/base/entity.rb +15 -5
- data/lib/ofx_kit/base/statement.rb +19 -1
- data/lib/ofx_kit/configuration/core.rb +75 -28
- data/lib/ofx_kit/configuration/date_parser.rb +1 -1
- data/lib/ofx_kit/configuration/mapping_applicator.rb +5 -3
- data/lib/ofx_kit/configuration/section_proxy.rb +14 -5
- data/lib/ofx_kit/{domain/credit_card_account.rb → credit_card_account.rb} +1 -0
- data/lib/ofx_kit/{domain/credit_card_statement.rb → credit_card_statement.rb} +1 -0
- data/lib/ofx_kit/errors/configuration_error.rb +8 -0
- data/lib/ofx_kit/errors/encoding_error.rb +8 -0
- data/lib/ofx_kit/errors/error.rb +11 -0
- data/lib/ofx_kit/errors/invalid_body_error.rb +8 -0
- data/lib/ofx_kit/errors/invalid_header_error.rb +8 -0
- data/lib/ofx_kit/errors/multiple_statements_error.rb +8 -0
- data/lib/ofx_kit/errors/parse_error.rb +8 -0
- data/lib/ofx_kit/errors/unsupported_version_error.rb +20 -0
- data/lib/ofx_kit/parser.rb +81 -29
- data/lib/ofx_kit/tokenizer/base.rb +11 -6
- data/lib/ofx_kit/tokenizer/ofx1.rb +2 -2
- data/lib/ofx_kit/tokenizer/ofx2.rb +1 -1
- data/lib/ofx_kit/transaction.rb +45 -0
- data/lib/ofx_kit/transaction_collection.rb +126 -0
- data/lib/ofx_kit/version.rb +2 -1
- data/lib/ofx_kit.rb +43 -24
- metadata +36 -13
- data/lib/ofx_kit/configuration.rb +0 -6
- data/lib/ofx_kit/domain/balance.rb +0 -10
- data/lib/ofx_kit/domain/bank_account.rb +0 -8
- data/lib/ofx_kit/domain/transaction.rb +0 -13
- data/lib/ofx_kit/domain/transaction_collection.rb +0 -94
- data/lib/ofx_kit/errors.rb +0 -36
- /data/lib/generators/{ofx → ofx_kit}/templates/ofx_mappings.yml +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module OFX
|
|
4
4
|
class Configuration
|
|
5
|
+
##
|
|
5
6
|
# Proxy object returned by section accessors (e.g. +OFX.config.transaction+).
|
|
6
7
|
# Provides a fluent interface for adding individual user-layer field mappings.
|
|
7
8
|
class SectionProxy
|
|
@@ -11,14 +12,22 @@ module OFX
|
|
|
11
12
|
@xml_tag = xml_tag
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
##
|
|
16
|
+
# Maps +xml_key+ (the OFX XML element name, String) to a Ruby attribute name
|
|
17
|
+
# via the +to+ keyword (String or Symbol) for this section.
|
|
18
|
+
#
|
|
19
|
+
# Raises Errors::ConfigurationError if +xml_key+ is a core-protected field.
|
|
20
|
+
#
|
|
21
|
+
# === Example: Map a custom bank-specific field
|
|
22
|
+
#
|
|
23
|
+
# OFX.configure do |config|
|
|
24
|
+
# config.transaction.map 'MYFIELD', to: :my_attribute
|
|
25
|
+
# end
|
|
26
|
+
# OFX.new("statement.ofx").transactions.first.my_attribute #=> "custom value"
|
|
18
27
|
def map(xml_key, to:)
|
|
19
28
|
core_attr = @core_fields.dig(@xml_tag.to_s, xml_key.to_s)
|
|
20
29
|
if core_attr
|
|
21
|
-
raise OFX::ConfigurationError,
|
|
30
|
+
raise OFX::Errors::ConfigurationError,
|
|
22
31
|
"Cannot override core mapping '#{@xml_tag}.#{xml_key}' (reserved as '#{core_attr}')"
|
|
23
32
|
end
|
|
24
33
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OFX
|
|
4
|
+
# Namespace for all gem-specific exception classes.
|
|
5
|
+
# All errors inherit from Errors::Error, so callers can rescue the entire hierarchy
|
|
6
|
+
# with <tt>rescue OFX::Errors::Error</tt>.
|
|
7
|
+
module Errors
|
|
8
|
+
# Base error class for all OFX-related exceptions.
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OFX
|
|
4
|
+
module Errors
|
|
5
|
+
##
|
|
6
|
+
# Raised when the OFX version declared in the file is not supported.
|
|
7
|
+
class UnsupportedVersionError < Error
|
|
8
|
+
##
|
|
9
|
+
# The unsupported version string found in the file (String).
|
|
10
|
+
attr_reader :version
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# Creates a new error for the given unsupported +version+ string.
|
|
14
|
+
def initialize(version)
|
|
15
|
+
@version = version
|
|
16
|
+
super("Unsupported OFX version: #{version}")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/ofx_kit/parser.rb
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module OFX
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
4
|
+
##
|
|
5
|
+
# The object returned by OFX.new. Provides access to the parsed statements,
|
|
6
|
+
# accounts, transactions, and balances from an OFX file or IO object.
|
|
7
|
+
# Prefer the top-level OFX.new entry point over instantiating this class directly.
|
|
8
8
|
class Parser
|
|
9
|
-
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
##
|
|
10
|
+
# Parses the given OFX +resource+ and builds the statement graph.
|
|
11
|
+
# +resource+ is a file path (String) or IO object containing OFX data.
|
|
12
|
+
# If a +block+ is given with arity 1, it receives the parser instance;
|
|
13
|
+
# otherwise it is evaluated in the parser's context.
|
|
12
14
|
def initialize(resource, &block)
|
|
13
|
-
@filename
|
|
14
|
-
content
|
|
15
|
-
tokenizer
|
|
16
|
-
@document
|
|
15
|
+
@filename = extract_filename(resource)
|
|
16
|
+
content = read_resource(resource)
|
|
17
|
+
tokenizer = build_tokenizer(content).new(content)
|
|
18
|
+
@document = Base::Document.new(headers: tokenizer.headers, body: tokenizer.body)
|
|
17
19
|
@statements = Base::Builder.new(@document).statements
|
|
18
20
|
|
|
19
21
|
return unless block_given?
|
|
@@ -21,34 +23,69 @@ module OFX
|
|
|
21
23
|
block.arity == 1 ? block.call(self) : instance_eval(&block)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
#
|
|
26
|
+
##
|
|
27
|
+
# All statements in the file (Array of BankStatement or CreditCardStatement).
|
|
28
|
+
#
|
|
29
|
+
# === Example: Iterate statements in a multi-account file
|
|
30
|
+
#
|
|
31
|
+
# ofx = OFX.new("multi.ofx")
|
|
32
|
+
# ofx.statements.each do |stmt|
|
|
33
|
+
# puts "#{stmt.account.account_id}: #{stmt.transactions.length} transactions"
|
|
34
|
+
# end
|
|
35
|
+
attr_reader :statements
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# Original file path, if a path string was provided (String or +nil+).
|
|
39
|
+
attr_reader :filename
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# Returns the parsed OFX header fields (Hash).
|
|
29
43
|
def headers
|
|
30
44
|
@document.headers
|
|
31
45
|
end
|
|
32
46
|
|
|
33
|
-
|
|
34
|
-
#
|
|
35
|
-
#
|
|
47
|
+
##
|
|
48
|
+
# Returns the account for files containing a single statement
|
|
49
|
+
# (BankAccount, CreditCardAccount, or +nil+).
|
|
50
|
+
# Raises Errors::MultipleStatementsError if the file contains more than one statement.
|
|
51
|
+
#
|
|
52
|
+
# === Example
|
|
53
|
+
#
|
|
54
|
+
# ofx = OFX.new("statement.ofx")
|
|
55
|
+
# ofx.account.account_id #=> "123456789"
|
|
56
|
+
# ofx.account.currency #=> "USD"
|
|
57
|
+
# ofx.account.bank_id #=> "021000021" # BankAccount only
|
|
36
58
|
def account
|
|
37
59
|
if statements.length > 1
|
|
38
|
-
raise MultipleStatementsError, 'File contains multiple statements. Use `accounts` to get all accounts.'
|
|
60
|
+
raise Errors::MultipleStatementsError, 'File contains multiple statements. Use `accounts` to get all accounts.'
|
|
39
61
|
end
|
|
40
62
|
|
|
41
63
|
statements.first&.account
|
|
42
64
|
end
|
|
43
65
|
|
|
44
|
-
|
|
66
|
+
##
|
|
67
|
+
# Returns all accounts across all statements
|
|
68
|
+
# (Array of BankAccount or CreditCardAccount).
|
|
69
|
+
#
|
|
70
|
+
# === Example: Multi-statement file
|
|
71
|
+
#
|
|
72
|
+
# ofx = OFX.new("multi.ofx")
|
|
73
|
+
# ofx.accounts.map(&:account_id) #=> ["123456", "789012"]
|
|
45
74
|
def accounts
|
|
46
75
|
statements.map(&:account)
|
|
47
76
|
end
|
|
48
77
|
|
|
49
|
-
|
|
78
|
+
##
|
|
79
|
+
# Returns all transactions aggregated across all statements as a TransactionCollection.
|
|
50
80
|
# Emits a warning when the file contains multiple statements.
|
|
51
|
-
#
|
|
81
|
+
#
|
|
82
|
+
# === Example
|
|
83
|
+
#
|
|
84
|
+
# ofx = OFX.new("statement.ofx")
|
|
85
|
+
# ofx.transactions.length #=> 42
|
|
86
|
+
# ofx.transactions.credits.length #=> 10
|
|
87
|
+
# ofx.transactions.total_debits.format #=> "-$1,234.56"
|
|
88
|
+
# ofx.transactions.net.format #=> "$500.00"
|
|
52
89
|
def transactions
|
|
53
90
|
if statements.length > 1 && OFX.config.multi_statement_warnings?
|
|
54
91
|
warn "[OFX] `transactions` is aggregating across #{statements.length} statements. " \
|
|
@@ -59,18 +96,26 @@ module OFX
|
|
|
59
96
|
TransactionCollection.new(statements.flat_map { |s| s.transactions.to_a })
|
|
60
97
|
end
|
|
61
98
|
|
|
62
|
-
|
|
63
|
-
#
|
|
64
|
-
#
|
|
99
|
+
##
|
|
100
|
+
# Returns the balance for files containing a single statement (Balance or +nil+).
|
|
101
|
+
# Raises Errors::MultipleStatementsError if the file contains more than one statement.
|
|
102
|
+
#
|
|
103
|
+
# === Example
|
|
104
|
+
#
|
|
105
|
+
# ofx = OFX.new("statement.ofx")
|
|
106
|
+
# ofx.balance.amount.format #=> "$2,500.00"
|
|
107
|
+
# ofx.balance.amount_cents #=> 250000
|
|
108
|
+
# ofx.balance.posted_at #=> 2024-01-31 00:00:00 +0000
|
|
65
109
|
def balance
|
|
66
110
|
if statements.length > 1
|
|
67
|
-
raise MultipleStatementsError, 'File contains multiple statements. Use `balances` to get all balances.'
|
|
111
|
+
raise Errors::MultipleStatementsError, 'File contains multiple statements. Use `balances` to get all balances.'
|
|
68
112
|
end
|
|
69
113
|
|
|
70
114
|
statements.first&.balance
|
|
71
115
|
end
|
|
72
116
|
|
|
73
|
-
|
|
117
|
+
##
|
|
118
|
+
# Returns balances for each statement (Array of Balance or +nil+).
|
|
74
119
|
def balances
|
|
75
120
|
if statements.length > 1 && OFX.config.multi_statement_warnings?
|
|
76
121
|
warn "[OFX] `balances` is aggregating across #{statements.length} statements. " \
|
|
@@ -81,9 +126,16 @@ module OFX
|
|
|
81
126
|
statements.map(&:balance)
|
|
82
127
|
end
|
|
83
128
|
|
|
129
|
+
##
|
|
84
130
|
# Returns a structured summary of all statements, including transaction counts,
|
|
85
131
|
# credit/debit totals (in cents), and closing balance.
|
|
86
|
-
#
|
|
132
|
+
#
|
|
133
|
+
# Returns a Hash with keys:
|
|
134
|
+
# - +:headers+ — compact OFX header fields
|
|
135
|
+
# - +:statements+ — hash keyed by account_id, each with:
|
|
136
|
+
# +:currency+, +:transactions+ ({count:, net_cents:}),
|
|
137
|
+
# +:credits+ ({count:, total_cents:}), +:debits+ ({count:, total_cents:}),
|
|
138
|
+
# +:balance_cents+
|
|
87
139
|
def summary
|
|
88
140
|
{
|
|
89
141
|
headers: headers.compact,
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module OFX
|
|
4
|
-
module Tokenizer
|
|
4
|
+
module Tokenizer # :nodoc:
|
|
5
|
+
##
|
|
5
6
|
# Abstract base class for OFX tokenizers.
|
|
6
|
-
# Subclasses must implement
|
|
7
|
+
# Subclasses must implement #parse! to populate +@headers+ and +@body+
|
|
7
8
|
# from the raw file content.
|
|
8
9
|
class Base
|
|
9
|
-
|
|
10
|
-
#
|
|
11
|
-
attr_reader :headers
|
|
10
|
+
##
|
|
11
|
+
# Parsed header key/value pairs (Hash).
|
|
12
|
+
attr_reader :headers
|
|
13
|
+
##
|
|
14
|
+
# Parsed XML body (Nokogiri::XML::Document).
|
|
15
|
+
attr_reader :body
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
##
|
|
18
|
+
# Creates a new tokenizer and immediately parses +content+ (raw OFX String).
|
|
14
19
|
def initialize(content)
|
|
15
20
|
@content = content.dup.force_encoding('UTF-8')
|
|
16
21
|
parse!
|
|
@@ -14,7 +14,7 @@ module OFX
|
|
|
14
14
|
content = convert_to_utf8(@content)
|
|
15
15
|
parts = content.split(/<OFX>/i, 2)
|
|
16
16
|
|
|
17
|
-
raise InvalidHeaderError, 'Missing <OFX> tag in OFX file' if parts.size < 2
|
|
17
|
+
raise Errors::InvalidHeaderError, 'Missing <OFX> tag in OFX file' if parts.size < 2
|
|
18
18
|
|
|
19
19
|
@headers = parse_headers(parts[0])
|
|
20
20
|
@body = parse_body("<OFX>#{parts[1]}")
|
|
@@ -35,7 +35,7 @@ module OFX
|
|
|
35
35
|
def parse_body(raw)
|
|
36
36
|
normalized = normalize_sgml(raw)
|
|
37
37
|
doc = Nokogiri::XML(normalized, &:nonet)
|
|
38
|
-
raise InvalidBodyError, "OFX body could not be parsed: #{doc.errors.first}" if doc.errors.any?
|
|
38
|
+
raise Errors::InvalidBodyError, "OFX body could not be parsed: #{doc.errors.first}" if doc.errors.any?
|
|
39
39
|
|
|
40
40
|
doc
|
|
41
41
|
end
|
|
@@ -11,7 +11,7 @@ module OFX
|
|
|
11
11
|
content = convert_to_utf8(@content)
|
|
12
12
|
doc = Nokogiri::XML(content, &:nonet)
|
|
13
13
|
|
|
14
|
-
raise InvalidBodyError, "OFX2 body could not be parsed: #{doc.errors.first}" if doc.errors.any?
|
|
14
|
+
raise Errors::InvalidBodyError, "OFX2 body could not be parsed: #{doc.errors.first}" if doc.errors.any?
|
|
15
15
|
|
|
16
16
|
@headers = parse_headers(doc)
|
|
17
17
|
@body = doc
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OFX
|
|
4
|
+
# Represents a single financial transaction parsed from an OFX statement.
|
|
5
|
+
#
|
|
6
|
+
# === Example
|
|
7
|
+
#
|
|
8
|
+
# txn = OFX.new("statement.ofx").transactions.first
|
|
9
|
+
# txn.type #=> "DEBIT"
|
|
10
|
+
# txn.name #=> "AMAZON.COM"
|
|
11
|
+
# txn.amount #=> #<Money fractional:-5099 currency:USD>
|
|
12
|
+
# txn.posted_at #=> 2024-01-15 00:00:00 +0000
|
|
13
|
+
# txn.account #=> #<OFX::BankAccount ...>
|
|
14
|
+
# txn.statement #=> #<OFX::BankStatement ...>
|
|
15
|
+
class Transaction < Base::Entity
|
|
16
|
+
# Unique transaction identifier / FITID (String).
|
|
17
|
+
attr_accessor :fit_id
|
|
18
|
+
# Transaction type, e.g. "DEBIT", "CREDIT", "CHECK" (String).
|
|
19
|
+
attr_accessor :type
|
|
20
|
+
# Date the transaction was posted (Time or +nil+).
|
|
21
|
+
attr_accessor :posted_at
|
|
22
|
+
# Date the transaction actually occurred (Time or +nil+).
|
|
23
|
+
attr_accessor :occurred_at
|
|
24
|
+
# Transaction amount as a Money object (or +nil+).
|
|
25
|
+
attr_accessor :amount
|
|
26
|
+
# Transaction amount in the smallest currency unit, e.g. cents (Integer or +nil+).
|
|
27
|
+
attr_accessor :amount_cents
|
|
28
|
+
# Payee or description name (String or +nil+).
|
|
29
|
+
attr_accessor :name
|
|
30
|
+
# Memo or additional description (String or +nil+).
|
|
31
|
+
attr_accessor :memo
|
|
32
|
+
# Payee name from the PAYEE field, when present (String or +nil+).
|
|
33
|
+
attr_accessor :payee
|
|
34
|
+
# Check number, if applicable (String or +nil+).
|
|
35
|
+
attr_accessor :check_number
|
|
36
|
+
# Reference number (String or +nil+).
|
|
37
|
+
attr_accessor :ref_number
|
|
38
|
+
# Standard industry code / SIC (String or +nil+).
|
|
39
|
+
attr_accessor :sic
|
|
40
|
+
|
|
41
|
+
# The statement (BankStatement or CreditCardStatement) and account
|
|
42
|
+
# (BankAccount or CreditCardAccount) this transaction belongs to.
|
|
43
|
+
wired_by_builder :statement, :account
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OFX
|
|
4
|
+
##
|
|
5
|
+
# An +Enumerable+ collection of Transaction objects parsed from an OFX statement.
|
|
6
|
+
# Provides convenience filters for credits and debits.
|
|
7
|
+
class TransactionCollection
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# The statement (BankStatement, CreditCardStatement, or +nil+) this collection belongs to.
|
|
12
|
+
# Overridden per-instance by Base::Builder at build time.
|
|
13
|
+
def statement = nil
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# Creates a new collection from +transactions+ (Array of Transaction).
|
|
17
|
+
def initialize(transactions)
|
|
18
|
+
@transactions = transactions
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# Iterates over each transaction.
|
|
23
|
+
#
|
|
24
|
+
# :yields: transaction
|
|
25
|
+
def each(&)
|
|
26
|
+
@transactions.each(&)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# Returns the number of transactions in the collection (Integer).
|
|
31
|
+
def length
|
|
32
|
+
@transactions.length
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# Returns a new TransactionCollection containing only positive-amount transactions.
|
|
37
|
+
#
|
|
38
|
+
# === Example
|
|
39
|
+
#
|
|
40
|
+
# ofx.transactions.credits.length #=> 5
|
|
41
|
+
# ofx.transactions.credits.first.amount #=> #<Money fractional:10000 currency:USD>
|
|
42
|
+
def credits
|
|
43
|
+
sub = self.class.new(select { |t| t.amount.positive? })
|
|
44
|
+
# Propagate statement wiring so inferred_currency resolves to the correct
|
|
45
|
+
# account currency even when this sub-collection has no transactions.
|
|
46
|
+
if (stmt = statement)
|
|
47
|
+
sub.define_singleton_method(:statement) { stmt }
|
|
48
|
+
end
|
|
49
|
+
sub
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
# Returns a new TransactionCollection containing only negative-amount transactions.
|
|
54
|
+
#
|
|
55
|
+
# === Example
|
|
56
|
+
#
|
|
57
|
+
# ofx.transactions.debits.length #=> 37
|
|
58
|
+
# ofx.transactions.debits.first.name #=> "AMAZON.COM"
|
|
59
|
+
# ofx.transactions.debits.first.amount_cents #=> -5099
|
|
60
|
+
def debits
|
|
61
|
+
sub = self.class.new(select { |t| t.amount.negative? })
|
|
62
|
+
# Same as credits — statement is the authoritative source of currency.
|
|
63
|
+
if (stmt = statement)
|
|
64
|
+
sub.define_singleton_method(:statement) { stmt }
|
|
65
|
+
end
|
|
66
|
+
sub
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# Returns the sum of all positive transaction amounts as a Money object.
|
|
71
|
+
#
|
|
72
|
+
# === Example
|
|
73
|
+
#
|
|
74
|
+
# ofx.transactions.total_credits.format #=> "$350.00"
|
|
75
|
+
def total_credits
|
|
76
|
+
sum_amounts(credits)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# Returns the sum of all negative transaction amounts as a Money object.
|
|
81
|
+
#
|
|
82
|
+
# === Example
|
|
83
|
+
#
|
|
84
|
+
# ofx.transactions.total_debits.format #=> "-$1,234.56"
|
|
85
|
+
def total_debits
|
|
86
|
+
sum_amounts(debits)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
# Returns the net amount (credits + debits) as a Money object.
|
|
91
|
+
# This is NOT the account balance — it reflects only the transactions present in
|
|
92
|
+
# the OFX file and ignores any accumulated prior balance (e.g. opening cash balance
|
|
93
|
+
# or unpaid invoice from a previous period). Use Balance for the actual account balance.
|
|
94
|
+
#
|
|
95
|
+
# === Example
|
|
96
|
+
#
|
|
97
|
+
# ofx.transactions.net.format #=> "$500.00"
|
|
98
|
+
def net
|
|
99
|
+
total_credits + total_debits
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
##
|
|
103
|
+
# Returns a duplicate of the internal transactions array.
|
|
104
|
+
def to_a
|
|
105
|
+
@transactions.dup
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
# Returns +true+ if both collections contain the same transactions as +other+.
|
|
110
|
+
def ==(other)
|
|
111
|
+
@transactions == other.to_a
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def currency
|
|
117
|
+
statement&.account&.currency
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def sum_amounts(collection)
|
|
121
|
+
return Money.new(0, currency) if collection.none?
|
|
122
|
+
|
|
123
|
+
collection.map(&:amount).inject(:+)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
data/lib/ofx_kit/version.rb
CHANGED
data/lib/ofx_kit.rb
CHANGED
|
@@ -8,50 +8,64 @@ require 'stringio'
|
|
|
8
8
|
require 'time'
|
|
9
9
|
|
|
10
10
|
require_relative 'ofx_kit/version'
|
|
11
|
-
require_relative 'ofx_kit/errors'
|
|
12
|
-
require_relative 'ofx_kit/
|
|
11
|
+
require_relative 'ofx_kit/errors/error'
|
|
12
|
+
require_relative 'ofx_kit/errors/parse_error'
|
|
13
|
+
require_relative 'ofx_kit/errors/invalid_header_error'
|
|
14
|
+
require_relative 'ofx_kit/errors/invalid_body_error'
|
|
15
|
+
require_relative 'ofx_kit/errors/unsupported_version_error'
|
|
16
|
+
require_relative 'ofx_kit/errors/encoding_error'
|
|
17
|
+
require_relative 'ofx_kit/errors/configuration_error'
|
|
18
|
+
require_relative 'ofx_kit/errors/multiple_statements_error'
|
|
19
|
+
require_relative 'ofx_kit/configuration/core'
|
|
20
|
+
require_relative 'ofx_kit/configuration/section_proxy'
|
|
21
|
+
require_relative 'ofx_kit/configuration/date_parser'
|
|
22
|
+
require_relative 'ofx_kit/configuration/mapping_applicator'
|
|
13
23
|
require_relative 'ofx_kit/base/entity'
|
|
14
24
|
require_relative 'ofx_kit/base/account'
|
|
15
25
|
require_relative 'ofx_kit/base/statement'
|
|
16
26
|
require_relative 'ofx_kit/base/document'
|
|
17
|
-
require_relative 'ofx_kit/
|
|
18
|
-
require_relative 'ofx_kit/
|
|
19
|
-
require_relative 'ofx_kit/
|
|
20
|
-
require_relative 'ofx_kit/
|
|
21
|
-
require_relative 'ofx_kit/
|
|
22
|
-
require_relative 'ofx_kit/
|
|
23
|
-
require_relative 'ofx_kit/
|
|
27
|
+
require_relative 'ofx_kit/bank_account'
|
|
28
|
+
require_relative 'ofx_kit/credit_card_account'
|
|
29
|
+
require_relative 'ofx_kit/transaction'
|
|
30
|
+
require_relative 'ofx_kit/transaction_collection'
|
|
31
|
+
require_relative 'ofx_kit/balance'
|
|
32
|
+
require_relative 'ofx_kit/bank_statement'
|
|
33
|
+
require_relative 'ofx_kit/credit_card_statement'
|
|
24
34
|
require_relative 'ofx_kit/base/builder'
|
|
25
35
|
require_relative 'ofx_kit/tokenizer/base'
|
|
26
36
|
require_relative 'ofx_kit/tokenizer/ofx1'
|
|
27
37
|
require_relative 'ofx_kit/tokenizer/ofx2'
|
|
28
38
|
require_relative 'ofx_kit/parser'
|
|
29
39
|
|
|
40
|
+
##
|
|
30
41
|
# Top-level namespace for the ofx_kit gem.
|
|
31
|
-
# Provides module-level access to the shared
|
|
32
|
-
# a
|
|
42
|
+
# Provides module-level access to the shared Configuration instance and
|
|
43
|
+
# a configure block for customizing field mappings and XML tags.
|
|
44
|
+
#
|
|
45
|
+
# === Example: Configure custom field mappings
|
|
33
46
|
#
|
|
34
|
-
# @example Configure custom field mappings
|
|
35
47
|
# OFX.configure do |config|
|
|
36
48
|
# config.transaction.map 'MYFIELD', to: :my_attribute
|
|
37
49
|
# end
|
|
38
50
|
module OFX
|
|
39
51
|
class << self
|
|
40
|
-
|
|
52
|
+
##
|
|
53
|
+
# Parses an OFX file or IO object and returns a Parser instance.
|
|
41
54
|
# This is the primary entry point for the gem.
|
|
55
|
+
# +resource+ is a file path (String) or IO object containing OFX data.
|
|
42
56
|
#
|
|
43
|
-
#
|
|
44
|
-
# @return [Parser]
|
|
57
|
+
# === Example: Parse a file path
|
|
45
58
|
#
|
|
46
|
-
# @example Parse a file path
|
|
47
59
|
# ofx = OFX.new("statement.ofx")
|
|
48
60
|
# ofx.account #=> OFX::BankAccount
|
|
49
61
|
# ofx.transactions #=> OFX::TransactionCollection
|
|
50
62
|
#
|
|
51
|
-
#
|
|
63
|
+
# === Example: Parse an IO object
|
|
64
|
+
#
|
|
52
65
|
# ofx = OFX.new(File.open("statement.ofx"))
|
|
53
66
|
#
|
|
54
|
-
#
|
|
67
|
+
# === Example: Block form
|
|
68
|
+
#
|
|
55
69
|
# OFX.new("statement.ofx") do |ofx|
|
|
56
70
|
# puts ofx.balance
|
|
57
71
|
# end
|
|
@@ -59,22 +73,27 @@ module OFX
|
|
|
59
73
|
Parser.new(resource, &)
|
|
60
74
|
end
|
|
61
75
|
|
|
62
|
-
|
|
63
|
-
#
|
|
64
|
-
#
|
|
76
|
+
##
|
|
77
|
+
# Yields the current Configuration instance for customization.
|
|
78
|
+
#
|
|
79
|
+
# :yields: config
|
|
80
|
+
#
|
|
81
|
+
# Raises Errors::ConfigurationError if the block raises any error.
|
|
65
82
|
def configure
|
|
66
83
|
yield config
|
|
67
|
-
rescue ConfigurationError
|
|
84
|
+
rescue Errors::ConfigurationError
|
|
68
85
|
raise
|
|
69
86
|
rescue StandardError => e
|
|
70
|
-
raise ConfigurationError, e.message
|
|
87
|
+
raise Errors::ConfigurationError, e.message
|
|
71
88
|
end
|
|
72
89
|
|
|
73
|
-
|
|
90
|
+
##
|
|
91
|
+
# Returns the shared configuration instance (lazy-initialized).
|
|
74
92
|
def config
|
|
75
93
|
@config ||= Configuration.new
|
|
76
94
|
end
|
|
77
95
|
|
|
96
|
+
##
|
|
78
97
|
# Resets the configuration to its default state.
|
|
79
98
|
# Useful in tests to restore default field mappings between examples.
|
|
80
99
|
def reset_config!
|