Class: Bankjob::Statement
- Inherits:
-
Object
- Object
- Bankjob::Statement
- Defined in:
- lib/bankjob/statement.rb
Overview
A Statement object represents a bank statement and is generally the result of running a Bankjob scraper. The Statement holds an array of Transaction objects and specifies the closing balance and the currency in use.
A Scraper will create a Statement by scraping web pages in an online banking site. The Statement can then be stored as a file in CSV (Comma Separated Values) format using to_csv
or in OFX (Open Financial eXchange www.ofx.net) format using to_ofx
.
One special ability of Statement is the ability to merge with an existing statement, automatically eliminating overlapping transactions. This means that when writing subsequent statements to the same CSV file (note well: CSV only) a continous transaction record can be built up over a long period.
Constant Summary collapse
- CHECKING =
OFX value for the ACCTTYPE of a checking account
"CHECKING"
- SAVINGS =
OFX value for the ACCTTYPE of a savings account
"SAVINGS"
- MONEYMRKT =
OFX value for the ACCTTYPE of a money market account
"MONEYMRKT"
- CREDITLINE =
OFX value for the ACCTTYPE of a loan account
"CREDITLINE"
- ONE_MINUTE =
60
- ELEVEN_59_PM =
seconds at 23:59
23 * 60 * 60 + 59 * 60
- MIDDAY =
12 * 60 * 60
Instance Attribute Summary collapse
-
#account_number ⇒ Object
the account number of the statement - a 1-22 char string that must be passed into the initalizer of the Statement Translates to the OFX element ACCTID.
-
#account_type ⇒ Object
the type of bank account the statement is for Tranlsates to the OFX type ACCTTYPE and must be one of * CHECKING * SAVINGS * MONEYMRKT * CREDITLINE Use a constant to set this - defaults to CHECKING.
-
#bank_id ⇒ Object
the identifier of the bank - a 1-9 char string (may be empty) Translates to the OFX element BANKID.
-
#closing_available ⇒ Object
the avaliable funds in the account after the last transaction in the statement (generally the same as closing_balance) Translates to the OFX element BALAMT in AVAILBAL.
-
#closing_balance ⇒ Object
the account balance after the last transaction in the statement Translates to the OFX element BALAMT in LEDGERBAL.
-
#currency ⇒ Object
readonly
the three-letter currency symbol generated into the OFX output (defaults to EUR) This is passed into the initializer (usually by the Scraper - see Scraper#currency).
-
#from_date ⇒ Object
the first date of the period the statement covers Translates to the OFX element DTSTART.
-
#to_date ⇒ Object
the last date of the period the statement covers Translates to the OFX element DTEND.
-
#transactions ⇒ Object
the array of Transaction objects that comprise the statement.
Class Method Summary collapse
-
.csv_header ⇒ Object
Generates a string for use as a header in a CSV file for a statement.
Instance Method Summary collapse
-
#==(other) ⇒ Object
Overrides == to allow comparison of Statement objects.
-
#add_transaction(transaction) ⇒ Object
Appends a new Transaction to the end of this Statement.
-
#finish(most_recent_first, fake_times = false) ⇒ Object
Finishes the statement after scraping in two ways depending on the information that the scraper was able to obtain.
-
#from_csv(source, decimal = ".") ⇒ Object
Reads in transactions from a CSV file or string specified by
source
and adds them to this statement. -
#initialize(account_number, currency = "EUR") ⇒ Statement
constructor
Creates a new empty Statement with no transactions.
-
#merge(other) ⇒ Object
Merges the transactions of
other
into the transactions of this statement and returns the result. -
#merge!(other) ⇒ Object
Merges the transactions of
other
into the transactions of this statement. -
#merge_transactions(other) ⇒ Object
Merges the transactions of
other
into the transactions of this statement and returns the resulting array of transactions Raises an exception if the two statements overlap in a discontiguous fashion. -
#to_csv ⇒ Object
Generates a CSV (comma separated values) string with a single row for each transaction.
-
#to_ofx ⇒ Object
<xsd:sequence> <xsd:element name=“BANKID” type=“ofx:BankIdType”/> <xsd:element name=“BRANCHID” type=“ofx:AccountIdType” minOccurs=“0”/> <xsd:element name=“ACCTID” type=“ofx:AccountIdType”/> <xsd:element name=“ACCTTYPE” type=“ofx:AccountEnum”/> <xsd:element name=“ACCTKEY” type=“ofx:AccountIdType” minOccurs=“0”/> </xsd:sequence> </xsd:extension> </xsd:complexContent> </xsd:complexType>.
- #to_s ⇒ Object
Constructor Details
#initialize(account_number, currency = "EUR") ⇒ Statement
Creates a new empty Statement with no transactions. The account_number
must be specified as a 1-22 character string. The specified currency
defaults to EUR if nothing is passed in.
83 84 85 86 87 88 89 90 |
# File 'lib/bankjob/statement.rb', line 83 def initialize(account_number, currency = "EUR") @account_number = account_number @currency = currency @transactions = [] @account_type = CHECKING @closing_balance = nil @closing_available = nil end |
Instance Attribute Details
#account_number ⇒ Object
the account number of the statement - a 1-22 char string that must be passed into the initalizer of the Statement Translates to the OFX element ACCTID
59 60 61 |
# File 'lib/bankjob/statement.rb', line 59 def account_number @account_number end |
#account_type ⇒ Object
the type of bank account the statement is for Tranlsates to the OFX type ACCTTYPE and must be one of
-
CHECKING
-
SAVINGS
-
MONEYMRKT
-
CREDITLINE
Use a constant to set this - defaults to CHECKING
68 69 70 |
# File 'lib/bankjob/statement.rb', line 68 def account_type @account_type end |
#bank_id ⇒ Object
the identifier of the bank - a 1-9 char string (may be empty) Translates to the OFX element BANKID
54 55 56 |
# File 'lib/bankjob/statement.rb', line 54 def bank_id @bank_id end |
#closing_available ⇒ Object
the avaliable funds in the account after the last transaction in the statement (generally the same as closing_balance) Translates to the OFX element BALAMT in AVAILBAL
43 44 45 |
# File 'lib/bankjob/statement.rb', line 43 def closing_available @closing_available end |
#closing_balance ⇒ Object
the account balance after the last transaction in the statement Translates to the OFX element BALAMT in LEDGERBAL
39 40 41 |
# File 'lib/bankjob/statement.rb', line 39 def closing_balance @closing_balance end |
#currency ⇒ Object (readonly)
the three-letter currency symbol generated into the OFX output (defaults to EUR) This is passed into the initializer (usually by the Scraper - see Scraper#currency)
50 51 52 |
# File 'lib/bankjob/statement.rb', line 50 def currency @currency end |
#from_date ⇒ Object
the first date of the period the statement covers Translates to the OFX element DTSTART
76 77 78 |
# File 'lib/bankjob/statement.rb', line 76 def from_date @from_date end |
#to_date ⇒ Object
the last date of the period the statement covers Translates to the OFX element DTEND
72 73 74 |
# File 'lib/bankjob/statement.rb', line 72 def to_date @to_date end |
#transactions ⇒ Object
the array of Transaction objects that comprise the statement
46 47 48 |
# File 'lib/bankjob/statement.rb', line 46 def transactions @transactions end |
Class Method Details
.csv_header ⇒ Object
Generates a string for use as a header in a CSV file for a statement.
Delegates to Transaction#csv_header
181 182 183 |
# File 'lib/bankjob/statement.rb', line 181 def self.csv_header return Transaction.csv_header end |
Instance Method Details
#==(other) ⇒ Object
Overrides == to allow comparison of Statement objects. Two Statements are considered equal (that is, ==) if and only iff they have the same values for:
-
to_date
-
from_date
-
closing_balance
-
closing_available
-
each and every transaction.
Note that the transactions are compared with Transaction.==
110 111 112 113 114 115 116 117 118 119 |
# File 'lib/bankjob/statement.rb', line 110 def ==(other) # :nodoc: if other.kind_of?(Statement) return (from_date == other.from_date and to_date == other.to_date and closing_balance == other.closing_balance and closing_available == other.closing_available and transactions == other.transactions) end return false end |
#add_transaction(transaction) ⇒ Object
Appends a new Transaction to the end of this Statement
95 96 97 |
# File 'lib/bankjob/statement.rb', line 95 def add_transaction(transaction) @transactions << transaction end |
#finish(most_recent_first, fake_times = false) ⇒ Object
Finishes the statement after scraping in two ways depending on the information that the scraper was able to obtain. Optionally have your scraper class call this after scraping is finished.
This method:
-
Sets the closing balance and available_balance and the to_ and from_dates by using the first and last transactions in the list. Which transaction is used depends on whether
most_recent_first
is true or false. The scraper may just set these directly in which case this may not be necessary. -
If
fake_times
is true time-stamps are invented and added to the transaction date attributes. This is useful if the website beings scraped shows dates, but not times, but has transactions listed in chronoligical arder. Without this process, the ofx generated has no proper no indication of the order of transactions that occurred in the same day other than the order in the statement and this may be ignored by the client. (Specifically, Wesabe will reorder transactions in the same day if they all appear to occur at the same time).Note that the algorithm to set the fake times is a little tricky. Assuming the transactionsa are most-recent-first, the first last transaction on each day is set at 11:59pm each transaction prior to that is one minute earlier.
But for the first transactions in the statement, the first is set at a few minutes after midnight, then we count backward. (The actual number of minutes is based on the number of transactions + 1 to be sure it doesnt pass midnight)
This is crucial because transactions for a given day will often span 2 or more statement. By starting just after midnight and going back to just before midnight we reduce the chance of overlap.
If the to-date is the same as the from-date for a transaction, then we start at midday, so that prior and subsequent statements don’t overlap.
This simple algorithm basically guarantees no overlaps so long as:
i. The number of transactions is small compared to the number of minutes in a day
ii. A single day will not span more than 3 statements
If the statement is most-recent-last (+most_recent_first = false+) the same
algorithm is applied, only in reverse
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 |
# File 'lib/bankjob/statement.rb', line 363 def finish(most_recent_first, fake_times=false) if !@transactions.empty? then # if the user hasn't set the balances, set them to the first or last # transaction balance depending on the order if most_recent_first then @closing_balance ||= transactions.first.new_balance @closing_available ||= transactions.first.new_balance @to_date ||= transactions.first.date @from_date ||= transactions.last.date else @closing_balance ||= transactions.last.new_balance @closing_available ||= transactions.last.new_balance @to_date ||= transactions.last.date @from_date ||= transactions.first.date end if fake_times and to_date.hour == 0 then # the statement was unable to scrape times to go with the dates, but the # client (say wesabe) will get the transaction order wrong if there are no # times, so here we add times that order the transactions according to the # order of the array of transactions # the delta is 1 minute forward or backward fr if to_date == from_date then # all of the statement's transactions occur in the same day - to try to # avoid overlap with subsequent or previous transacitons we group order them # from 11am onward seconds = MIDDAY else seconds = (transactions.length + 1) * 60 end if most_recent_first then yday = transactions.first.date.yday start = 0 delta = 1 finish = transactions.length else yday = transactions.last.date.yday start = transactions.length - 1 finish = -1 delta = -1 end i = start until i == finish tx = transactions[i] if tx.date.yday != yday # starting a new day, begin the countdown from 23:59 again yday = tx.date.yday seconds = ELEVEN_59_PM end tx.date += seconds unless tx.date.hour > 0 seconds -= ONE_MINUTE i += delta end end end end |
#from_csv(source, decimal = ".") ⇒ Object
Reads in transactions from a CSV file or string specified by source
and adds them to this statement.
Uses a simple (dumb) heuristic to determine if the source
is a file or a string: if it contains a comma (,) then it is a string otherwise it is treated as a file path.
193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/bankjob/statement.rb', line 193 def from_csv(source, decimal = ".") if (source =~ /,/) # assume source is a string FasterCSV.parse(source) do |row| add_transaction(Transaction.from_csv(row, decimal)) end else # assume source is a filepath FasterCSV.foreach(source) do |row| add_transaction(Transaction.from_csv(row, decimal)) end end end |
#merge(other) ⇒ Object
Merges the transactions of other
into the transactions of this statement and returns the result. Neither statement is changed. See #merge! if you want to modify the statement. Raises an exception if the two statements overlap in a discontiguous fashion.
142 143 144 145 146 147 148 149 |
# File 'lib/bankjob/statement.rb', line 142 def merge(other) union = merge_transactions(other) merged = self.dup merged.closing_balance = nil merged.closing_available = nil merged.transactions = union return merged end |
#merge!(other) ⇒ Object
Merges the transactions of other
into the transactions of this statement. Causes this statement to be changed. See #merge for details.
155 156 157 158 159 |
# File 'lib/bankjob/statement.rb', line 155 def merge!(other) @closing_balance = nil @closing_available = nil @transactions = merge_transactions(other) end |
#merge_transactions(other) ⇒ Object
Merges the transactions of other
into the transactions of this statement and returns the resulting array of transactions Raises an exception if the two statements overlap in a discontiguous fashion.
126 127 128 129 130 131 132 133 134 |
# File 'lib/bankjob/statement.rb', line 126 def merge_transactions(other) if (other.kind_of?(Statement)) union = transactions | other.transactions # the set union of both # now check that the union contains all of the originals, otherwise # we have merged some sort of non-contiguous range raise "Failed to merge transactions properly." unless union.first(@transactions.length) == @transactions return union end end |
#to_csv ⇒ Object
Generates a CSV (comma separated values) string with a single row for each transaction. Note that no header row is generated as it would make it difficult to concatenate and merge subsequent CSV strings (but we should consider it as a user option in the future)
168 169 170 171 172 173 174 |
# File 'lib/bankjob/statement.rb', line 168 def to_csv buf = "" transactions.each do |transaction| buf << transaction.to_csv end return buf end |
#to_ofx ⇒ Object
<xsd:sequence>
<xsd:element name="BANKID" type="ofx:BankIdType"/>
<xsd:element name="BRANCHID" type="ofx:AccountIdType" minOccurs="0"/>
<xsd:element name="ACCTID" type="ofx:AccountIdType"/>
<xsd:element name="ACCTTYPE" type="ofx:AccountEnum"/>
<xsd:element name="ACCTKEY" type="ofx:AccountIdType" minOccurs="0"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
The to_ofx method will only generate the essential elements which are
-
BANKID - the bank identifier (a 1-9 char string - may be empty)
-
ACCTID - the account number (a 1-22 char string - may not be empty!)
-
ACCTTYPE - the type of account - must be one of:
"CHECKING", "SAVINGS", "MONEYMRKT", "CREDITLINE"
(See Transaction for a definition of STMTTRN)
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/bankjob/statement.rb', line 279 def to_ofx buf = "" # Use Builder to generate XML. Builder works by catching missing_method # calls and generating an XML element with the name of the missing method, # nesting according to the nesting of the calls and using arguments for content x = Builder::XmlMarkup.new(:target => buf, :indent => 2) x.OFX { x.BANKMSGSRSV1 { #Bank Message Response x.STMTTRNRS { #Statement-transaction aggregate response x.STMTRS { #Statement response x.CURDEF currency #Currency x.BANKACCTFROM { x.BANKID bank_id # bank identifier x.ACCTID account_number x.ACCTTYPE account_type # acct type: checking/savings/... } x.BANKTRANLIST { #Transactions x.DTSTART Bankjob.date_time_to_ofx(from_date) x.DTEND Bankjob.date_time_to_ofx(to_date) transactions.each { |transaction| buf << transaction.to_ofx } } x.LEDGERBAL { # the final balance at the end of the statement x.BALAMT closing_balance # balance amount x.DTASOF Bankjob.date_time_to_ofx(to_date) # balance date } x.AVAILBAL { # the final Available balance x.BALAMT closing_available x.DTASOF Bankjob.date_time_to_ofx(to_date) } } } } } return buf end |
#to_s ⇒ Object
423 424 425 426 427 428 429 430 |
# File 'lib/bankjob/statement.rb', line 423 def to_s buf = "#{self.class}: close_bal = #{closing_balance}, avail = #{closing_available}, curr = #{currency}, transactions:" transactions.each do |tx| buf << "\n\t\t#{tx.to_s}" end buf << "\n---\n" return buf end |