├── spec
├── sample-data
│ ├── empty.iif
│ ├── empty.csv
│ ├── empty.ofx
│ ├── empty.qbo
│ ├── with-bank-info.ofx
│ ├── with-transaction.ofx
│ ├── basic.csv
│ ├── basic.iif
│ └── basic.qbo
├── spec_helper.rb
├── stripe-iiftoqbo_spec.rb
├── ofx_spec.rb
└── iif_spec.rb
├── .gitignore
├── .rspec
├── lib
├── iif
│ ├── entry.rb
│ ├── transaction.rb
│ └── parser.rb
├── iif.rb
├── ofx
│ └── transaction.rb
├── ofx.rb
└── stripe-iiftoqbo.rb
├── stripe-iiftoqbo.gemspec
├── LICENSE
├── bin
└── stripe-iiftoqbo
└── README.md
/spec/sample-data/empty.iif:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | .#*
3 | \#*
4 |
--------------------------------------------------------------------------------
/spec/sample-data/empty.csv:
--------------------------------------------------------------------------------
1 | Date,Name,Account,Memo,Amount
2 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --format documentation
3 | -r ./spec/spec_helper.rb
4 |
5 |
--------------------------------------------------------------------------------
/lib/iif/entry.rb:
--------------------------------------------------------------------------------
1 | require 'ostruct'
2 |
3 | module IIF
4 | class Entry < OpenStruct
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/iif/transaction.rb:
--------------------------------------------------------------------------------
1 | module IIF
2 | class Transaction
3 | attr_accessor :entries
4 |
5 | def initialize
6 | self.entries = []
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/iif.rb:
--------------------------------------------------------------------------------
1 | require 'iif/parser'
2 |
3 | def IIF(resource, &block)
4 | parser = IIF::Parser.new(resource)
5 |
6 | if block_given?
7 | if block.arity == 1
8 | yield parser
9 | else
10 | parser.instance_eval(&block)
11 | end
12 | end
13 |
14 | parser
15 | end
16 |
--------------------------------------------------------------------------------
/lib/ofx/transaction.rb:
--------------------------------------------------------------------------------
1 | module OFX
2 | class Transaction
3 | attr_accessor :trntype
4 | attr_accessor :dtposted
5 | attr_accessor :trnamt
6 | attr_accessor :fitid
7 | attr_accessor :name
8 | attr_accessor :memo
9 |
10 | def trnamt=(amt)
11 | @trnamt = BigDecimal.new(amt)
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/sample-data/empty.ofx:
--------------------------------------------------------------------------------
1 | OFXHEADER:100
2 | DATA:OFXSGML
3 | VERSION:103
4 | SECURITY:NONE
5 | ENCODING:USASCII
6 | CHARSET:1252
7 | COMPRESSION:NONE
8 | OLDFILEUID:NONE
9 | NEWFILEUID:NONE
10 |
11 | 0INFO20140211000000ENG00INFOUSD
12 |
--------------------------------------------------------------------------------
/spec/sample-data/empty.qbo:
--------------------------------------------------------------------------------
1 | OFXHEADER:100
2 | DATA:OFXSGML
3 | VERSION:103
4 | SECURITY:NONE
5 | ENCODING:USASCII
6 | CHARSET:1252
7 | COMPRESSION:NONE
8 | OLDFILEUID:NONE
9 | NEWFILEUID:NONE
10 |
11 | 0INFO20140211000000ENGStripe0000INFOUSD123456789CHECKING0.0
12 |
--------------------------------------------------------------------------------
/stripe-iiftoqbo.gemspec:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | Gem::Specification.new do |s|
3 | s.name = 'stripe-iiftoqbo'
4 | s.version = '0.1.4'
5 | s.summary = 'Stripe IIF-to-QBO converter for Quickbooks Online'
6 | s.description = 'Converts Stripe\'s IIF transaction file into a QBO file for importing into Quickbooks Online. A QBO file is in OFX (Open Financial Exchange) format.'
7 | s.homepage = 'https://github.com/gtolle/stripe-iiftoqbo'
8 | s.email = ['gilman.tolle@gmail.com']
9 | s.authors = ['Gilman Tolle']
10 | s.license = 'MIT'
11 | s.files = `git ls-files`.split("\n")
12 | s.require_paths = ["lib"]
13 |
14 | s.executables << 'stripe-iiftoqbo'
15 |
16 | s.add_runtime_dependency 'bigdecimal', '~> 1.2'
17 | s.add_runtime_dependency 'nokogiri', '~> 1.6'
18 | end
19 |
--------------------------------------------------------------------------------
/spec/sample-data/with-bank-info.ofx:
--------------------------------------------------------------------------------
1 | OFXHEADER:100
2 | DATA:OFXSGML
3 | VERSION:103
4 | SECURITY:NONE
5 | ENCODING:USASCII
6 | CHARSET:1252
7 | COMPRESSION:NONE
8 | OLDFILEUID:NONE
9 | NEWFILEUID:NONE
10 |
11 | 0INFO20140211000000ENGStripe0000INFOUSD123456789TestCHECKING20140101201402010.020140201
12 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # This file was generated by the `rspec --init` command. Conventionally, all
2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3 | # Require this file using `require "spec_helper"` to ensure that it is only
4 | # loaded once.
5 | #
6 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7 | RSpec.configure do |config|
8 | config.treat_symbols_as_metadata_keys_with_true_values = true
9 | config.run_all_when_everything_filtered = true
10 | # config.filter_run :focus
11 |
12 | # Run specs in random order to surface order dependencies. If you find an
13 | # order dependency and want to debug it, you can fix the order by providing
14 | # the seed, which is printed after each run.
15 | # --seed 1234
16 | config.order = 'random'
17 | end
18 |
19 | require_relative '../lib/iif'
20 | require_relative '../lib/ofx'
21 | require_relative '../lib/stripe-iiftoqbo'
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/spec/sample-data/with-transaction.ofx:
--------------------------------------------------------------------------------
1 | OFXHEADER:100
2 | DATA:OFXSGML
3 | VERSION:103
4 | SECURITY:NONE
5 | ENCODING:USASCII
6 | CHARSET:1252
7 | COMPRESSION:NONE
8 | OLDFILEUID:NONE
9 | NEWFILEUID:NONE
10 |
11 | 0INFO20140211000000ENGStripe0000INFOUSD123456789TestCHECKING2014010120140201CREDIT20140101+100.23TestNameMemo memo0.020140201
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 | -----------
3 |
4 | Copyright (c) 2014, Gilman Tolle
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
--------------------------------------------------------------------------------
/spec/stripe-iiftoqbo_spec.rb:
--------------------------------------------------------------------------------
1 | describe StripeIIFToQBO do
2 | empty_qbo = File.read(File.dirname(__FILE__) + "/sample-data/empty.qbo")
3 | empty_csv = File.read(File.dirname(__FILE__) + "/sample-data/empty.csv")
4 | basic_iif = File.read(File.dirname(__FILE__) + "/sample-data/basic.iif")
5 | basic_qbo = File.read(File.dirname(__FILE__) + "/sample-data/basic.qbo")
6 | basic_csv = File.read(File.dirname(__FILE__) + "/sample-data/basic.csv")
7 |
8 | it "should create an empty QBO file" do
9 | iiftoqbo = StripeIIFToQBO::Converter.new( :server_time => Date.new(2014,2,11) )
10 | qbo = iiftoqbo.to_qbo
11 | expect( qbo ).to eq(empty_qbo)
12 | end
13 |
14 | it "should create an empty CSV file" do
15 | iiftoqbo = StripeIIFToQBO::Converter.new( :server_time => Date.new(2014,2,11) )
16 | csv = iiftoqbo.to_csv
17 | expect( csv ).to eq(empty_csv)
18 | end
19 |
20 | it "should create a basic QBO file" do
21 | iiftoqbo = StripeIIFToQBO::Converter.new( :server_time => Date.new(2014,2,11), :iif_file => basic_iif )
22 | qbo = iiftoqbo.to_qbo
23 | expect( qbo ).to eq(basic_qbo)
24 | end
25 |
26 | it "should create a basic CSV file" do
27 | iiftoqbo = StripeIIFToQBO::Converter.new( :server_time => Date.new(2014,2,11), :iif_file => basic_iif )
28 | csv = iiftoqbo.to_csv
29 | expect( csv ).to eq(basic_csv)
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/sample-data/basic.csv:
--------------------------------------------------------------------------------
1 | Date,Name,Account,Memo,Amount
2 | 01/23/2014,Nicole Chiu-Wang,Stripe Third-party Account,CHECK Transfer from Stripe: tr_3MCmCnHFyYXFB5,-135.15
3 | 01/23/2014,Stripe,Stripe Payment Processing Fees,GENERAL JOURNAL Fees for Transfer from Stripe: tr_3MCmCnHFyYXFB5,-0.25
4 | 01/23/2014,Transfer to Stripe Checking Account,Stripe Checking Account,DEPOSIT Transfer from Stripe: tr_3MCmQsVsNuIX7J,-85.0
5 | 01/22/2014,Credit Card Charge,Stripe Sales,GENERAL JOURNAL Charge ID: ch_3M1PskZ2rglde3 | Description: Styling session between stylist Gloria McGee and client Emily Crozier,249.0
6 | 01/22/2014,Stripe,Stripe Payment Processing Fees,GENERAL JOURNAL Fees for charge ID: ch_3M1PskZ2rglde3,-7.52
7 | 07/10/2013,Credit Card Refund,Stripe Returns,GENERAL JOURNAL Refund of charge ch_2AaUvVOXTJxqgU,-60.0
8 | 07/10/2013,Stripe,Stripe Payment Processing Fees,GENERAL JOURNAL Refund of fees for ch_2AaUvVOXTJxqgU,2.04
9 | 07/10/2013,Credit Card Charge,Stripe Sales,GENERAL JOURNAL Charge ID: ch_2AaUVt4SQVF5mE | Description: Styling session between stylist Ellen Kyle and client Stephanie Ann,60.0
10 | 07/10/2013,Stripe,Stripe Payment Processing Fees,GENERAL JOURNAL Fees for charge ID: ch_2AaUVt4SQVF5mE,-2.04
11 | 12/10/2012,Transfer to Stripe Checking Account,Stripe Checking Account,DEPOSIT Transfer from Stripe: ach_0qt6m5dlIL2XMq,-15.0
12 | 12/03/2012,Stripe Connect Charge,Stripe Sales,GENERAL JOURNAL Stripe Connect fee for transaction ID: ch_0qbUpzmgBi6WVJ,15.0
13 |
--------------------------------------------------------------------------------
/bin/stripe-iiftoqbo:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'optparse'
4 | require 'bigdecimal'
5 | require 'csv'
6 | require 'pp'
7 |
8 | require 'stripe-iiftoqbo'
9 |
10 | account_id = nil
11 | iif_file = nil
12 | dump_csv = false
13 | payments_file = nil
14 | transfers_file = nil
15 |
16 | optparser = OptionParser.new do |opts|
17 | executable_name = File.split($0)[1]
18 | opts.banner = <<-EOS
19 |
20 | Usage: #{executable_name} [-p PAYMENTS_CSV_FILE] [-t TRANSFERS_CSV_FILE] [-c] ACCOUNT_NAME IIF_FILE
21 |
22 | Stripe-iiftoqbo converts an .iif file of your live transaction data
23 | exported from Stripe into a .qbo file that can be imported into
24 | QuickBooks Online.
25 |
26 | ACCOUNT_NAME : the name of your Stripe account to be included into your .qbo file (required)
27 | IIF_FILE : the .iif file to convert (required)
28 |
29 | EOS
30 |
31 | opts.on('-p', '--payments [PAYMENTS_CSV_FILE]', "Populate .qbo charge memo using memo field from a Stripe Payments CSV export") do |filename|
32 | payments_file = filename
33 | end
34 |
35 | opts.on('-t', '--transfers [TRANSFERS_CSV_FILE]', "Populate .qbo transfer memo using memo field from a Stripe Transfers CSV export") do |filename|
36 | transfers_file = filename
37 | end
38 |
39 | opts.on('-c', '--csv', "Output CSV of transactions instead of QBO, for debugging and analysis") do
40 | dump_csv = true
41 | end
42 | end
43 |
44 | optparser.parse!
45 |
46 | if ARGV.length < 2
47 | puts optparser
48 | exit(-1)
49 | end
50 |
51 | iiftoqbo = StripeIIFToQBO::Converter.new( :account_id => ARGV[0],
52 | :iif_file => ARGV[1],
53 | :payments_file => payments_file,
54 | :transfers_file => transfers_file )
55 | if dump_csv
56 | puts iiftoqbo.to_csv
57 | else
58 | puts iiftoqbo.to_qbo
59 | end
60 |
--------------------------------------------------------------------------------
/spec/ofx_spec.rb:
--------------------------------------------------------------------------------
1 | describe OFX do
2 | empty_ofx = File.read(File.dirname(__FILE__) + "/sample-data/empty.ofx")
3 | ofx_with_info = File.read(File.dirname(__FILE__) + "/sample-data/with-bank-info.ofx")
4 | ofx_with_transaction = File.read(File.dirname(__FILE__) + "/sample-data/with-transaction.ofx")
5 |
6 | it "should generate empty OFX files" do
7 | ofx_builder = OFX::Builder.new do |ofx|
8 | ofx.dtserver = Date.new(2014,2,11)
9 | end
10 |
11 | ofx = ofx_builder.to_ofx
12 | expect( ofx ).to eq(empty_ofx)
13 | end
14 |
15 | it "should generate OFX files with bank info but no transactions" do
16 | ofx_builder = OFX::Builder.new do |ofx|
17 | ofx.dtserver = Date.new(2014,2,11)
18 | ofx.fi_org = "Stripe"
19 | ofx.fi_fid = "0"
20 | ofx.bank_id = "123456789"
21 | ofx.acct_id = "Test"
22 | ofx.acct_type = "CHECKING"
23 | ofx.dtstart = Date.new(2014,1,1)
24 | ofx.dtend = Date.new(2014,2,1)
25 | ofx.bal_amt = 0
26 | ofx.dtasof = Date.new(2014,2,1)
27 | end
28 |
29 | ofx = ofx_builder.to_ofx
30 | expect( ofx ).to eq(ofx_with_info)
31 | end
32 |
33 |
34 | it "should generate OFX files with transactions" do
35 | ofx_builder = OFX::Builder.new do |ofx|
36 | ofx.dtserver = Date.new(2014,2,11)
37 | ofx.fi_org = "Stripe"
38 | ofx.fi_fid = "0"
39 | ofx.bank_id = "123456789"
40 | ofx.acct_id = "Test"
41 | ofx.acct_type = "CHECKING"
42 | ofx.dtstart = Date.new(2014,1,1)
43 | ofx.dtend = Date.new(2014,2,1)
44 | ofx.bal_amt = 0
45 | ofx.dtasof = Date.new(2014,2,1)
46 | end
47 |
48 | ofx_builder.transaction do |ofx_tr|
49 | ofx_tr.dtposted = Date.new(2014,1,1)
50 | ofx_tr.trnamt = "100.23"
51 | ofx_tr.fitid = "Test"
52 | ofx_tr.name = "Name"
53 | ofx_tr.memo = "Memo memo"
54 | end
55 |
56 | ofx = ofx_builder.to_ofx
57 | expect( ofx ).to eq(ofx_with_transaction)
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/spec/sample-data/basic.iif:
--------------------------------------------------------------------------------
1 | !TRNS TRNSID TRNSTYPE DATE ACCNT AMOUNT MEMO NAME
2 | !SPL TRNSID TRNSTYPE DATE ACCNT AMOUNT MEMO NAME
3 | !ENDTRNS
4 | TRNS CHECK 01/23/2014 Stripe Account -135.15 Transfer from Stripe: tr_3MCmCnHFyYXFB5 Nicole Chiu-Wang
5 | SPL CHECK 01/23/2014 Stripe Third-party Account 135.15 Transfer from Stripe: tr_3MCmCnHFyYXFB5 Nicole Chiu-Wang
6 | ENDTRNS
7 | TRNS GENERAL JOURNAL 01/23/2014 Stripe Account -0.25 Fees for Transfer from Stripe: tr_3MCmCnHFyYXFB5
8 | SPL GENERAL JOURNAL 01/23/2014 Stripe Payment Processing Fees 0.25 Fees for Transfer from Stripe: tr_3MCmCnHFyYXFB5
9 | ENDTRNS
10 | TRNS DEPOSIT 01/23/2014 Stripe Checking Account 85.00 Transfer from Stripe: tr_3MCmQsVsNuIX7J
11 | SPL DEPOSIT 01/23/2014 Stripe Account -85.00 Transfer from Stripe: tr_3MCmQsVsNuIX7J
12 | ENDTRNS
13 | TRNS GENERAL JOURNAL 01/22/2014 Stripe Sales -249.00 Charge ID: ch_3M1PskZ2rglde3 | Description: Styling session between stylist Gloria McGee and client Emily Crozier
14 | SPL GENERAL JOURNAL 01/22/2014 Stripe Payment Processing Fees 7.52 Fees for charge ID: ch_3M1PskZ2rglde3
15 | SPL GENERAL JOURNAL 01/22/2014 Stripe Account 241.48 Net for charge ID: ch_3M1PskZ2rglde3
16 | ENDTRNS
17 | TRNS GENERAL JOURNAL 07/10/2013 Stripe Returns 60.00 Refund of charge ch_2AaUvVOXTJxqgU
18 | SPL GENERAL JOURNAL 07/10/2013 Stripe Account -57.96 Refund for refunded charge ID: ch_2AaUvVOXTJxqgU
19 | SPL GENERAL JOURNAL 07/10/2013 Stripe Payment Processing Fees -2.04 Refund of fees for ch_2AaUvVOXTJxqgU
20 | ENDTRNS
21 | TRNS GENERAL JOURNAL 07/10/2013 Stripe Sales -60.00 Charge ID: ch_2AaUVt4SQVF5mE | Description: Styling session between stylist Ellen Kyle and client Stephanie Ann
22 | SPL GENERAL JOURNAL 07/10/2013 Stripe Payment Processing Fees 2.04 Fees for charge ID: ch_2AaUVt4SQVF5mE
23 | SPL GENERAL JOURNAL 07/10/2013 Stripe Account 57.96 Net for charge ID: ch_2AaUVt4SQVF5mE
24 | ENDTRNS
25 | TRNS DEPOSIT 12/10/2012 Stripe Checking Account 15.00 Transfer from Stripe: ach_0qt6m5dlIL2XMq
26 | SPL DEPOSIT 12/10/2012 Stripe Account -15.00 Transfer from Stripe: ach_0qt6m5dlIL2XMq
27 | ENDTRNS
28 | TRNS GENERAL JOURNAL 12/03/2012 Stripe Sales -15.00 Stripe Connect fee for transaction ID: ch_0qbUpzmgBi6WVJ
29 | SPL GENERAL JOURNAL 12/03/2012 Stripe Account 15.00 Stripe Connect fee for transaction ID: ch_0qbUpzmgBi6WVJ
30 | ENDTRNS
31 |
--------------------------------------------------------------------------------
/lib/iif/parser.rb:
--------------------------------------------------------------------------------
1 | require 'bigdecimal'
2 |
3 | require_relative 'transaction'
4 | require_relative 'entry'
5 |
6 | module IIF
7 | class Parser
8 | attr_accessor :definitions
9 | attr_accessor :entries
10 | attr_accessor :transactions
11 |
12 | def initialize(resource)
13 | @definitions = {}
14 | @entries = []
15 | @transactions = []
16 |
17 | resource = open_resource(resource)
18 | resource.rewind
19 | parse_file(resource)
20 | create_transactions
21 | end
22 |
23 | def open_resource(resource)
24 | if resource.respond_to?(:read)
25 | resource
26 | else
27 | open(resource)
28 | end
29 | rescue Exception
30 | StringIO.new(resource)
31 | end
32 |
33 | def parse_file(resource)
34 | resource.each_line do |line|
35 | fields = line.strip.split(/\t/)
36 | if fields[0][0] == '!'
37 | parse_definition(fields)
38 | else
39 | parse_data(fields)
40 | end
41 | end
42 | end
43 |
44 | def parse_definition(fields)
45 | key = fields[0][1..-1]
46 | values = fields[1..-1]
47 | @definitions[key] = values.map { |v| v.downcase }
48 | end
49 |
50 | def parse_data(fields)
51 | definition = @definitions[fields[0]]
52 |
53 | entry = Entry.new
54 | entry.type = fields[0]
55 |
56 | fields[1..-1].each_with_index do |field, idx|
57 | entry.send(definition[idx] + "=", field)
58 | end
59 |
60 | entry.amount = BigDecimal.new(entry.amount) if entry.amount
61 | entry.date = Date.strptime(entry.date, "%m/%d/%Y") if entry.date
62 |
63 | @entries.push(entry)
64 | end
65 |
66 | def create_transactions
67 | transaction = nil
68 | in_transaction = false
69 |
70 | @entries.each do |entry|
71 |
72 | case entry.type
73 |
74 | when "TRNS"
75 | if in_transaction
76 | @transactions.push(transaction)
77 | in_transaction = false
78 | end
79 | transaction = Transaction.new
80 | in_transaction = true
81 |
82 | when "ENDTRNS"
83 | @transactions.push(transaction)
84 | in_transaction = false
85 |
86 | end
87 |
88 | transaction.entries.push(entry) if in_transaction
89 | end
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/spec/iif_spec.rb:
--------------------------------------------------------------------------------
1 | describe IIF do
2 |
3 | empty_iif = File.read(File.dirname(__FILE__) + "/sample-data/empty.iif")
4 | basic_iif = File.read(File.dirname(__FILE__) + "/sample-data/basic.iif")
5 |
6 | it "should parse empty IIF files" do
7 | iif = IIF( empty_iif )
8 | expect( iif.transactions.length ).to eq(0)
9 | end
10 |
11 | it "should parse basic IIF files" do
12 | iif = IIF( basic_iif )
13 | expect( iif.transactions.length ).to eq(8)
14 | first_transaction = iif.transactions[0]
15 | expect( first_transaction.entries.length ).to eq(2)
16 | first_entry = first_transaction.entries.first
17 |
18 | expect( first_entry.type ).to eq("TRNS")
19 | expect( first_entry.trnstype ).to eq("CHECK")
20 | expect( first_entry.date ).to eq(Date.new(2014,1,23))
21 | expect( first_entry.accnt ).to eq("Stripe Account")
22 | expect( first_entry.amount ).to eq(-135.15)
23 | expect( first_entry.memo ).to eq("Transfer from Stripe: tr_3MCmCnHFyYXFB5")
24 |
25 | second_entry = first_transaction.entries[1]
26 |
27 | expect( second_entry.type ).to eq("SPL")
28 | expect( second_entry.trnstype ).to eq("CHECK")
29 | expect( second_entry.date ).to eq(Date.new(2014,1,23))
30 | expect( second_entry.accnt ).to eq("Stripe Third-party Account")
31 | expect( second_entry.amount ).to eq(135.15)
32 | expect( second_entry.memo ).to eq("Transfer from Stripe: tr_3MCmCnHFyYXFB5")
33 | expect( second_entry.name ).to eq("Nicole Chiu-Wang")
34 |
35 | second_transaction = iif.transactions[1]
36 |
37 | first_entry = second_transaction.entries[0]
38 |
39 | expect( first_entry.type ).to eq("TRNS")
40 | expect( first_entry.trnstype ).to eq("GENERAL JOURNAL")
41 | expect( first_entry.date ).to eq(Date.new(2014,1,23))
42 | expect( first_entry.accnt ).to eq("Stripe Account")
43 | expect( first_entry.amount ).to eq(-0.25)
44 | expect( first_entry.memo ).to eq("Fees for Transfer from Stripe: tr_3MCmCnHFyYXFB5")
45 |
46 | second_entry = second_transaction.entries[1]
47 |
48 | expect( second_entry.type ).to eq("SPL")
49 | expect( second_entry.trnstype ).to eq("GENERAL JOURNAL")
50 | expect( second_entry.date ).to eq(Date.new(2014,1,23))
51 | expect( second_entry.accnt ).to eq("Stripe Payment Processing Fees")
52 | expect( second_entry.amount ).to eq(0.25)
53 | expect( second_entry.memo ).to eq("Fees for Transfer from Stripe: tr_3MCmCnHFyYXFB5")
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/spec/sample-data/basic.qbo:
--------------------------------------------------------------------------------
1 | OFXHEADER:100
2 | DATA:OFXSGML
3 | VERSION:103
4 | SECURITY:NONE
5 | ENCODING:USASCII
6 | CHARSET:1252
7 | COMPRESSION:NONE
8 | OLDFILEUID:NONE
9 | NEWFILEUID:NONE
10 |
11 | 0INFO20140211000000ENGStripe0000INFOUSD123456789CHECKING2012120320140123DEBIT20140123-135.15Transfer from Stripe: tr_3MCmCnHFyYXFB5Nicole Chiu-WangTransfer from Stripe: tr_3MCmCnHFyYXFB5DEBIT20140123-0.25Fees for Transfer from Stripe: tr_3MCmCnHFyYXFB5StripeFees for Transfer from Stripe: tr_3MCmCnHFyYXFB5DEBIT20140123-85.0Transfer from Stripe: tr_3MCmQsVsNuIX7JTransfer to Stripe Checking AccountTransfer from Stripe: tr_3MCmQsVsNuIX7JCREDIT20140122+249.0Charge ID: ch_3M1PskZ2rglde3 | Description: Styling session between stylist Gloria McGee and client Emily CrozierCredit Card ChargeCharge ID: ch_3M1PskZ2rglde3 | Description: Styling session between stylist Gloria McGee and client Emily CrozierDEBIT20140122-7.52Fees for charge ID: ch_3M1PskZ2rglde3StripeFees for charge ID: ch_3M1PskZ2rglde3DEBIT20130710-60.0Refund of charge ch_2AaUvVOXTJxqgUCredit Card RefundRefund of charge ch_2AaUvVOXTJxqgUCREDIT20130710+2.04Refund of fees for ch_2AaUvVOXTJxqgUStripeRefund of fees for ch_2AaUvVOXTJxqgUCREDIT20130710+60.0Charge ID: ch_2AaUVt4SQVF5mE | Description: Styling session between stylist Ellen Kyle and client Stephanie AnnCredit Card ChargeCharge ID: ch_2AaUVt4SQVF5mE | Description: Styling session between stylist Ellen Kyle and client Stephanie AnnDEBIT20130710-2.04Fees for charge ID: ch_2AaUVt4SQVF5mEStripeFees for charge ID: ch_2AaUVt4SQVF5mEDEBIT20121210-15.0Transfer from Stripe: ach_0qt6m5dlIL2XMqTransfer to Stripe Checking AccountTransfer from Stripe: ach_0qt6m5dlIL2XMqCREDIT20121203+15.0Stripe Connect fee for transaction ID: ch_0qbUpzmgBi6WVJStripe Connect ChargeStripe Connect fee for transaction ID: ch_0qbUpzmgBi6WVJ0.020140123
12 |
--------------------------------------------------------------------------------
/lib/ofx.rb:
--------------------------------------------------------------------------------
1 | require "nokogiri"
2 |
3 | require "ofx/transaction"
4 |
5 | module OFX
6 | class Builder
7 |
8 | attr_accessor :fi_org
9 | attr_accessor :fi_fid
10 | attr_accessor :dtserver
11 |
12 | attr_accessor :bank_id
13 | attr_accessor :acct_id
14 | attr_accessor :acct_type # CHECKING, SAVINGS, MONEYMRKT, CREDITLINE
15 |
16 | attr_accessor :dtstart
17 | attr_accessor :dtend
18 |
19 | attr_accessor :transactions
20 |
21 | attr_accessor :bal_amt
22 | attr_accessor :dtasof
23 |
24 | def initialize(&block)
25 | @headers = [
26 | [ "OFXHEADER", "100" ],
27 | [ "DATA", "OFXSGML" ],
28 | [ "VERSION", "103" ],
29 | [ "SECURITY", "NONE" ],
30 | [ "ENCODING", "USASCII" ],
31 | [ "CHARSET", "1252" ],
32 | [ "COMPRESSION", "NONE" ],
33 | [ "OLDFILEUID", "NONE" ],
34 | [ "NEWFILEUID", "NONE" ]
35 | ]
36 | @transactions = []
37 | self.dtserver = Date.today
38 | if block_given?
39 | yield self
40 | end
41 | end
42 |
43 | def bal_amt=(amt)
44 | @bal_amt = BigDecimal.new(amt)
45 | end
46 |
47 | def transaction(&block)
48 | transaction = OFX::Transaction.new
49 | yield transaction
50 | self.transactions.push transaction
51 | end
52 |
53 | def to_ofx
54 | print_headers +
55 | print_body
56 | end
57 |
58 | def print_headers
59 | @headers.map { |key, value| "#{key}:#{value}" }.join("\n") + "\n\n"
60 | end
61 |
62 | def print_body
63 | builder = Nokogiri::XML::Builder.new do |xml|
64 | xml.OFX {
65 | xml.SIGNONMSGSRSV1 {
66 | xml.SONRS {
67 | xml.STATUS {
68 | xml.CODE "0"
69 | xml.SEVERITY "INFO"
70 | }
71 | xml.DTSERVER format_datetime(self.dtserver)
72 | xml.LANGUAGE "ENG"
73 | xml.FI {
74 | xml.ORG self.fi_org
75 | xml.FID self.fi_fid
76 | }
77 | xml.send "INTU.BID", self.fi_fid
78 | }
79 | }
80 | xml.BANKMSGSRSV1 {
81 | xml.STMTTRNRS {
82 | xml.TRNUID "0"
83 | xml.STATUS {
84 | xml.CODE "0"
85 | xml.SEVERITY "INFO"
86 | }
87 | xml.STMTRS {
88 | xml.CURDEF "USD"
89 | xml.BANKACCTFROM {
90 | xml.BANKID self.bank_id
91 | xml.ACCTID self.acct_id
92 | xml.ACCTTYPE self.acct_type
93 | }
94 | xml.BANKTRANLIST {
95 | if self.dtstart
96 | xml.DTSTART format_date(self.dtstart)
97 | end
98 | if self.dtend
99 | xml.DTEND format_date(self.dtend)
100 | end
101 | self.transactions.each do |transaction|
102 | xml.STMTTRN {
103 | xml.TRNTYPE format_trntype(transaction.trnamt)
104 | xml.DTPOSTED format_date(transaction.dtposted)
105 | xml.TRNAMT format_amount(transaction.trnamt)
106 | xml.FITID transaction.fitid
107 | xml.NAME transaction.name
108 | xml.MEMO transaction.memo
109 | }
110 | end
111 | }
112 | xml.LEDGERBAL {
113 | if self.bal_amt
114 | xml.BALAMT format_balance(self.bal_amt)
115 | end
116 | if self.dtasof
117 | xml.DTASOF format_date(self.dtasof)
118 | end
119 | }
120 | }
121 | }
122 | }
123 | }
124 | end
125 | builder.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION)
126 | end
127 |
128 | def format_datetime(time)
129 | time.strftime("%Y%m%d000000")
130 | end
131 |
132 | def format_date(time)
133 | time.strftime("%Y%m%d")
134 | end
135 |
136 | def format_amount(amount)
137 | if amount > 0
138 | "+#{amount.to_s('F')}"
139 | else
140 | "#{amount.to_s('F')}"
141 | end
142 | end
143 |
144 | def format_trntype(amount)
145 | if amount > 0
146 | "CREDIT"
147 | else
148 | "DEBIT"
149 | end
150 | end
151 |
152 | def format_balance(balance)
153 | balance.to_s('F')
154 | end
155 | end
156 | end
157 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Stripe IIF to QuickBooks Online QBO File Translator
2 |
3 | Does your company use Stripe to charge credit cards?
4 |
5 | Does your company use QuickBooks Online for accounting?
6 |
7 | Did you export your full transaction history from Stripe as an .IIF file?
8 |
9 | Did you hope to import that transaction history into QuickBooks Online to do your taxes?
10 |
11 | Did you realize that QBO doesn't support importing data from .IIF files, which is what Stripe gave you?
12 |
13 | Then this tool might be helpful.
14 |
15 | Stripe-iiftoqbo takes an IIF file and produces a QBO file that you can import into QuickBooks Online.
16 |
17 | It's been tested with:
18 |
19 | * Credit Card Payments
20 | * Subscription Credit Card Payments
21 | * Credit Card Refunds
22 | * Transfers to a Company Bank Account
23 | * Payouts to Third Parties
24 | * Stripe Connect Fees Collected
25 | * Stripe Connect Fees Refunded
26 | * Stripe Transaction Fees
27 |
28 | You can use the Stripe data to come up with top-line revenue numbers from credit card payments and cost of goods sold if you're a marketplace with payouts. You can also double-check that all the transfers from Stripe sync up with transfers into your bank account.
29 |
30 | ## Usage
31 |
32 | Usage: stripe-iiftoqbo [-p PAYMENTS_CSV_FILE] [-t TRANSFERS_CSV_FILE] [-c] ACCOUNT_NAME IIF_FILE
33 |
34 | ## Quickstart
35 |
36 | First, get your IIF file from Stripe.
37 |
38 | Go to your Stripe Dashboard, then your account menu, then Account Settings. Go to the Data tab, and hit "Export to Quickbooks". Choose your date range (e.g. all of 2013), and hit Export to IIF.
39 |
40 | Double-check your file to make sure it's an .IIF.
41 |
42 | $ head balance_history.iif
43 | !TRNS TRNSID TRNSTYPE DATE ACCNT AMOUNT MEMO
44 | !SPL TRNSID TRNSTYPE DATE ACCNT AMOUNT MEMO
45 | !ENDTRNS
46 | TRNS PAYOUT (FIRST LAST) 01/23/2014 Third-party Account 135.15 Transfer from Stripe: tr_3MCmCnHFyYXFB5
47 | SPL PAYOUT (FIRST LAST) 01/23/2014 Stripe Account -135.4 Transfer from Stripe: tr_3MCmCnHFyYXFB5
48 | SPL GENERAL JOURNAL 01/23/2014 Stripe Payment Processing Fees 0.25 Fees for transfer ID: tr_3MCmCnHFyYXFB5
49 | ENDTRNS
50 |
51 | Pick a name for your company (e.g. MyCompanyName). It's going to be saved into the QBO file. I'd recommend using your Stripe Account Name.
52 |
53 | Then, run the app:
54 |
55 | ruby -Ilib bin/stripe-iiftoqbo MyCompanyName balance_history.iif > balance_history.qbo
56 |
57 | You'll get a nice .QBO file with all of your transactions.
58 |
59 | Now, go to QuickBooks Online. Go to the 'gear' menu and hit 'Chart of Accounts'. Create a new 'Bank' > 'Checking' account and call it 'Stripe'.
60 |
61 | Now go to the Transactions > Banking page. Hit the dropdown arrow next to Update and choose 'File Upload'.
62 |
63 | Upload your new .QBO file. You should see the company name you specified, and a date range from the IIF file. Choose your 'Stripe' account from the QuickBooks Online dropdown.
64 |
65 | You should see a line item for each:
66 |
67 | * Credit Card Charge you collected
68 | * Stripe Fee you paid on that charge
69 | * Transfer to your checking account
70 | * Transfer to a third-party using Stripe Payounts
71 | * Stripe Fee you paid on that transfer
72 | * Stripe Connect Fee you collected
73 | * Credit Card Charge that was refunded
74 | * Refund of Stripe Fee for that charge
75 |
76 | Categorize and accept each transaction. Now you can see how much you made, how much you paid to Stripe, and how much you paid to your vendors.
77 |
78 | ## Extras
79 |
80 | If you want to merge the description for each payment into the 'memo' field of your QBO file so you can see them in QuickBooks, go to the Stripe Payments page and export your payments as a CSV. It'll look like this:
81 |
82 | $ head payments.csv
83 | id,Description,Created,Amount,Amount Refunded,Currency,Converted Amount,Converted Amount Refunded,Fee,Converted Currency,Mode,Status,Customer ID,Customer Description,Customer Email,Captured,Card Last4,Card Type,Card Exp Month,Card Exp Year,Card Name,Card Address Line1,Card Address Line2,Card Address City,Card Address State,Card Address Country,Card Address Zip,Card Issue Country,Card Fingerprint,Card CVC Status,Card AVS Zip Status,Card AVS Line1 Status,Dispute Status
84 | ch_3QSlijsVumgdnQ,,2014-02-03 01:47,19.00,0.00,usd,19.00,0.00,0.85,usd,Live,Paid,cus_29a92b191,test@test.com,,true,1111,Visa,1,2016,,,,,,,,US,ztg6Hv5g3sbjBE57,,,,
85 | ch_3PimG0oAu3LRSg,Test Description To Merge,2014-02-01 02:16,249.00,0.00,usd,249.00,0.00,7.52,usd,Live,Paid,cus_292ab3c2b,test@test.com,,true,2222,Visa,1,2016,,,,,,,,US,2Xv5QDDhdKj23Z0l,,,,
86 |
87 | Then, run the tool again with ```-p payments.csv```. For each charge in the IIF, if there's a matching Charge ID in the payments file, the tool will merge it into the QBO memo.
88 |
89 | You can also export your Transfers and then merge descriptions using ```-t transfers.csv```.
90 |
91 | If you want to inspect the transactions from your .IIF file in CSV format (using Excel, for example), give the '-c' flag. It'll dump CSV instead of QBO.
92 |
93 | ## License
94 |
95 | New MIT License - Copyright (c) 2014 Gilman Tolle
96 |
97 | See LICENSE for details
98 |
--------------------------------------------------------------------------------
/lib/stripe-iiftoqbo.rb:
--------------------------------------------------------------------------------
1 | require 'csv'
2 | require_relative 'iif'
3 | require_relative 'ofx'
4 |
5 | module StripeIIFToQBO
6 | class Converter
7 | def initialize( options={} )
8 | @account_id = options[:account_id] if options[:account_id]
9 | @iif_file = options[:iif_file] if options[:iif_file]
10 | @payments_file = options[:payments_file] if options[:payments_file]
11 | @transfers_file = options[:transfers_file] if options[:transfers_file]
12 | @server_time = options[:server_time] || Date.today
13 |
14 | load_payments_file(@payments_file)
15 | load_transfers_file(@transfers_file)
16 | load_iif_file(@iif_file)
17 | end
18 |
19 | def load_payments_file(payments_file)
20 | @payments = {}
21 |
22 | if payments_file
23 | CSV.foreach(payments_file, :headers => true, :encoding => 'windows-1251:utf-8') do |row|
24 | @payments[row["id"]] = row["Description"] || ""
25 | end
26 | end
27 | end
28 |
29 | def load_transfers_file(transfers_file)
30 | @transfers = {}
31 |
32 | if transfers_file
33 | CSV.foreach(transfers_file, :headers => true, :encoding => 'windows-1251:utf-8') do |row|
34 | @transfers[row["ID"]] = row["Description"] || ""
35 | end
36 | end
37 | end
38 |
39 | def load_iif_file(iif_file)
40 | @ofx_entries = []
41 |
42 | if iif_file
43 | IIF(iif_file) do |iif|
44 | iif.transactions.each do |transaction|
45 | transaction.entries.each do |iif_entry|
46 | ofx_entry = convert_iif_entry_to_ofx(iif_entry)
47 | if ofx_entry
48 | @ofx_entries.push( ofx_entry )
49 | end
50 | end
51 | end
52 | end
53 | end
54 | end
55 |
56 | def convert_iif_entry_to_ofx(iif_entry)
57 | ofx_entry = {}
58 |
59 | ofx_entry[:date] = iif_entry.date
60 | ofx_entry[:fitid] = iif_entry.memo
61 | ofx_entry[:accnt] = iif_entry.accnt
62 | ofx_entry[:trnstype] = iif_entry.trnstype
63 | ofx_entry[:memo] = iif_entry.memo
64 |
65 | case iif_entry.accnt
66 |
67 | when "Stripe Third-party Account"
68 | ofx_entry[:amount] = -iif_entry.amount
69 | ofx_entry[:name] = iif_entry.name
70 |
71 | ofx_entry[:memo] =~ /Transfer from Stripe: (\S+)/
72 | transfer_id = $1
73 |
74 | if @transfers[transfer_id]
75 | ofx_entry[:memo] = "#{@transfers[transfer_id]} #{iif_entry.memo}"
76 | end
77 |
78 | when "Stripe Payment Processing Fees"
79 | ofx_entry[:amount] = -iif_entry.amount
80 | ofx_entry[:name] = "Stripe"
81 |
82 | when "Stripe Checking Account"
83 | ofx_entry[:amount] = -iif_entry.amount
84 | ofx_entry[:name] = "Transfer to #{iif_entry.accnt}"
85 |
86 | when "Stripe Sales"
87 | ofx_entry[:amount] = -iif_entry.amount
88 |
89 | if iif_entry.memo =~ /Stripe Connect fee/
90 | ofx_entry[:name] = "Stripe Connect Charge"
91 | elsif iif_entry.memo =~ /Charge/
92 | ofx_entry[:name] = "Credit Card Charge"
93 | else
94 | ofx_entry[:name] = iif_entry.accnt
95 | end
96 |
97 | ofx_entry[:memo] =~ /Charge ID: (\S+)/
98 | charge_id = $1
99 |
100 | if @payments[charge_id]
101 | ofx_entry[:memo] = "#{@payments[charge_id]} Charge ID: #{charge_id}"
102 | ofx_entry[:fitid] = charge_id
103 | end
104 |
105 | when "Stripe Returns"
106 | ofx_entry[:amount] = -iif_entry.amount
107 | ofx_entry[:name] = "Credit Card Refund"
108 |
109 | ofx_entry[:memo] =~ /Refund of charge (\S+)/
110 | charge_id = $1
111 |
112 | if @payments[charge_id]
113 | ofx_entry[:memo] = "#{@payments[charge_id]} Refund of Charge ID: #{charge_id}"
114 | end
115 |
116 | when "Stripe Account"
117 | return nil
118 | end
119 |
120 | return ofx_entry
121 | end
122 |
123 | def to_csv
124 | rows = []
125 | rows.push(["Date", "Name", "Account", "Memo", "Amount"].to_csv)
126 | @ofx_entries.each do |ofx_entry|
127 | rows.push([ ofx_entry[:date].strftime("%m/%d/%Y"), ofx_entry[:name], ofx_entry[:accnt], "#{ofx_entry[:trnstype]} #{ofx_entry[:memo]}", ofx_entry[:amount].to_s('F') ].to_csv)
128 | end
129 | return rows.join
130 | end
131 |
132 | def to_qbo
133 | min_date = nil
134 | max_date = nil
135 |
136 | @ofx_entries.each do |e|
137 | if e[:date]
138 | min_date = e[:date] if min_date.nil? or e[:date] < min_date
139 | max_date = e[:date] if max_date.nil? or e[:date] > max_date
140 | end
141 | end
142 |
143 | ofx_builder = OFX::Builder.new do |ofx|
144 | ofx.dtserver = @server_time
145 | ofx.fi_org = "Stripe"
146 | ofx.fi_fid = "0"
147 | ofx.bank_id = "123456789"
148 | ofx.acct_id = @account_id
149 | ofx.acct_type = "CHECKING"
150 | ofx.dtstart = min_date
151 | ofx.dtend = max_date
152 | ofx.bal_amt = 0
153 | ofx.dtasof = max_date
154 | end
155 |
156 | @ofx_entries.each do |ofx_entry|
157 | ofx_builder.transaction do |ofx_tr|
158 | ofx_tr.dtposted = ofx_entry[:date]
159 | ofx_tr.trnamt = ofx_entry[:amount]
160 | ofx_tr.fitid = ofx_entry[:fitid]
161 | ofx_tr.name = ofx_entry[:name]
162 | ofx_tr.memo = ofx_entry[:memo]
163 | end
164 | end
165 |
166 | return ofx_builder.to_ofx
167 | end
168 | end
169 | end
170 |
--------------------------------------------------------------------------------