Skip to content

Instantly share code, notes, and snippets.

@elandesign
Created December 6, 2013 17:32
Show Gist options
  • Save elandesign/7828888 to your computer and use it in GitHub Desktop.
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.
#!/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 = @options.from..@options.to
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