stripe2qb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +93 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config/.keep +0 -0
- data/config/stripe2qb.yml +36 -0
- data/lib/stripe2qb/configuration.rb +33 -0
- data/lib/stripe2qb/converters/base.rb +43 -0
- data/lib/stripe2qb/converters/charge_to_sales_receipt.rb +59 -0
- data/lib/stripe2qb/converters/refund_to_refund_receipt.rb +68 -0
- data/lib/stripe2qb/converters/transfer_to_deposit.rb +157 -0
- data/lib/stripe2qb/converters.rb +7 -0
- data/lib/stripe2qb/options_reading.rb +12 -0
- data/lib/stripe2qb/quickbooks_api/access_token.rb +36 -0
- data/lib/stripe2qb/quickbooks_api.rb +68 -0
- data/lib/stripe2qb/stripe_api.rb +40 -0
- data/lib/stripe2qb/version.rb +3 -0
- data/lib/stripe2qb.rb +11 -0
- data/stripe2qb.gemspec +36 -0
- metadata +141 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8987570f2160c419fbbe9dcac2c8d53addf95725
|
4
|
+
data.tar.gz: dedad6c810f7fb2773e5ee6cd55aa36ed63a6c47
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9fee3c5cff6cca199d44f772e7b9a3081adfd7b53d662f8f7709d3bb5267d02dce641a451a16d9ce30b089e02431c12c319faf2f5745680c4feac0f54d7ac83c
|
7
|
+
data.tar.gz: c7897d522c869f8bba578aa4fc7478f5cf3e88d4a40d871de43ded84fbf15add4c399cbf7c7e7957ad8e5105f1461dedf856be61e5315c9ae52a9c05732a0790
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, and in the interest of
|
4
|
+
fostering an open and welcoming community, we pledge to respect all people who
|
5
|
+
contribute through reporting issues, posting feature requests, updating
|
6
|
+
documentation, submitting pull requests or patches, and other activities.
|
7
|
+
|
8
|
+
We are committed to making participation in this project a harassment-free
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
11
|
+
body size, race, ethnicity, age, religion, or nationality.
|
12
|
+
|
13
|
+
Examples of unacceptable behavior by participants include:
|
14
|
+
|
15
|
+
* The use of sexualized language or imagery
|
16
|
+
* Personal attacks
|
17
|
+
* Trolling or insulting/derogatory comments
|
18
|
+
* Public or private harassment
|
19
|
+
* Publishing other's private information, such as physical or electronic
|
20
|
+
addresses, without explicit permission
|
21
|
+
* Other unethical or unprofessional conduct
|
22
|
+
|
23
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
24
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
25
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
26
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
27
|
+
threatening, offensive, or harmful.
|
28
|
+
|
29
|
+
By adopting this Code of Conduct, project maintainers commit themselves to
|
30
|
+
fairly and consistently applying these principles to every aspect of managing
|
31
|
+
this project. Project maintainers who do not follow or enforce the Code of
|
32
|
+
Conduct may be permanently removed from the project team.
|
33
|
+
|
34
|
+
This code of conduct applies both within project spaces and in public spaces
|
35
|
+
when an individual is representing the project or its community.
|
36
|
+
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
38
|
+
reported by contacting a project maintainer at anthony@anthonywang.com. All
|
39
|
+
complaints will be reviewed and investigated and will result in a response that
|
40
|
+
is deemed necessary and appropriate to the circumstances. Maintainers are
|
41
|
+
obligated to maintain confidentiality with regard to the reporter of an
|
42
|
+
incident.
|
43
|
+
|
44
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
45
|
+
version 1.3.0, available at
|
46
|
+
[http://contributor-covenant.org/version/1/3/0/][version]
|
47
|
+
|
48
|
+
[homepage]: http://contributor-covenant.org
|
49
|
+
[version]: http://contributor-covenant.org/version/1/3/0/
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Anthony Wang
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# Stripe2QB
|
2
|
+
|
3
|
+
[Stripe2QB](https://github.com/wangthony/stripe2qb) reads your Stripe
|
4
|
+
transactions and imports them into Quickbooks.
|
5
|
+
|
6
|
+
Stripe2QB is built on top of the [stripe](https://github.com/stripe/stripe-ruby)
|
7
|
+
and [quickbooks-ruby](https://github.com/ruckus/quickbooks-ruby) gems.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'stripe2qb'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install stripe2qb
|
24
|
+
|
25
|
+
## Configuration
|
26
|
+
|
27
|
+
The suggested way to configure Stripe2QB is by pointing it to a YAML file.
|
28
|
+
There is a template file at `config/stripe2qb.yml` that you can copy into your
|
29
|
+
application. See that file for information on configuration options.
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
s2qb = Stripe2QB.new # look for the default at config/stripe2qb.yml
|
33
|
+
|
34
|
+
s2qb = Stripe2QB.new('path/to/config.yml') # use a different config file path
|
35
|
+
|
36
|
+
s2qb = Stripe2QB.new(quickbooks_api: { ... }, stripe_api: { ... }) # use a Hash
|
37
|
+
```
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
The `#process` method looks for deposited (paid)
|
42
|
+
[Stripe Transfers](https://stripe.com/docs/api#transfers) on a certain date
|
43
|
+
(or date range), and converts them into
|
44
|
+
[Quickbooks Deposits](https://developer.intuit.com/docs/api/accounting/deposit).
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
s2qb = Stripe2QB.new
|
48
|
+
|
49
|
+
# process transfers on or after October 1st, 2016
|
50
|
+
s2qb.process('2016-10-01')
|
51
|
+
|
52
|
+
# process transfers in the month of October, 2016
|
53
|
+
s2qb.process('2016-10-01', '2016-11-01')
|
54
|
+
```
|
55
|
+
|
56
|
+
In addition to creating a Deposit, all
|
57
|
+
[Stripe Charges](https://stripe.com/docs/api#charges) associated with each
|
58
|
+
Transfer are converted into
|
59
|
+
[Quickbooks SalesReceipts](https://developer.intuit.com/docs/api/accounting/salesreceipt),
|
60
|
+
and all [Stripe Refunds](https://stripe.com/docs/api#refunds) associated with
|
61
|
+
each Transfer are converted into
|
62
|
+
[Quickbooks RefundReceipts](https://developer.intuit.com/docs/api/accounting/refundreceipt).
|
63
|
+
The SalesReceipts and RefundReceipts are linked to the corresponding Deposit.
|
64
|
+
|
65
|
+
Also, each Deposit will contain a line item for the Stripe processing fees and
|
66
|
+
fee refunds included in the Transfer.
|
67
|
+
|
68
|
+
You can safely re-process dates without creating duplicate records, as long as
|
69
|
+
you do not edit the `payment_ref_number` or `private_note` fields in records
|
70
|
+
created by Stripe2QB.
|
71
|
+
|
72
|
+
## Disclaimer
|
73
|
+
|
74
|
+
Even though this gem is used in production applications, and most of the API
|
75
|
+
operations involved are reversible / fixable, there may be cases where API calls
|
76
|
+
made by this gem have unforeseen consequences.
|
77
|
+
|
78
|
+
Therefore, you assume all liability for any unwanted changes to data caused by
|
79
|
+
use of this gem. The author and contributors assume no responsibility. **Use at your own risk.**
|
80
|
+
|
81
|
+
## Development
|
82
|
+
|
83
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
84
|
+
|
85
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
86
|
+
|
87
|
+
## Contributing
|
88
|
+
|
89
|
+
Bug reports and pull requests are welcome on GitHub. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
90
|
+
|
91
|
+
## License
|
92
|
+
|
93
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "stripe2qb"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/config/.keep
ADDED
File without changes
|
@@ -0,0 +1,36 @@
|
|
1
|
+
quickbooks_api:
|
2
|
+
# find realm ID at https://developer.intuit.com/v2/apiexplorer?apiname=V3QBO
|
3
|
+
realm_id: 'your Quickbooks API company ID'
|
4
|
+
|
5
|
+
# find OAuth consumer key & secret at https://developer.intuit.com/v2/ui#/app/dashboard
|
6
|
+
# (you have to sign up for a developer account and create an app first - see http://www.mooreds.com/wordpress/archives/1396)
|
7
|
+
oauth_consumer_key: 'your Quickbooks OAuth consumer key'
|
8
|
+
oauth_consumer_secret: 'your Quickbooks OAuth consumer secret'
|
9
|
+
|
10
|
+
# generate token & secret at https://appcenter.intuit.com/Playground/OAuth/IA
|
11
|
+
# (set the duration to 15552000 = 180 days * 24 * 60 * 60)
|
12
|
+
api_access_token: 'your Quickbooks API access token'
|
13
|
+
api_access_secret: 'your Quickbooks API access secret'
|
14
|
+
|
15
|
+
# name or Quickbooks ID of the Customer for Stripe orders
|
16
|
+
receipt_customer: 'Stripe Customer' # String name or integer ID
|
17
|
+
|
18
|
+
# name or Quickbooks ID of the Item (Product/Service) for Stripe orders
|
19
|
+
receipt_item: 'Stripe Sales' # String name or integer ID
|
20
|
+
|
21
|
+
# name or Quickbooks ID of the Payment Method for Stripe transfers
|
22
|
+
receipt_payment_method: 'Stripe' # String name or integer ID
|
23
|
+
|
24
|
+
# name or Quickbooks ID of the bank Account where Stripe transfers are deposited from
|
25
|
+
receipt_account: 'Undeposited Funds' # String name or integer ID
|
26
|
+
|
27
|
+
# name or Quickbooks ID of the bank Account where Stripe transfers are deposited to
|
28
|
+
deposit_account: 'Business Checking' # String name or integer ID
|
29
|
+
|
30
|
+
# name or Quickbooks ID of the Vendor that you want to assign Stripe transfers to
|
31
|
+
deposit_fees_vendor: 'Stripe Payments Platform' # String name or integer ID
|
32
|
+
|
33
|
+
# name or Quickbooks ID of the Account where you want to book Stripe processing fees
|
34
|
+
deposit_fees_account: 'Stripe Merchant Fees' # String name or integer ID
|
35
|
+
stripe_api:
|
36
|
+
api_key: 'your Stripe API key'
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'stripe2qb/options_reading'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Stripe2QB
|
5
|
+
class ConfigurationError < StandardError; end
|
6
|
+
|
7
|
+
class Configuration
|
8
|
+
include OptionsReading
|
9
|
+
|
10
|
+
attr_reader :quickbooks_api
|
11
|
+
attr_reader :stripe_api
|
12
|
+
|
13
|
+
def initialize(options)
|
14
|
+
if options.is_a?(String)
|
15
|
+
file = File.open(options)
|
16
|
+
options = YAML.load(file.read)
|
17
|
+
end
|
18
|
+
|
19
|
+
quickbooks_api_options = get_required_from_options('quickbooks_api', options)
|
20
|
+
@quickbooks_api = QuickbooksApi.new(quickbooks_api_options)
|
21
|
+
|
22
|
+
stripe_api_options = get_required_from_options('stripe_api', options)
|
23
|
+
@stripe_api = StripeApi.new(stripe_api_options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def process(start_date, end_date = nil)
|
27
|
+
transfers = stripe_api.get_transfers(start_date, end_date || Date.today)
|
28
|
+
transfers.each do |transfer|
|
29
|
+
Converters::TransferToDeposit.new(transfer, self).find_or_create
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Stripe2QB
|
2
|
+
module Converters
|
3
|
+
class Base
|
4
|
+
attr_reader :quickbooks_api
|
5
|
+
attr_reader :stripe_api
|
6
|
+
|
7
|
+
def initialize(configuration)
|
8
|
+
@quickbooks_api = configuration.quickbooks_api
|
9
|
+
@stripe_api = configuration.stripe_api
|
10
|
+
end
|
11
|
+
|
12
|
+
def find
|
13
|
+
raise 'not implemented'
|
14
|
+
end
|
15
|
+
|
16
|
+
def exists?
|
17
|
+
!find.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def create!
|
21
|
+
raise 'not implemented'
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_or_create
|
25
|
+
find || create!
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete!
|
29
|
+
raise 'not implemented'
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def format_date(value)
|
35
|
+
Time.at(value).in_time_zone('UTC').to_date.to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
def format_amount(value)
|
39
|
+
(value / 100.0).round(2)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Stripe2QB
|
2
|
+
module Converters
|
3
|
+
class ChargeToSalesReceipt < Base
|
4
|
+
attr_reader :charge
|
5
|
+
attr_reader :sales_receipt
|
6
|
+
|
7
|
+
def initialize(charge, configuration)
|
8
|
+
super(configuration)
|
9
|
+
|
10
|
+
@charge = charge
|
11
|
+
@sales_receipt = build_sales_receipt
|
12
|
+
end
|
13
|
+
|
14
|
+
def find
|
15
|
+
@found ||= quickbooks_api.sales_receipt_service.find_by(:payment_ref_num, charge.id).first
|
16
|
+
end
|
17
|
+
|
18
|
+
def create!
|
19
|
+
raise "SalesReceipt for #{charge.id} already exists: #{find.id}" if exists?
|
20
|
+
|
21
|
+
quickbooks_api.sales_receipt_service.create(sales_receipt)
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete!
|
25
|
+
return false unless exists?
|
26
|
+
|
27
|
+
result = quickbooks_api.sales_receipt_service.delete(find)
|
28
|
+
@found = nil
|
29
|
+
|
30
|
+
result
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def build_sales_receipt
|
36
|
+
sales_receipt = Quickbooks::Model::SalesReceipt.new
|
37
|
+
sales_receipt.customer_id = quickbooks_api.receipt_customer.id
|
38
|
+
sales_receipt.payment_method_id = quickbooks_api.receipt_payment_method.id
|
39
|
+
sales_receipt.deposit_to_account_id = quickbooks_api.receipt_account.id
|
40
|
+
sales_receipt.txn_date = format_date(charge.created)
|
41
|
+
sales_receipt.payment_ref_number = charge.id
|
42
|
+
sales_receipt.private_note = "Imported by Stripe2QB: #{charge.id}"
|
43
|
+
sales_receipt.auto_doc_number!
|
44
|
+
|
45
|
+
line_item = Quickbooks::Model::Line.new
|
46
|
+
line_item.amount = amount = format_amount(charge.amount)
|
47
|
+
line_item.description = charge.description
|
48
|
+
line_item.sales_item! do |detail|
|
49
|
+
detail.quantity = 1
|
50
|
+
detail.unit_price = amount
|
51
|
+
detail.item_id = quickbooks_api.receipt_item.id
|
52
|
+
end
|
53
|
+
sales_receipt.line_items << line_item
|
54
|
+
|
55
|
+
sales_receipt
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Stripe2QB
|
2
|
+
module Converters
|
3
|
+
class RefundToRefundReceipt < Base
|
4
|
+
attr_reader :refund
|
5
|
+
attr_reader :refund_receipt
|
6
|
+
|
7
|
+
def initialize(refund, configuration)
|
8
|
+
super(configuration)
|
9
|
+
|
10
|
+
@refund = refund
|
11
|
+
@refund_receipt = build_refund_receipt
|
12
|
+
end
|
13
|
+
|
14
|
+
def find
|
15
|
+
return @found if @found
|
16
|
+
|
17
|
+
# Quickbooks doesn't have payment_ref_number on RefundReceipts, so we
|
18
|
+
# have to search for a match on private_note...
|
19
|
+
# first narrow down results by txn_date
|
20
|
+
refund_receipts = quickbooks_api.refund_receipt_service.find_by(:txn_date, format_date(refund.created))
|
21
|
+
refund_receipts.each do |refund_receipt|
|
22
|
+
return @found = refund_receipt if refund_receipt.private_note =~ /#{refund.id}/
|
23
|
+
end
|
24
|
+
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def create!
|
29
|
+
raise "RefundReceipt for #{refund.id} already exists: #{find.id}" if exists?
|
30
|
+
|
31
|
+
quickbooks_api.refund_receipt_service.create(refund_receipt)
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete!
|
35
|
+
return false unless exists?
|
36
|
+
|
37
|
+
result = quickbooks_api.refund_receipt_service.delete(find)
|
38
|
+
@found = nil
|
39
|
+
|
40
|
+
result
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def build_refund_receipt
|
46
|
+
refund_receipt = Quickbooks::Model::RefundReceipt.new
|
47
|
+
refund_receipt.customer_id = quickbooks_api.receipt_customer.id
|
48
|
+
refund_receipt.payment_method_id = quickbooks_api.receipt_payment_method.id
|
49
|
+
refund_receipt.deposit_to_account_id = quickbooks_api.receipt_account.id
|
50
|
+
refund_receipt.txn_date = format_date(refund.created)
|
51
|
+
refund_receipt.private_note = "Imported by Stripe2QB: #{refund.id}"
|
52
|
+
refund_receipt.auto_doc_number!
|
53
|
+
|
54
|
+
line_item = Quickbooks::Model::Line.new
|
55
|
+
line_item.amount = amount = format_amount(-1 * refund.amount) # convert to positive number for RefundReceipt
|
56
|
+
line_item.description = refund.description
|
57
|
+
line_item.sales_item! do |detail|
|
58
|
+
detail.quantity = 1
|
59
|
+
detail.unit_price = amount
|
60
|
+
detail.item_id = quickbooks_api.receipt_item.id
|
61
|
+
end
|
62
|
+
refund_receipt.line_items << line_item
|
63
|
+
|
64
|
+
refund_receipt
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module Stripe2QB
|
2
|
+
module Converters
|
3
|
+
class TransferToDeposit < Base
|
4
|
+
attr_reader :transfer
|
5
|
+
attr_reader :charge_to_sales_receipts
|
6
|
+
attr_reader :refund_to_refund_receipts
|
7
|
+
attr_reader :deposit
|
8
|
+
|
9
|
+
def initialize(transfer, configuration)
|
10
|
+
super(configuration)
|
11
|
+
|
12
|
+
@transfer = transfer
|
13
|
+
|
14
|
+
charges = stripe_api.get_transfer_charges(transfer.id)
|
15
|
+
@charge_to_sales_receipts = charges.map {|charge| ChargeToSalesReceipt.new(charge, configuration) }
|
16
|
+
|
17
|
+
refunds = stripe_api.get_transfer_refunds(transfer.id)
|
18
|
+
@refund_to_refund_receipts = refunds.map {|refund| RefundToRefundReceipt.new(refund, configuration) }
|
19
|
+
|
20
|
+
@deposit = build_empty_deposit
|
21
|
+
end
|
22
|
+
|
23
|
+
def find
|
24
|
+
return @found if @found
|
25
|
+
|
26
|
+
# Quickbooks doesn't have an external ID on Deposits, so we have to
|
27
|
+
# search for a match on private_note...
|
28
|
+
# first narrow down results by txn_date
|
29
|
+
deposits = quickbooks_api.deposit_service.find_by(:txn_date, format_date(transfer.created))
|
30
|
+
deposits.each do |deposit|
|
31
|
+
return @found = deposit if deposit.private_note =~ /#{transfer.id}/
|
32
|
+
end
|
33
|
+
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def create!
|
38
|
+
raise "Deposit for #{transfer.id} already exists: #{find.id}" if exists?
|
39
|
+
|
40
|
+
create_dependencies!
|
41
|
+
build_deposit_line_items
|
42
|
+
quickbooks_api.deposit_service.create(deposit)
|
43
|
+
end
|
44
|
+
|
45
|
+
def create_dependencies!
|
46
|
+
charge_to_sales_receipts.each do |charge_to_sales_receipt|
|
47
|
+
charge_to_sales_receipt.find_or_create
|
48
|
+
end
|
49
|
+
|
50
|
+
refund_to_refund_receipts.each do |refund_to_refund_receipt|
|
51
|
+
refund_to_refund_receipt.find_or_create
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def delete!
|
56
|
+
return false unless exists?
|
57
|
+
|
58
|
+
result = quickbooks_api.deposit_service.delete(find)
|
59
|
+
@found = nil
|
60
|
+
|
61
|
+
delete_dependencies!
|
62
|
+
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
def delete_dependencies!
|
67
|
+
charge_to_sales_receipts.each do |charge_to_sales_receipt|
|
68
|
+
charge_to_sales_receipt.delete! if charge_to_sales_receipt.exists?
|
69
|
+
end
|
70
|
+
|
71
|
+
refund_to_refund_receipts.each do |refund_to_refund_receipt|
|
72
|
+
refund_to_refund_receipt.delete! if refund_to_refund_receipt.exists?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def build_empty_deposit
|
79
|
+
deposit = Quickbooks::Model::Deposit.new
|
80
|
+
deposit.deposit_to_account_id = quickbooks_api.deposit_account.id
|
81
|
+
deposit.txn_date = format_date(transfer.created)
|
82
|
+
deposit.private_note = "Imported by Stripe2QB: #{transfer.id}"
|
83
|
+
deposit.auto_doc_number!
|
84
|
+
|
85
|
+
deposit
|
86
|
+
end
|
87
|
+
|
88
|
+
def build_deposit_line_items
|
89
|
+
charge_to_sales_receipts.each do |charge_to_sales_receipt|
|
90
|
+
deposit.line_items << build_deposit_line_item_for_sales_receipt(charge_to_sales_receipt.find)
|
91
|
+
end
|
92
|
+
|
93
|
+
refund_to_refund_receipts.each do |refund_to_refund_receipt|
|
94
|
+
deposit.line_items << build_deposit_line_item_for_refund_receipt(refund_to_refund_receipt.find)
|
95
|
+
end
|
96
|
+
|
97
|
+
deposit.line_items << build_deposit_line_item_for_fees
|
98
|
+
deposit.line_items << build_deposit_line_item_for_fee_refunds
|
99
|
+
end
|
100
|
+
|
101
|
+
def build_deposit_line_item_for_sales_receipt(sales_receipt)
|
102
|
+
line_item = Quickbooks::Model::DepositLineItem.new
|
103
|
+
line_item.amount = sales_receipt.total
|
104
|
+
line_item.description = "Deposit for SalesReceipt #{sales_receipt.id}"
|
105
|
+
|
106
|
+
linked_txn = Quickbooks::Model::LinkedTransaction.new
|
107
|
+
linked_txn.txn_id = sales_receipt.id
|
108
|
+
linked_txn.txn_type = 'SalesReceipt'
|
109
|
+
linked_txn.txn_line_id = 0
|
110
|
+
line_item.linked_transactions = [ linked_txn ]
|
111
|
+
|
112
|
+
line_item
|
113
|
+
end
|
114
|
+
|
115
|
+
def build_deposit_line_item_for_refund_receipt(refund_receipt)
|
116
|
+
line_item = Quickbooks::Model::DepositLineItem.new
|
117
|
+
line_item.amount = -1 * refund_receipt.total # convert to negative number for Deposit
|
118
|
+
line_item.description = "Deposit for RefundReceipt #{refund_receipt.id}"
|
119
|
+
|
120
|
+
linked_txn = Quickbooks::Model::LinkedTransaction.new
|
121
|
+
linked_txn.txn_id = refund_receipt.id
|
122
|
+
linked_txn.txn_type = 'RefundReceipt'
|
123
|
+
linked_txn.txn_line_id = 0
|
124
|
+
line_item.linked_transactions = [ linked_txn ]
|
125
|
+
|
126
|
+
line_item
|
127
|
+
end
|
128
|
+
|
129
|
+
def build_deposit_line_item_for_fees
|
130
|
+
line_item = Quickbooks::Model::DepositLineItem.new
|
131
|
+
fees = charge_to_sales_receipts.inject(0) {|sum, charge_to_sales_receipts| sum += charge_to_sales_receipts.charge.fee }
|
132
|
+
line_item.amount = format_amount(-1 * fees) # convert to negative number for Deposit
|
133
|
+
line_item.description = "Stripe Fees for #{transfer.id}"
|
134
|
+
set_deposit_line_detail_for_fees(line_item)
|
135
|
+
|
136
|
+
line_item
|
137
|
+
end
|
138
|
+
|
139
|
+
def build_deposit_line_item_for_fee_refunds
|
140
|
+
line_item = Quickbooks::Model::DepositLineItem.new
|
141
|
+
fee_refunds = refund_to_refund_receipts.inject(0) {|sum, refund_to_refund_receipt| sum += refund_to_refund_receipt.refund.fee }
|
142
|
+
line_item.amount = format_amount(-1 * fee_refunds) # convert to positive number for Deposit
|
143
|
+
line_item.description = "Stripe Fee Refunds for #{transfer.id}"
|
144
|
+
set_deposit_line_detail_for_fees(line_item)
|
145
|
+
|
146
|
+
line_item
|
147
|
+
end
|
148
|
+
|
149
|
+
def set_deposit_line_detail_for_fees(line_item)
|
150
|
+
line_item.deposit_line_detail! do |detail|
|
151
|
+
detail.entity = Quickbooks::Model::BaseReference.new(quickbooks_api.deposit_fees_vendor.id, type: 'Vendor')
|
152
|
+
detail.account_id = quickbooks_api.deposit_fees_account.id
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Stripe2QB
|
2
|
+
module OptionsReading
|
3
|
+
def set_attribute_from_options(key, options)
|
4
|
+
value = get_required_from_options(key, options)
|
5
|
+
instance_variable_set("@#{key}", value)
|
6
|
+
end
|
7
|
+
|
8
|
+
def get_required_from_options(key, options)
|
9
|
+
options[key] or raise ConfigurationError.new("missing #{key}")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'oauth'
|
2
|
+
|
3
|
+
module Stripe2QB
|
4
|
+
class QuickbooksApi
|
5
|
+
class AccessToken
|
6
|
+
include OptionsReading
|
7
|
+
|
8
|
+
attr_reader :api_access_token
|
9
|
+
attr_reader :api_access_secret
|
10
|
+
attr_reader :oauth_consumer_key
|
11
|
+
attr_reader :oauth_consumer_secret
|
12
|
+
|
13
|
+
attr_reader :oauth
|
14
|
+
|
15
|
+
def initialize(options = {})
|
16
|
+
set_attribute_from_options('api_access_token', options)
|
17
|
+
set_attribute_from_options('api_access_secret', options)
|
18
|
+
set_attribute_from_options('oauth_consumer_key', options)
|
19
|
+
set_attribute_from_options('oauth_consumer_secret', options)
|
20
|
+
|
21
|
+
oauth_consumer = ::OAuth::Consumer.new(
|
22
|
+
oauth_consumer_key,
|
23
|
+
oauth_consumer_secret,
|
24
|
+
{
|
25
|
+
site: 'https://oauth.intuit.com',
|
26
|
+
request_token_path: '/oauth/v1/get_request_token',
|
27
|
+
authorize_url: 'https://appcenter.intuit.com/Connect/Begin',
|
28
|
+
access_token_path: '/oauth/v1/get_access_token'
|
29
|
+
}
|
30
|
+
)
|
31
|
+
|
32
|
+
@oauth = ::OAuth::AccessToken.new(oauth_consumer, api_access_token, api_access_secret)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'stripe2qb/quickbooks_api/access_token'
|
2
|
+
require 'active_support/inflector'
|
3
|
+
require 'quickbooks-ruby'
|
4
|
+
|
5
|
+
module Stripe2QB
|
6
|
+
class QuickbooksApi
|
7
|
+
include OptionsReading
|
8
|
+
include ActiveSupport::Inflector
|
9
|
+
|
10
|
+
attr_reader :realm_id
|
11
|
+
attr_reader :access_token
|
12
|
+
attr_reader :receipt_customer
|
13
|
+
attr_reader :receipt_item
|
14
|
+
attr_reader :receipt_payment_method
|
15
|
+
attr_reader :receipt_account
|
16
|
+
attr_reader :deposit_account
|
17
|
+
attr_reader :deposit_fees_vendor
|
18
|
+
attr_reader :deposit_fees_account
|
19
|
+
|
20
|
+
def initialize(options)
|
21
|
+
set_attribute_from_options('realm_id', options)
|
22
|
+
@access_token = AccessToken.new(options)
|
23
|
+
@receipt_customer = find_by_name_or_id(:customer, 'receipt_customer', options, :display_name)
|
24
|
+
@receipt_item = find_by_name_or_id(:item, 'receipt_item', options)
|
25
|
+
@receipt_payment_method = find_by_name_or_id(:payment_method, 'receipt_payment_method', options)
|
26
|
+
@receipt_account = find_by_name_or_id(:account, 'receipt_account', options)
|
27
|
+
@deposit_account = find_by_name_or_id(:account, 'deposit_account', options)
|
28
|
+
@deposit_fees_vendor = find_by_name_or_id(:vendor, 'deposit_fees_vendor', options, :display_name)
|
29
|
+
@deposit_fees_account = find_by_name_or_id(:account, 'deposit_fees_account', options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def renew_access_token
|
33
|
+
result = access_token_service.renew
|
34
|
+
|
35
|
+
{ token: result.token, secret: result.secret, created_on: Date.today }
|
36
|
+
end
|
37
|
+
|
38
|
+
def method_missing(symbol, *args)
|
39
|
+
if symbol.to_s =~ /^(.+)_service$/
|
40
|
+
get_service($1.camelize)
|
41
|
+
else
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def find_by_name_or_id(type, key, options, name_attr = :name)
|
49
|
+
value = get_required_from_options(key, options)
|
50
|
+
service = send("#{type}_service")
|
51
|
+
|
52
|
+
unless object = value.is_a?(String) ? service.find_by(name_attr, value).first : service.find_by(:id, value).first
|
53
|
+
msg = value.is_a?(String) ? "#{name_attr} '#{value}'" : "ID #{value}"
|
54
|
+
raise ConfigurationError.new("no #{type} for #{msg}")
|
55
|
+
end
|
56
|
+
|
57
|
+
object
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_service(name)
|
61
|
+
service = "Quickbooks::Service::#{name}".constantize.new
|
62
|
+
service.access_token = access_token.oauth
|
63
|
+
service.company_id = realm_id
|
64
|
+
|
65
|
+
service
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'stripe'
|
2
|
+
|
3
|
+
module Stripe2QB
|
4
|
+
class StripeApi
|
5
|
+
include OptionsReading
|
6
|
+
|
7
|
+
attr_reader :api_key
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
Stripe.api_key = set_attribute_from_options('api_key', options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_transfers(start_date, end_date = nil)
|
14
|
+
Stripe::Transfer.list(date: date_range_criteria(start_date, end_date), status: 'paid', limit: 100)
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_transfer_charges(transfer_id)
|
18
|
+
Stripe::BalanceTransaction.all(transfer: transfer_id, type: 'charge', limit: 100)
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_transfer_refunds(transfer_id)
|
22
|
+
Stripe::BalanceTransaction.all(transfer: transfer_id, type: 'refund', limit: 100)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def date_range_criteria(start_date, end_date = nil)
|
28
|
+
start_time = to_time(start_date)
|
29
|
+
end_time = to_time(end_date || Date.parse(start_date) + 1.day)
|
30
|
+
|
31
|
+
{ gte: start_time, lt: end_time }
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_time(date)
|
35
|
+
date = Date.parse(date) if date.is_a?(String)
|
36
|
+
|
37
|
+
Time.parse("#{date} 0:00 UTC").to_i
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/stripe2qb.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'stripe2qb/configuration'
|
2
|
+
require 'stripe2qb/converters'
|
3
|
+
require 'stripe2qb/quickbooks_api'
|
4
|
+
require 'stripe2qb/stripe_api'
|
5
|
+
require 'stripe2qb/version'
|
6
|
+
|
7
|
+
module Stripe2QB
|
8
|
+
def self.new(options = 'config/stripe2qb.yml')
|
9
|
+
Configuration.new(options)
|
10
|
+
end
|
11
|
+
end
|
data/stripe2qb.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'stripe2qb/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "stripe2qb"
|
8
|
+
spec.version = Stripe2QB::VERSION
|
9
|
+
spec.authors = ["Anthony Wang"]
|
10
|
+
spec.email = ["anthony@anthonywang.com"]
|
11
|
+
|
12
|
+
spec.summary = 'Import Stripe transactions into Quickbooks'
|
13
|
+
spec.description = 'Import Stripe transactions into Quickbooks. Relies on the Stripe and Quickbooks gems.'
|
14
|
+
spec.homepage = "http://rubygems.org/gems/stripe2qb"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
18
|
+
# delete this section to allow pushing this gem to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
23
|
+
end
|
24
|
+
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
31
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
32
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
33
|
+
|
34
|
+
spec.add_runtime_dependency 'quickbooks-ruby', '~> 0.4'
|
35
|
+
spec.add_runtime_dependency 'activesupport'
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stripe2qb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Anthony Wang
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-10-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.11'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.11'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: quickbooks-ruby
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.4'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.4'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activesupport
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Import Stripe transactions into Quickbooks. Relies on the Stripe and
|
84
|
+
Quickbooks gems.
|
85
|
+
email:
|
86
|
+
- anthony@anthonywang.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- ".rspec"
|
93
|
+
- ".travis.yml"
|
94
|
+
- CODE_OF_CONDUCT.md
|
95
|
+
- Gemfile
|
96
|
+
- LICENSE.txt
|
97
|
+
- README.md
|
98
|
+
- Rakefile
|
99
|
+
- bin/console
|
100
|
+
- bin/setup
|
101
|
+
- config/.keep
|
102
|
+
- config/stripe2qb.yml
|
103
|
+
- lib/stripe2qb.rb
|
104
|
+
- lib/stripe2qb/configuration.rb
|
105
|
+
- lib/stripe2qb/converters.rb
|
106
|
+
- lib/stripe2qb/converters/base.rb
|
107
|
+
- lib/stripe2qb/converters/charge_to_sales_receipt.rb
|
108
|
+
- lib/stripe2qb/converters/refund_to_refund_receipt.rb
|
109
|
+
- lib/stripe2qb/converters/transfer_to_deposit.rb
|
110
|
+
- lib/stripe2qb/options_reading.rb
|
111
|
+
- lib/stripe2qb/quickbooks_api.rb
|
112
|
+
- lib/stripe2qb/quickbooks_api/access_token.rb
|
113
|
+
- lib/stripe2qb/stripe_api.rb
|
114
|
+
- lib/stripe2qb/version.rb
|
115
|
+
- stripe2qb.gemspec
|
116
|
+
homepage: http://rubygems.org/gems/stripe2qb
|
117
|
+
licenses:
|
118
|
+
- MIT
|
119
|
+
metadata:
|
120
|
+
allowed_push_host: https://rubygems.org
|
121
|
+
post_install_message:
|
122
|
+
rdoc_options: []
|
123
|
+
require_paths:
|
124
|
+
- lib
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
requirements: []
|
136
|
+
rubyforge_project:
|
137
|
+
rubygems_version: 2.5.1
|
138
|
+
signing_key:
|
139
|
+
specification_version: 4
|
140
|
+
summary: Import Stripe transactions into Quickbooks
|
141
|
+
test_files: []
|