Created
December 6, 2013 17:32
-
-
Save elandesign/7828888 to your computer and use it in GitHub Desktop.
Generate an OFX file of dummy, sane(ish) transactions. Used to test bank statement uploads for FreeAgent.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# == Synopsis | |
# Generate a dummy OFX bank transactions file | |
# Depends on nokogiri, faker and pickup gems | |
# OFX format reverse engineered from my Nationwide bank download. YMMV. | |
# | |
# == Usage | |
# ofx-generator [options] > file.ofx | |
# | |
# == Options | |
# -c, --count The number of dummy transactions to generate (average 1 per day) | |
# -f, --from Dated from (3 months ago) | |
# -t, --to Dated to (yesterday) | |
# --currency The bank currency (GBP) | |
# --opening-balance The opening balance (zero) | |
# --bank-id The BANKID (0) | |
# --account-id The ACCTID (****1234) | |
# --account-type The ACCTTYPE (CHECKING) | |
# | |
# == Author | |
# Paul Smith | |
require 'optparse' | |
require 'ostruct' | |
require 'date' | |
require 'bigdecimal' | |
require 'faker' | |
require 'nokogiri' | |
require 'pickup' | |
class OfxGenerator | |
attr_reader :options | |
DATE_FORMAT = "%Y%m%d%H%M%S.%L[#{Time.now.utc_offset}]" | |
NUMBER_FORMAT = "%.2f" | |
TRANSACTION_TYPES = { | |
"CREDIT" => { :weight => 300, :range => BigDecimal('0.01')..BigDecimal('10000.00') }, | |
"POS" => { :weight => 1000, :range => BigDecimal('-1000.00')..BigDecimal('-0.01') }, | |
"XFER" => { :weight => 100, :range => BigDecimal('-1000.00')..BigDecimal('1000.00') }, | |
"FEE" => { :weight => 10, :range => BigDecimal('-40.00')..BigDecimal('-0.01'), :name => "Bank Fees" }, | |
"DIRECTDEBIT" => { :weight => 10, :range => BigDecimal('-200.00')..BigDecimal('-10.00') } | |
} | |
TRANSACTIONS = Pickup.new(TRANSACTION_TYPES.inject({}) { |weights, (type, options)| weights[type] = options[:weight]; weights }) | |
def initialize(arguments) | |
@arguments = arguments | |
@options = OpenStruct.new | |
@options.verbose = false | |
@options.from = Date.today << 3 | |
@options.to = Date.today - 1 | |
@options.count = nil | |
@options.currency = 'GBP' | |
@options.bankid = 0 | |
@options.acctid = "****1234" | |
@options.accttype = "CHECKING" | |
@options.opening_balance = BigDecimal('0.00') | |
end | |
# Parse options, check arguments, then process the command | |
def run | |
if parsed_options? | |
process_command | |
else | |
output_options | |
end | |
end | |
protected | |
def parsed_options? | |
opts = OptionParser.new | |
opts.on('-V', '--verbose') { @options.verbose = true } | |
opts.on('-c', '--count [NUMBER]') { |count| @options.count = count.to_i } | |
opts.on('-f', '--from [DATE]') { |date| @options.from = Date.parse(date) } | |
opts.on('-t', '--to [DATE]') { |date| @options.to = Date.parse(date) } | |
opts.on('--currency [CURRENCY]') { |currency| @options.currency = currency } | |
opts.on('--opening-balance [BAL]') { |balance| @options.opening_balance = BigDecimal(balance) } | |
opts.on('--bank-id [ID]') { |id| @options.bankid = id } | |
opts.on('--account-id [ID]') { |id| @options.acctid = id } | |
opts.on('--account-type [TYPE]') { |type| @options.accttype = type } | |
opts.parse!(@arguments) rescue return false | |
@options.count ||= (@options.to - @options.from).to_i | |
@date_range = @[email protected] | |
true | |
end | |
def output_options | |
puts "Options:\n" | |
@options.marshal_dump.each do |name, val| | |
puts " #{name} = #{val}" | |
end | |
end | |
def process_command | |
transactions = @options.count.times.collect { build_transaction }.sort { |a, b| a[:date] <=> b[:date] } | |
builder = Nokogiri::XML::Builder.new(:encoding => "utf-8") do |xml| | |
xml << '<?OFX OFXHEADER="200" VERSION="203" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>' | |
xml.OFX do | |
xml.SIGNONMSGSRSV1 do | |
xml.SONRS do | |
xml.STATUS do | |
xml.CODE 0 | |
xml.SEVERITY 'INFO' | |
end | |
xml.DTSERVER Time.now.strftime(DATE_FORMAT) | |
xml.LANGUAGE 'ENG' | |
end | |
end | |
xml.BANKMSGSRSV1 do | |
xml.STMTTRNRS do | |
xml.TRNUID 'A' | |
xml.STATUS do | |
xml.CODE 0 | |
xml.SEVERITY 'INFO' | |
end | |
xml.STMTRS do | |
xml.CURDEF @options.currency | |
xml.BANKACCTFROM do | |
xml.BANKID @options.bankid | |
xml.ACCTID @options.acctid | |
xml.ACCTTYPE @options.accttype | |
end | |
end | |
xml.BANKTRANLIST do | |
xml.DTSTART @options.from.strftime(DATE_FORMAT) | |
xml.DTEND @options.to.strftime(DATE_FORMAT) | |
transactions.each do |tx| | |
xml.STMTTRN do | |
xml.TRNTYPE tx[:type] | |
xml.DTPOSTED tx[:date].strftime(DATE_FORMAT) | |
xml.TRNAMT NUMBER_FORMAT % tx[:value] | |
xml.FITID fitid(tx) | |
xml.NAME tx[:name] | |
end | |
end | |
end | |
xml.LEDGERBAL do | |
xml.BALAMT NUMBER_FORMAT % (@options.opening_balance + transactions.map { |tx| tx[:value] }.reduce(:+)) | |
xml.DTASOF @options.to.strftime(DATE_FORMAT) | |
end | |
end | |
end | |
end | |
end | |
puts builder.to_xml | |
end | |
def build_transaction | |
type = TRANSACTIONS.pick | |
options = TRANSACTION_TYPES[type] | |
{ | |
:type => type, | |
:value => random_value(options[:range]), | |
:date => rand(@date_range), | |
:name => options[:name] || Faker::Company.name | |
} | |
end | |
def fitid(tx) | |
"00#{tx[:type]}#{tx[:date].strftime('%Y%m%d')}1200000000#{(tx[:value] * 100).to_i}#{tx[:name].gsub(' ', '')}" | |
end | |
def random_value(range) | |
baseline = range.min | |
integer_rage = (range.max - range.min) * 100 | |
pennies = rand(integer_rage) | |
baseline + (BigDecimal(pennies) / BigDecimal('100')) | |
end | |
end | |
app = OfxGenerator.new(ARGV) | |
app.run |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment