ofx_kit 0.1.0 → 1.0.0
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 +35 -81
- 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 +35 -14
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56019a467dfb822daae452b467ec5e51cc6044513048e5fa6ed2db82522e39d0
|
|
4
|
+
data.tar.gz: aba4a9a24703b9b7b58af9a3c04ec271a8d6d5c2975163487ee61e551a8fcf6f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e07dc48f4349908f645485553f10ed1aa76ede8927e89b6da95137f1a5a7bf3258d8e03835e58c060bbf0a12921a725b36105712ef9b8490f401f8ae125de25a
|
|
7
|
+
data.tar.gz: d09b9ff3bd3d04ad198d12a0bca8ee4e12ee2b2d9daa09e08c6d832bff2243c13e22e6fee0497a6b0fcc2e315c6d4ae6472b4078d7e63e6b544738e52e70fc2b
|
data/README.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
# OFX
|
|
1
|
+
# OFX Kit
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/ofx_kit) [](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
|
|
|
@@ -123,47 +126,46 @@ ofx.balance # => OFX::MultipleStatementsError (use `balances`)
|
|
|
123
126
|
|
|
124
127
|
## Configuration
|
|
125
128
|
|
|
126
|
-
|
|
129
|
+
### Field mappings
|
|
127
130
|
|
|
128
|
-
|
|
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
|
-
|
|
135
|
-
config.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
ofx = OFX.new("statement.ofx")
|
|
139
|
-
ofx.account.branch_code # => "0272"
|
|
140
|
-
ofx.transactions.first.extended_memo # => "Tarifa bancaria"
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### Renaming built-in fields
|
|
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"
|
|
144
138
|
|
|
145
|
-
|
|
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.
|
|
155
|
-
ofx.transactions.first.
|
|
156
|
-
|
|
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** —
|
|
160
|
-
>
|
|
161
|
-
>
|
|
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,
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
214
|
-
|
|
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
|
-
```
|
|
195
|
+
### Currency
|
|
220
196
|
|
|
221
|
-
|
|
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
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'rails/generators'
|
|
4
4
|
|
|
5
|
-
module
|
|
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
|
-
#
|
|
14
|
-
#
|
|
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
|
data/lib/ofx_kit/base/account.rb
CHANGED
|
@@ -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
|
data/lib/ofx_kit/base/builder.rb
CHANGED
|
@@ -2,18 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module OFX
|
|
4
4
|
module Base
|
|
5
|
-
|
|
6
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/ofx_kit/base/entity.rb
CHANGED
|
@@ -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
|
|
7
|
+
# and relationship wiring via Base::Builder.
|
|
7
8
|
#
|
|
8
9
|
# Subclasses gain two class-level macros:
|
|
9
|
-
# -
|
|
10
|
-
# -
|
|
11
|
-
#
|
|
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
|
-
|
|
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* (
|
|
10
|
-
# referenced by name inside
|
|
11
|
-
# - *User* (
|
|
12
|
-
# at runtime via
|
|
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
|
|
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
|
-
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
#
|
|
51
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
@@ -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
|
|