├── 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 | --------------------------------------------------------------------------------