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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +42 -82
  3. data/lib/generators/{ofx → ofx_kit}/eject_generator.rb +5 -3
  4. data/lib/ofx_kit/balance.rb +25 -0
  5. data/lib/ofx_kit/bank_account.rb +25 -0
  6. data/lib/ofx_kit/{domain/bank_statement.rb → bank_statement.rb} +1 -0
  7. data/lib/ofx_kit/base/account.rb +3 -0
  8. data/lib/ofx_kit/base/builder.rb +8 -4
  9. data/lib/ofx_kit/base/document.rb +16 -4
  10. data/lib/ofx_kit/base/entity.rb +15 -5
  11. data/lib/ofx_kit/base/statement.rb +19 -1
  12. data/lib/ofx_kit/configuration/core.rb +75 -28
  13. data/lib/ofx_kit/configuration/date_parser.rb +1 -1
  14. data/lib/ofx_kit/configuration/mapping_applicator.rb +5 -3
  15. data/lib/ofx_kit/configuration/section_proxy.rb +14 -5
  16. data/lib/ofx_kit/{domain/credit_card_account.rb → credit_card_account.rb} +1 -0
  17. data/lib/ofx_kit/{domain/credit_card_statement.rb → credit_card_statement.rb} +1 -0
  18. data/lib/ofx_kit/errors/configuration_error.rb +8 -0
  19. data/lib/ofx_kit/errors/encoding_error.rb +8 -0
  20. data/lib/ofx_kit/errors/error.rb +11 -0
  21. data/lib/ofx_kit/errors/invalid_body_error.rb +8 -0
  22. data/lib/ofx_kit/errors/invalid_header_error.rb +8 -0
  23. data/lib/ofx_kit/errors/multiple_statements_error.rb +8 -0
  24. data/lib/ofx_kit/errors/parse_error.rb +8 -0
  25. data/lib/ofx_kit/errors/unsupported_version_error.rb +20 -0
  26. data/lib/ofx_kit/parser.rb +81 -29
  27. data/lib/ofx_kit/tokenizer/base.rb +11 -6
  28. data/lib/ofx_kit/tokenizer/ofx1.rb +2 -2
  29. data/lib/ofx_kit/tokenizer/ofx2.rb +1 -1
  30. data/lib/ofx_kit/transaction.rb +45 -0
  31. data/lib/ofx_kit/transaction_collection.rb +126 -0
  32. data/lib/ofx_kit/version.rb +2 -1
  33. data/lib/ofx_kit.rb +43 -24
  34. metadata +36 -13
  35. data/lib/ofx_kit/configuration.rb +0 -6
  36. data/lib/ofx_kit/domain/balance.rb +0 -10
  37. data/lib/ofx_kit/domain/bank_account.rb +0 -8
  38. data/lib/ofx_kit/domain/transaction.rb +0 -13
  39. data/lib/ofx_kit/domain/transaction_collection.rb +0 -94
  40. data/lib/ofx_kit/errors.rb +0 -36
  41. /data/lib/generators/{ofx → ofx_kit}/templates/ofx_mappings.yml +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f82d22f9c1dce1691202e851448967eccd5ee7438871b10b5392038c128f084
4
- data.tar.gz: 1bb156f591d24d2297457e7e0a27a7bcf13bb003fc69670c349285af757610de
3
+ metadata.gz: c0538fb5e30bca1b8591a3941d97118342eaff9df4bbe959e3878d00fb7157e8
4
+ data.tar.gz: 60eb27469afdf3699db3bfcad650af9a14613bfb80c4c934b8962376a9488b5b
5
5
  SHA512:
6
- metadata.gz: 8586682ea5992fb07a2fa516bc02f113cad703034d97fa088590fd55c913e9996a850e43d5442898af6642b37d3af9563584038912454db5e96a9371089e6b8e
7
- data.tar.gz: 7e8ab351c06a24a55d13299563bce802e32247c22d6046d36aef40f43c010dfa9597a1b7481108fca7f635886638a0a340c3ff6d8c658b37ec990bad6790811e
6
+ metadata.gz: 8440081a443b21960fafd8163de2986ef437be3dd85e149da9ddbd4dba68f05464e1e09feed28393cc2dd3b60176cd03d5a61b69a510cd0eae776f3e70634edc
7
+ data.tar.gz: 388b029adf5d69347053a478ad5246b2ce9b12b2b0ee654f6e571389cb383305b14ae7272dbe195ecdf3bb4e84c6e31fd79d0e4dce93d7b414aa3efee71b6d64
data/README.md CHANGED
@@ -1,4 +1,7 @@
1
- # OFX
1
+ # OFX Kit
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/ofx_kit.svg)](https://badge.fury.io/rb/ofx_kit) [![Downloads](https://img.shields.io/gem/dt/ofx_kit?label=Downloads)](https://rubygems.org/gems/ofx_kit)
4
+
2
5
 
3
6
  A Ruby gem for parsing OFX (Open Financial Exchange) files. Supports OFX 1.x (SGML) and OFX 2.x (XML), bank statements and credit card statements, with a fluent API and configurable field mappings.
4
7
 
@@ -7,7 +10,7 @@ A Ruby gem for parsing OFX (Open Financial Exchange) files. Supports OFX 1.x (SG
7
10
  Add to your Gemfile:
8
11
 
9
12
  ```ruby
10
- gem "ofx_kit"
13
+ gem 'ofx_kit', '~> 1.0'
11
14
  ```
12
15
 
13
16
  ## Usage
@@ -123,47 +126,46 @@ ofx.balance # => OFX::MultipleStatementsError (use `balances`)
123
126
 
124
127
  ## Configuration
125
128
 
126
- Use `map` to bind OFX XML tags to Ruby attribute names of your choice.
129
+ ### Field mappings
127
130
 
128
- ### Adding new fields
129
-
130
- Map proprietary XML tags that your bank emits but the gem doesn't know about by default:
131
+ Use `map` to add new attributes or rename built-in ones:
131
132
 
132
133
  ```ruby
133
134
  OFX.configure do |config|
134
- config.bank_account.map "AGENCIA", to: "branch_code"
135
- config.transaction.map "HISPAYEEMEMO", to: "extended_memo"
136
- end
137
-
138
- ofx = OFX.new("statement.ofx")
139
- ofx.account.branch_code # => "0272"
140
- ofx.transactions.first.extended_memo # => "Tarifa bancaria"
141
- ```
135
+ # New field: your bank emits a tag the gem doesn't know about by default
136
+ config.bank_account.map "AGENCIA", to: "branch_code"
137
+ config.transaction.map "HISPAYEEMEMO", to: "extended_memo"
142
138
 
143
- ### Renaming built-in fields
144
-
145
- `map` renames the default attribute for any non-protected OFX tag:
146
-
147
- ```ruby
148
- OFX.configure do |config|
139
+ # Rename a built-in field to a name that fits your domain
149
140
  config.transaction.map "FITID", to: "uid" # default is fit_id
150
141
  config.transaction.map "NAME", to: "payee_name"
151
142
  end
152
143
 
153
144
  ofx = OFX.new("statement.ofx")
154
- ofx.transactions.first.uid # => "20240115001"
155
- ofx.transactions.first.payee_name # => "ACME Corp"
156
- # ofx.transactions.first.fit_id # => nil (FITID is now mapped to uid)
145
+ ofx.account.branch_code # => "0272"
146
+ ofx.transactions.first.extended_memo # => "Tarifa bancaria"
147
+ ofx.transactions.first.uid # => "20240115001"
148
+ # ofx.transactions.first.fit_id # => nil (FITID is now mapped to uid)
157
149
  ```
158
150
 
159
- > **Protected core fields** — The following OFX fields are used internally by the
160
- > gem to build Money objects and parse dates. They cannot be renamed:
161
- > `CURDEF`, `TRNAMT`, `DTPOSTED`, `DTUSER`, `BALAMT`, `DTASOF`.
162
- > Attempting to remap them raises `OFX::ConfigurationError`.
151
+ > **Protected core fields** — `CURDEF`, `TRNAMT`, `DTPOSTED`, `DTUSER`, `BALAMT`, `DTASOF`
152
+ > are used internally to build Money objects and parse dates. They cannot be renamed;
153
+ > attempting to do so raises `OFX::ConfigurationError`.
163
154
 
164
155
  ### Loading mappings from a YAML file
165
156
 
166
- For larger configurations, use a YAML file instead of inline `map` calls:
157
+ For larger configurations or Rails apps, a YAML file is cleaner than inline `map` calls.
158
+
159
+ **Rails** — eject the template with:
160
+
161
+ ```bash
162
+ rails generate ofx:eject
163
+ ```
164
+
165
+ This creates `config/initializers/ofx_mappings.yml`, which the gem detects and loads
166
+ automatically on boot — no `OFX.configure` call needed.
167
+
168
+ **Standalone** — point `load_mappings` at any YAML file:
167
169
 
168
170
  ```ruby
169
171
  OFX.configure do |config|
@@ -171,35 +173,15 @@ OFX.configure do |config|
171
173
  end
172
174
  ```
173
175
 
174
- The file must have a `FIELDS:` top-level key, with OFX section tags underneath.
175
- Each entry maps an XML tag to the Ruby attribute name you want to use:
176
+ The file must have a `FIELDS:` top-level key:
176
177
 
177
178
  ```yaml
178
179
  FIELDS:
179
180
  STMTTRN:
180
- # New field: your bank emits HISPAYEEMEMO but the gem doesn't know it by default
181
- HISPAYEEMEMO: extended_memo # → transaction.extended_memo
182
-
183
- # Override: rename a standard field to a name that fits your domain
184
- FITID: uid # → transaction.uid (default was fit_id)
185
-
181
+ HISPAYEEMEMO: extended_memo # transaction.extended_memo (new field)
182
+ FITID: uid # → transaction.uid (default was fit_id)
186
183
  BANKACCTFROM:
187
- # New field: Brazilian banks emit AGENCIA for the branch number
188
- AGENCIA: branch_code # → account.branch_code
189
- ```
190
-
191
- After loading:
192
-
193
- ```ruby
194
- ofx = OFX.new("statement.ofx")
195
-
196
- # Custom fields — read proprietary XML tags as Ruby attributes
197
- ofx.transactions.first.extended_memo # => "Tarifa bancaria"
198
- ofx.account.branch_code # => "0272"
199
-
200
- # Overridden field — standard tag mapped to a different name
201
- ofx.transactions.first.uid # => "20240115001"
202
- # ofx.transactions.first.fit_id # => nil (FITID is now mapped to uid)
184
+ AGENCIA: branch_code # account.branch_code (new field)
203
185
  ```
204
186
 
205
187
  ### Silencing warnings
@@ -210,40 +192,12 @@ ofx.transactions.first.uid # => "20240115001"
210
192
  OFX.config.multi_statement_warnings = false
211
193
  ```
212
194
 
213
- ### Default currency
195
+ ### Currency
214
196
 
215
- When a `TransactionCollection` is empty and has no statement context (e.g. an in-memory collection built in tests), aggregation methods like `total_credits` and `net` need a currency to produce a `Money.new(0, ...)` value. The fallback is `OFX.config.default_currency`, which defaults to `'USD'`:
216
-
217
- ```ruby
218
- OFX.config.default_currency = 'BRL'
219
- ```
220
-
221
- In normal usage this fallback is never reached — the gem wires every collection to its statement at parse time and reads the currency directly from `statement.account.currency`.
197
+ The OFX specification requires `CURDEF` in every statement (`STMTRS` / `CCSTMTRS`). If the tag is absent, the gem raises `OFX::Errors::InvalidBodyError` rather than silently assuming a currency.
222
198
 
223
199
  ### Rails
224
200
 
225
- **Field mappings** — run the eject generator to copy the default mappings into your app:
226
-
227
- ```bash
228
- rails generate ofx:eject
229
- ```
230
-
231
- This creates `config/initializers/ofx_mappings.yml` with all default mappings.
232
- The OFX gem detects and loads this file automatically on boot — no initializer or
233
- `OFX.configure` call needed.
234
-
235
- Edit the file to rename built-in fields or capture bank-specific XML tags:
236
-
237
- ```yaml
238
- # config/initializers/ofx_mappings.yml
239
- FIELDS:
240
- STMTTRN:
241
- FITID: "uid" # transaction.fit_id → transaction.uid
242
- HISPAYEEMEMO: "extended_memo" # → transaction.extended_memo (new field)
243
- BANKACCTFROM:
244
- AGENCIA: "branch_code" # → account.branch_code (new field)
245
- ```
246
-
247
201
  **Behavioral options** — create a standard initializer:
248
202
 
249
203
  ```ruby
@@ -272,6 +226,12 @@ end
272
226
 
273
227
  All tests must pass and RuboCop must report no offenses.
274
228
 
229
+ ### Generating documentation locally
230
+
231
+ ```bash
232
+ bundle exec rake rdoc
233
+ ```
234
+
275
235
  ## Testing locally via console
276
236
 
277
237
  You can exercise the gem interactively using `irb` from the project root. The `spec/fixtures/` directory contains sample OFX files ready to use.
@@ -2,7 +2,8 @@
2
2
 
3
3
  require 'rails/generators'
4
4
 
5
- module Ofx
5
+ module OFX
6
+ # Namespace for Rails generator classes provided by the ofx_kit gem.
6
7
  module Generators
7
8
  # Ejects OFX field mappings into the Rails application so they can be customized.
8
9
  #
@@ -10,8 +11,9 @@ module Ofx
10
11
  # mappings. The file is auto-detected and loaded by the OFX gem on boot —
11
12
  # no initializer or +OFX.configure+ call is needed.
12
13
  #
13
- # @example
14
- # rails generate ofx:eject
14
+ # === Example
15
+ #
16
+ # rails generate ofx_kit:eject
15
17
  class EjectGenerator < Rails::Generators::Base
16
18
  source_root File.expand_path('templates', __dir__)
17
19
 
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OFX
4
+ # Represents the ledger balance of an account at a specific point in time.
5
+ #
6
+ # === Example
7
+ #
8
+ # bal = OFX.new("statement.ofx").balance
9
+ # bal.amount.format #=> "$2,500.00"
10
+ # bal.amount_cents #=> 250000
11
+ # bal.posted_at #=> 2024-01-31 00:00:00 +0000
12
+ # bal.account #=> #<OFX::BankAccount ...>
13
+ class Balance < Base::Entity
14
+ # Closing balance as a Money object (or +nil+).
15
+ attr_accessor :amount
16
+ # Closing balance in the smallest currency unit, e.g. cents (Integer or +nil+).
17
+ attr_accessor :amount_cents
18
+ # Date the balance was posted (Time or +nil+).
19
+ attr_accessor :posted_at
20
+
21
+ # The statement (BankStatement or CreditCardStatement) and account
22
+ # (BankAccount or CreditCardAccount) this balance belongs to.
23
+ wired_by_builder :statement, :account
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OFX
4
+ # Represents a bank (checking or savings) account parsed from an OFX statement.
5
+ #
6
+ # === Example
7
+ #
8
+ # account = OFX.new("bank.ofx").account
9
+ # account.account_id #=> "123456789"
10
+ # account.bank_id #=> "021000021"
11
+ # account.account_type #=> "CHECKING"
12
+ # account.currency #=> "USD"
13
+ # account.balance #=> #<OFX::Balance ...>
14
+ # account.transactions #=> #<OFX::TransactionCollection ...>
15
+ class BankAccount < Base::Account
16
+ # Routing number of the financial institution (String or +nil+).
17
+ attr_accessor :bank_id
18
+ # Account number (String).
19
+ attr_accessor :account_id
20
+ # Account type, e.g. "CHECKING", "SAVINGS" (String or +nil+).
21
+ attr_accessor :account_type
22
+ # Branch identifier, when present (String or +nil+).
23
+ attr_accessor :branch_id
24
+ end
25
+ end
@@ -4,6 +4,7 @@ module OFX
4
4
  # Represents a complete bank statement parsed from an OFX file,
5
5
  # aggregating the account, its transactions, and the closing balance.
6
6
  class BankStatement < Base::Statement
7
+ # Always +true+.
7
8
  def bank_statement? = true
8
9
  end
9
10
  end
@@ -4,8 +4,11 @@ module OFX
4
4
  module Base
5
5
  # Abstract base class for financial accounts.
6
6
  class Account < Entity
7
+ # ISO 4217 currency code, e.g. "USD", "BRL" (String).
7
8
  attr_accessor :currency
8
9
 
10
+ # The statement (BankStatement or CreditCardStatement), closing balance (Balance or
11
+ # +nil+), and transactions (TransactionCollection) for this account.
9
12
  wired_by_builder :statement, :balance, :transactions
10
13
  end
11
14
  end
@@ -2,18 +2,22 @@
2
2
 
3
3
  module OFX
4
4
  module Base
5
- # Constructs domain objects ({BankStatement}, {CreditCardStatement}, etc.)
6
- # from a parsed {Base::Document}. Applies field mappings defined in {Configuration}
5
+ ##
6
+ # Constructs domain objects (BankStatement, CreditCardStatement, etc.)
7
+ # from a parsed Base::Document. Applies field mappings defined in Configuration
7
8
  # and converts raw OFX values (amounts, dates) into typed Ruby objects.
8
- class Builder
9
+ class Builder # :nodoc:
9
10
  include Configuration::DateParser
10
11
  include Configuration::MappingApplicator
11
12
 
13
+ ##
14
+ # Creates a new builder for the given +document+ (Base::Document).
12
15
  def initialize(document)
13
16
  @document = document
14
17
  end
15
18
 
16
- # @return [Array<BankStatement, CreditCardStatement>]
19
+ ##
20
+ # Returns all parsed statements (Array of BankStatement or CreditCardStatement).
17
21
  def statements
18
22
  @document.bank_statement_nodes.map { |n| build_bank_statement(n) } +
19
23
  @document.credit_card_statement_nodes.map { |n| build_credit_card_statement(n) }
@@ -2,28 +2,40 @@
2
2
 
3
3
  module OFX
4
4
  module Base
5
+ ##
5
6
  # Wraps the parsed OFX file, providing access to headers and XML body nodes.
6
- # Consumers use {#bank_statement_nodes} and {#credit_card_statement_nodes}
7
+ # Consumers use #bank_statement_nodes and #credit_card_statement_nodes
7
8
  # to extract statement data for domain object construction.
8
9
  class Document
10
+ ##
11
+ # Parsed OFX header key/value pairs (Hash).
9
12
  attr_reader :headers
10
13
 
14
+ ##
15
+ # Creates a new document.
16
+ # +headers+ is a Hash of parsed OFX header key/value pairs.
17
+ # +body+ is a Nokogiri::XML::Document of the parsed OFX body.
11
18
  def initialize(headers:, body:)
12
19
  @headers = headers
13
20
  @body = body
14
21
  end
15
22
 
16
- # @return [String, nil] OFX version declared in the file header
23
+ ##
24
+ # Returns the OFX version declared in the file header (String or +nil+).
17
25
  def version
18
26
  @headers['VERSION']
19
27
  end
20
28
 
21
- # @return [Nokogiri::XML::NodeSet] all STMTRS (bank statement) nodes in the document
29
+ ##
30
+ # Returns all STMTRS (bank statement) nodes in the document
31
+ # (Nokogiri::XML::NodeSet).
22
32
  def bank_statement_nodes
23
33
  @body.css('STMTRS')
24
34
  end
25
35
 
26
- # @return [Nokogiri::XML::NodeSet] all CCSTMTRS (credit card statement) nodes in the document
36
+ ##
37
+ # Returns all CCSTMTRS (credit card statement) nodes in the document
38
+ # (Nokogiri::XML::NodeSet).
27
39
  def credit_card_statement_nodes
28
40
  @body.css('CCSTMTRS')
29
41
  end
@@ -1,19 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OFX
4
- module Base
4
+ module Base # :nodoc:
5
+ ##
5
6
  # Abstract base for domain objects that support dynamic field mappings
6
- # and relationship wiring via {Base::Builder}.
7
+ # and relationship wiring via Base::Builder.
7
8
  #
8
9
  # Subclasses gain two class-level macros:
9
- # - {.ensure_attribute} — dynamically adds +attr_accessor+ for custom mapped fields
10
- # - {.wired_by_builder} — declares nil-returning placeholder methods that
11
- # {Base::Builder#wire_relations} overrides per-instance at build time
10
+ # - .ensure_attribute — dynamically adds +attr_accessor+ for custom mapped fields
11
+ # - .wired_by_builder — declares nil-returning placeholder methods that
12
+ # Base::Builder#wire_relations overrides per-instance at build time
12
13
  class Entity
14
+ ##
15
+ # Ensures the given attribute exists on the class, adding +attr_accessor+ if needed.
16
+ # Called by Base::Builder when applying custom field mappings.
17
+ # +name+ is an attribute name (String or Symbol).
13
18
  def self.ensure_attribute(name)
14
19
  attr_accessor name.to_sym unless method_defined?(name)
15
20
  end
16
21
 
22
+ ##
23
+ # Declares one or more placeholder instance methods that return +nil+.
24
+ # Base::Builder replaces each with a singleton method on the built instance
25
+ # that returns the wired relation.
26
+ # +names+ is an Array of Symbol relation names to declare.
17
27
  def self.wired_by_builder(*names)
18
28
  names.each { |name| define_method(name) { nil } }
19
29
  end
@@ -2,18 +2,36 @@
2
2
 
3
3
  module OFX
4
4
  module Base
5
+ ##
5
6
  # Base class for OFX statement types, aggregating an account,
6
7
  # its transactions, and the closing balance.
7
8
  class Statement
8
- attr_accessor :account, :transactions, :balance
9
+ ##
10
+ # The account associated with this statement (BankAccount or CreditCardAccount).
11
+ attr_accessor :account
12
+ ##
13
+ # The transactions in this statement (TransactionCollection).
14
+ attr_accessor :transactions
15
+ ##
16
+ # The closing balance for this statement (Balance or +nil+).
17
+ attr_accessor :balance
9
18
 
19
+ ##
20
+ # Creates a new statement.
21
+ # +account+ is a BankAccount or CreditCardAccount.
22
+ # +transactions+ is a TransactionCollection.
23
+ # +balance+ is a Balance or +nil+.
10
24
  def initialize(account:, transactions:, balance:)
11
25
  @account = account
12
26
  @transactions = transactions
13
27
  @balance = balance
14
28
  end
15
29
 
30
+ ##
31
+ # Always +false+.
16
32
  def bank_statement? = false
33
+ ##
34
+ # Always +false+.
17
35
  def credit_card_statement? = false
18
36
  end
19
37
  end
@@ -3,31 +3,45 @@
3
3
  require 'yaml'
4
4
 
5
5
  module OFX
6
+ ##
6
7
  # Manages XML-to-Ruby field mappings used during OFX document parsing.
7
8
  #
8
9
  # Mappings are split into two layers:
9
- # - *Core* ({core_mappings.yml}): OFX-standard fields whose Ruby attribute names are
10
- # referenced by name inside {Base::Builder}. These cannot be overridden.
11
- # - *User* ({field_mappings.yml}): convenience mappings that can be added to or replaced
12
- # at runtime via {#load_mappings} or the {OFX.configure} block.
10
+ # - *Core* (+core_mappings.yml+): OFX-standard fields whose Ruby attribute names are
11
+ # referenced by name inside Base::Builder. These cannot be overridden.
12
+ # - *User* (+field_mappings.yml+): convenience mappings that can be added to or replaced
13
+ # at runtime via #load_mappings or the OFX.configure block.
13
14
  class Configuration
15
+ ##
16
+ # Absolute path to the built-in core OFX field mappings (read-only).
14
17
  CORE_MAPPINGS_PATH = File.join(__dir__, '..', 'mappings', 'core_mappings.yml')
18
+ ##
19
+ # Absolute path to the built-in user-layer field mappings.
15
20
  MAPPINGS_PATH = File.join(__dir__, '..', 'mappings', 'field_mappings.yml')
16
21
 
22
+ ##
17
23
  # Conventional path for user mappings in a Rails application.
18
- # Auto-loaded on boot when present. Ejected via +rails generate ofx:eject+.
24
+ # Auto-loaded on boot when present. Ejected via +rails generate ofx_kit:eject+.
19
25
  RAILS_MAPPINGS_PATH = 'config/initializers/ofx_mappings.yml'
20
26
 
27
+ ##
28
+ # Controls whether a warning is emitted when OFX::Parser#transactions or
29
+ # OFX::Parser#balances aggregate across multiple statements.
30
+ # Defaults to +true+.
21
31
  attr_writer :multi_statement_warnings
22
- attr_accessor :default_currency
23
32
 
33
+ ##
34
+ # Returns +true+ if multi-statement aggregation warnings are enabled.
24
35
  def multi_statement_warnings?
25
36
  @multi_statement_warnings
26
37
  end
27
38
 
39
+ ##
40
+ # Creates a new Configuration instance.
41
+ # +auto_load_path+ is the path to a YAML mappings file loaded automatically on
42
+ # initialization. Defaults to RAILS_MAPPINGS_PATH expanded from the working directory.
28
43
  def initialize(auto_load_path: File.expand_path(RAILS_MAPPINGS_PATH))
29
44
  @multi_statement_warnings = true
30
- @default_currency = 'USD'
31
45
 
32
46
  core = YAML.safe_load_file(CORE_MAPPINGS_PATH)
33
47
  @sections = core.fetch('SECTIONS', {})
@@ -40,23 +54,41 @@ module OFX
40
54
  load_mappings(auto_load_path) if File.exist?(auto_load_path)
41
55
  end
42
56
 
43
- # Returns a {SectionProxy} for the given section, allowing inline mapping
44
- # configuration via {SectionProxy#map}.
45
- %w[bank_statement credit_card_statement transaction bank_account credit_card_account balance].each do |section|
46
- define_method(section) { SectionProxy.new(@user_fields, @core_fields, xml_tag_for(section)) }
47
- end
57
+ ##
58
+ # Returns a SectionProxy for bank statement field mappings.
59
+ def bank_statement = SectionProxy.new(@user_fields, @core_fields, xml_tag_for(:bank_statement))
60
+
61
+ ##
62
+ # Returns a SectionProxy for credit card statement field mappings.
63
+ def credit_card_statement = SectionProxy.new(@user_fields, @core_fields, xml_tag_for(:credit_card_statement))
64
+
65
+ ##
66
+ # Returns a SectionProxy for transaction field mappings.
67
+ def transaction = SectionProxy.new(@user_fields, @core_fields, xml_tag_for(:transaction))
68
+
69
+ ##
70
+ # Returns a SectionProxy for bank account field mappings.
71
+ def bank_account = SectionProxy.new(@user_fields, @core_fields, xml_tag_for(:bank_account))
72
+
73
+ ##
74
+ # Returns a SectionProxy for credit card account field mappings.
75
+ def credit_card_account = SectionProxy.new(@user_fields, @core_fields, xml_tag_for(:credit_card_account))
76
+
77
+ ##
78
+ # Returns a SectionProxy for balance field mappings.
79
+ def balance = SectionProxy.new(@user_fields, @core_fields, xml_tag_for(:balance))
48
80
 
49
- # Returns the OFX XML tag name corresponding to the given section identifier.
50
- # @param section_name [String, Symbol] section identifier (e.g. +:transaction+)
51
- # @return [String, nil] the XML tag name, or +nil+ if not found
81
+ ##
82
+ # Returns the OFX XML tag name corresponding to the given +section_name+
83
+ # (String or Symbol), e.g. +:transaction+. Returns +nil+ if not found.
52
84
  def xml_tag_for(section_name)
53
85
  @section_to_tag[section_name.to_s]
54
86
  end
55
87
 
56
- # Returns the merged hash of XML tag → Ruby attribute mappings for the given section.
88
+ ##
89
+ # Returns the merged Hash of XML tag to Ruby attribute mappings for the given
90
+ # +section_name+ (String or Symbol).
57
91
  # Core mappings take precedence; user mappings extend them.
58
- # @param section_name [String, Symbol] section identifier
59
- # @return [Hash{String => String}] mapping of XML tags to Ruby attribute names
60
92
  def xml_mappings_for(section_name)
61
93
  tag = xml_tag_for(section_name)
62
94
  return {} unless tag
@@ -64,19 +96,34 @@ module OFX
64
96
  (@core_fields[tag] || {}).merge(@user_fields[tag] || {})
65
97
  end
66
98
 
67
- # Merges additional field mappings from a YAML file into the user-layer configuration.
99
+ ##
100
+ # Merges additional field mappings from a YAML file at +path+ (String)
101
+ # into the user-layer configuration.
68
102
  # The file must have a top-level +FIELDS+ key. Core OFX fields cannot be overridden.
69
- # @param path [String] path to the YAML mappings file
70
- # @raise [ConfigurationError] if the file is missing, malformed, references unknown
71
- # sections, or attempts to override a core field mapping
103
+ #
104
+ # Raises Errors::ConfigurationError if the file is missing, malformed, references
105
+ # unknown sections, or attempts to override a core field mapping.
106
+ #
107
+ # === Example: Load a custom mappings file
108
+ #
109
+ # OFX.configure do |config|
110
+ # config.load_mappings 'config/my_ofx_mappings.yml'
111
+ # end
112
+ #
113
+ # === Example: Expected YAML format
114
+ #
115
+ # # config/my_ofx_mappings.yml
116
+ # FIELDS:
117
+ # STMTTRN:
118
+ # MYFIELD: my_attribute
72
119
  def load_mappings(path)
73
- raise ConfigurationError, "Mappings file not found: #{path}" unless File.exist?(path)
120
+ raise Errors::ConfigurationError, "Mappings file not found: #{path}" unless File.exist?(path)
74
121
 
75
122
  raw = YAML.safe_load_file(path)
76
- raise ConfigurationError, 'Invalid mappings file: expected a Hash' unless raw.is_a?(Hash)
123
+ raise Errors::ConfigurationError, 'Invalid mappings file: expected a Hash' unless raw.is_a?(Hash)
77
124
 
78
125
  fields = raw.fetch('FIELDS') do
79
- raise ConfigurationError, "Invalid mappings file: missing top-level 'FIELDS' key"
126
+ raise Errors::ConfigurationError, "Invalid mappings file: missing top-level 'FIELDS' key"
80
127
  end
81
128
 
82
129
  fields.each { |tag, mappings| merge_user_section(tag, mappings) }
@@ -86,11 +133,11 @@ module OFX
86
133
 
87
134
  def merge_user_section(xml_tag, mappings)
88
135
  unless @sections.key?(xml_tag.to_s)
89
- raise ConfigurationError, "Unknown section '#{xml_tag}'. Valid sections: #{@sections.keys.join(', ')}"
136
+ raise Errors::ConfigurationError, "Unknown section '#{xml_tag}'. Valid sections: #{@sections.keys.join(', ')}"
90
137
  end
91
138
 
92
139
  unless mappings.is_a?(Hash)
93
- raise ConfigurationError, "Mapping value for '#{xml_tag}' must be a Hash, got #{mappings.class}"
140
+ raise Errors::ConfigurationError, "Mapping value for '#{xml_tag}' must be a Hash, got #{mappings.class}"
94
141
  end
95
142
 
96
143
  mappings.each_key { |k| assert_not_core!(xml_tag, k) }
@@ -103,7 +150,7 @@ module OFX
103
150
  core_attr = @core_fields.dig(xml_tag.to_s, xml_key.to_s)
104
151
  return unless core_attr
105
152
 
106
- raise ConfigurationError,
153
+ raise Errors::ConfigurationError,
107
154
  "Cannot override core mapping '#{xml_tag}.#{xml_key}' (reserved as '#{core_attr}')"
108
155
  end
109
156
  end
@@ -4,7 +4,7 @@ require 'time'
4
4
 
5
5
  module OFX
6
6
  class Configuration
7
- # Mixin included by {Base::Builder} to parse OFX date strings into +Time+ objects.
7
+ # Mixin included by Base::Builder to parse OFX date strings into +Time+ objects.
8
8
  # Handles the two formats found in OFX files (YYYYMMDD and YYYYMMDDHHMMSS),
9
9
  # stripping any timezone suffixes (e.g. +[+05:30]+) before parsing.
10
10
  module DateParser
@@ -2,9 +2,9 @@
2
2
 
3
3
  module OFX
4
4
  class Configuration
5
- # Mixin included by {Base::Builder} to apply {Configuration} field mappings
5
+ # Mixin included by Base::Builder to apply Configuration field mappings
6
6
  # from an XML node onto a domain object. Reads XML text values, ensures
7
- # custom attributes exist via {Base::Entity.ensure_attribute}, and assigns them.
7
+ # custom attributes exist via Base::Entity.ensure_attribute, and assigns them.
8
8
  module MappingApplicator
9
9
  private
10
10
 
@@ -23,7 +23,9 @@ module OFX
23
23
  def currency_for(node, section)
24
24
  xml_tag = OFX.config.xml_mappings_for(section).key('currency')
25
25
  value = xml_tag && text_at(node, xml_tag)
26
- (value.nil? || value.empty?) ? OFX.config.default_currency : value
26
+ raise Errors::InvalidBodyError, 'Missing required CURDEF tag' if value.nil? || value.empty?
27
+
28
+ value
27
29
  end
28
30
 
29
31
  def text_at(node, css_tag)