bank_teller 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.
data/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # Bank Teller
2
+ [![Code Climate](https://codeclimate.com/repos/572c190b20916e00680030b0/badges/df6d434fec0219cc1364/gpa.svg)](https://codeclimate.com/repos/572c190b20916e00680030b0/feed)
3
+
4
+ Bank Teller is a Ruby on Rails interface for interacting with Stripe. It is an implementation the Laravel library, [Cashier](http://github.com/laravel/cashier). Major props to Taylor Otwell and all of the contributors to Cashier, it's amazing. Bank Teller has some minor API differences from Cashier, mostly to match the Ruby style. To quote the Cashier project: "It handles almost all of the boilerplate subscription billing code you are dreading writing... coupons, swapping subscription, subscription 'quantities', cancellation grace periods, and invoice PDFs."
5
+
6
+ This gem cannot be used as a stand-alone gem. It is very tightly integrated with ActiveSupport and ActiveRecord. This gem is best used in a Ruby on Rails application.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'bank_teller'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Bank Teller comes with a couple of migrations:
21
+ 1. A migration to add fields to a `users` table. `users` should already exist.
22
+ 2. A migration to create a `subscriptions` table.
23
+
24
+ To add these migrations to your application, run:
25
+
26
+ $ rails generate bank_teller:install
27
+ $ rake db:migrate
28
+
29
+ ## Usage
30
+ ### Setting It Up
31
+ Stripe requires a private API key. Once you have aquired your private key, you'll need to set the environment variable `ENV["STRIPE_API_KEY"]` equal to your API key. If you're not sure how to use environment variables, checkout [Figaro](https://github.com/laserlemon/figaro) or my favorite, [dotenv](https://github.com/bkeepers/dotenv).
32
+
33
+ Once you have your key in place, all you need to do is include the `Billable` module in your `User` class:
34
+
35
+ ```ruby
36
+ # app/models/user.rb
37
+ class User < ActiveRecord::Base
38
+ include Billable
39
+ end
40
+ ```
41
+
42
+ ### Make Some Money
43
+ #### Create a Subscription
44
+ ```ruby
45
+ user = User.find(1)
46
+ user.new_subscription('main', 'monthly').create(token)
47
+ ```
48
+
49
+ `#new_subscription` is a method call on a `User` object that takes two arguments:
50
+ 1. The name of the plan, for internal use
51
+ 2. The ID of the plan you created with Stripe
52
+
53
+ `#create` takes one argument, the stripe credit card token. It sends the subscription to Stripe and creates the subscription record in the databse.
54
+
55
+ You can also send addtional fields for the user when creating a new subscription.
56
+
57
+ ```ruby
58
+ user.new_subscription('main', 'monthly').create(token, { email: 'john@johndoe.com' })
59
+ ```
60
+
61
+ To see all the options, [checkout the Stripe docs](https://stripe.com/docs/api#create_customer).
62
+
63
+ ##### Coupons!
64
+ ```ruby
65
+ user.new_subscription('main', 'monthly', coupon: 'code').create(token)
66
+ ```
67
+
68
+ <hr>
69
+
70
+ #### Subscription Status
71
+ ##### Active(ness)
72
+ See if a user has an active subscription:
73
+ ```ruby
74
+ user.subscribed?('main')
75
+ ```
76
+
77
+ See if a user's subscription is still on a trial:
78
+ ```ruby
79
+ user.subscription('main').on_trial?
80
+ ```
81
+
82
+ See if a user is subscribed to a specific plan:
83
+ ```ruby
84
+ user.subscribed_to_plan?('main', 'monthly')
85
+ ```
86
+
87
+ ##### Cancellations
88
+ See if a user has cancelled their subscription:
89
+ ```ruby
90
+ user.subscription('main').cancelled?
91
+ ```
92
+
93
+ See if a user has cancelled their subscription, but still has a grace period:
94
+ ```ruby
95
+ user.subscription('main').on_grace_period?
96
+ ```
97
+
98
+ #### Switch Plans
99
+ Swap between different Stripe plans:
100
+ ```ruby
101
+ user.subscription('main').swap('another-stripe-plan-id')
102
+ ```
103
+
104
+ #### Quantity
105
+ Add 1 to the quantity of plans:
106
+ ```ruby
107
+ user.subscription('main').increment_quantity
108
+ ```
109
+
110
+ Add n to the quantity of plans:
111
+ ```ruby
112
+ user.subscription('main').increment_quantity(10)
113
+ ```
114
+
115
+ Remove 1 from the quantity of plans:
116
+ ```ruby
117
+ user.subscription('main').decrement_quantity
118
+ ```
119
+
120
+ Remove n from the quantity of plans:
121
+ ```ruby
122
+ user.subscription('main').decrement_quantity(10)
123
+ ```
124
+
125
+ Directly update the quantity of plans:
126
+ ```ruby
127
+ user.subscription('main').update_quantity(20)
128
+ ```
129
+
130
+ #### Taxes
131
+ To charge tax for your plans, overwrite the `tax_percentage` method in your `User` class:
132
+ ```ruby
133
+ # app/models/user.rb
134
+ class User < ActiveRecord::Base
135
+ def tax_percentage
136
+ 9.25
137
+ end
138
+ end
139
+ ```
140
+
141
+ #### Cancel Subscriptions
142
+ Cancel a subscription with a grace period (time remaining in the active plan):
143
+ ```ruby
144
+ user.subscription('main').cancel
145
+ ```
146
+
147
+ ## Development
148
+
149
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
150
+
151
+ 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).
152
+
153
+ ## Contributing
154
+
155
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jasoncharnes/bank_teller.
156
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'bank_teller/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "bank_teller"
8
+ spec.version = BankTeller::VERSION
9
+ spec.authors = ["Jason Charnes"]
10
+ spec.email = ["jason@jasoncharnes.com"]
11
+
12
+ spec.summary = %q{A subscription billing interface modeled after Laravel Cashier}
13
+ spec.description = %q{A subscription billing interface modeled after Laravel Cashier}
14
+ spec.homepage = "http://www.github.com/jasoncharnes/bank_teller"
15
+
16
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
17
+ # delete this section to allow pushing this gem to any host.
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
20
+ else
21
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
22
+ end
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib", "app"]
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.11"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_runtime_dependency 'stripe', "~> 1.42.0"
32
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "bank_teller"
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,5 @@
1
+ module BankTeller
2
+ class Engine < ::Rails::Engine
3
+ config.autoload_paths += Dir["#{config.root}/lib/**/"]
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module BankTeller
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,70 @@
1
+ require "bank_teller/version"
2
+ require 'bank_teller/engine' if defined?(Rails)
3
+
4
+ module BankTeller
5
+ # The current currency.
6
+ #
7
+ # @return [String]
8
+ @@currency = 'usd'
9
+
10
+ # The current currency symbol.
11
+ #
12
+ # @return [String]
13
+ @@currency_symbol = '$'
14
+
15
+ # The custom currency formatter.
16
+ #
17
+ # @return [Lambda]
18
+ @@format_currency_using = nil
19
+
20
+ # Set the currency to be used when billing users.
21
+ #
22
+ # @param currency [Integer]
23
+ # @param symbol [String]
24
+ # @return [Boolean]
25
+ def self.use_currency(currency, symbol = nil)
26
+ @@currency = currency
27
+ symbol = self.guess_currency_symbol(currency) if symbol.nil?
28
+ self.use_currency_symbol(symbol)
29
+ end
30
+
31
+ def self.guess_currency_symbol(currency)
32
+ case currency.downcase
33
+ when 'usd', 'cad', 'aud'
34
+ '$'
35
+ when 'eur'
36
+ '€'
37
+ when 'gbp'
38
+ '£'
39
+ else
40
+ raise 'You must explicitly specify the currency symbol.'
41
+ end
42
+ end
43
+
44
+ def self.use_currency_symbol(symbol)
45
+ @@currency_symbol = symbol
46
+ end
47
+
48
+ def self.uses_currency
49
+ @@currency
50
+ end
51
+
52
+ def self.uses_currency_symbol
53
+ @@currency_symbol
54
+ end
55
+
56
+ def self.formatCurrencyUsing(callback)
57
+ @@format_currency_using = callback
58
+ end
59
+
60
+ def self.format_amount(amount)
61
+ self.format_currency_using(amount) if @@format_currency_using
62
+ amount = sprintf("%03d", amount).insert(-3, ".")
63
+
64
+ if amount.start_with?('-')
65
+ return "-#{self.uses_currency_symbol}#{amount.sub!(/^-/, '')}"
66
+ end
67
+
68
+ "#{uses_currency_symbol}#{amount}"
69
+ end
70
+ end
data/lib/billable.rb ADDED
@@ -0,0 +1,204 @@
1
+ require 'subscription_builder'
2
+ require 'stripe'
3
+
4
+ module Billable
5
+ def self.included(base)
6
+ base.class_eval do
7
+ has_many :subscriptions
8
+ Stripe.api_key = ENV["STRIPE_API_KEY"]
9
+ end
10
+ end
11
+
12
+ def charge(amount, options = {})
13
+ options.merge!({ currency: preferred_currency })
14
+ options[:amount] = amount
15
+
16
+ if !options.key?('source') && stripe_id
17
+ options[:customer] = stripe_id
18
+ end
19
+
20
+ if !options.key?('source') && !options.key?('customer')
21
+ raise 'No payment source provided.'
22
+ end
23
+
24
+ Stripe::Charge.create(options)
25
+ end
26
+
27
+ def refund(charge, options = {})
28
+ options[:charge] = charge
29
+ Stripe::Refund.create(options)
30
+ end
31
+
32
+ def invoice_for(description, amount, options = {})
33
+ if !stripe_id
34
+ raise 'User is not a customer. See the create_as_stripe_customer method.'
35
+ end
36
+
37
+ options.merge!({
38
+ customer: stripe_id,
39
+ amount: amount,
40
+ currency: preferred_currency,
41
+ description: description
42
+ })
43
+
44
+ Stripe::InvoiceItem.create(options)
45
+ end
46
+
47
+ def new_subscription(subscription, plan, *args)
48
+ SubscriptionBuilder.new(self, subscription, plan, *args)
49
+ end
50
+
51
+ def on_trial?(subscription = 'default', plan = nil)
52
+ return true if on_generic_trial?
53
+ subscription = get_subscription(subscription)
54
+
55
+ if plan.nil?
56
+ has_subscription_on_trial?(subscription)
57
+ else
58
+ has_subscription_on_trial?(subscription) && stripe_plan === plan
59
+ end
60
+ end
61
+
62
+ def has_subscription_on_trial?(subscription)
63
+ subscription && subscription.on_trial
64
+ end
65
+
66
+ def on_generic_trial?
67
+ trial_ends_at && DateTime.now < trial_ends_at.to_datetime
68
+ end
69
+
70
+ def subscribed?(subscription = 'default', plan = nil)
71
+ subscription = get_subscription(subscription)
72
+
73
+ if subscription.nil?
74
+ false
75
+ elsif plan.nil?
76
+ subscription.valid
77
+ else
78
+ subscription.valid && stripe_plan === plan
79
+ end
80
+ end
81
+
82
+ def get_subscription(subscription = 'default')
83
+ subscription_in_db = subscriptions.order(created_at: :desc).first
84
+ subscription_in_db if subscription_in_db.name === subscription
85
+ end
86
+
87
+ def subscription(name)
88
+ get_subscription(name)
89
+ end
90
+
91
+ def invoice
92
+ Stripe::Invoice.create(customer: stripe_id).pay
93
+ rescue
94
+ false
95
+ end
96
+
97
+ def upcoming_invoice
98
+ args = { customer: stripe_id }
99
+ stripe_invoice = Stripe::Invoice.upcoming(args)
100
+ Invoice.new(self, stripe_invoice)
101
+ rescue
102
+ false
103
+ end
104
+
105
+ def find_invoice(id)
106
+ stripe_invoice = Stripe::Invoice.retrieve(id)
107
+ Invoice.new(self, stripe_invoice)
108
+ rescue
109
+ false
110
+ end
111
+
112
+ def find_invoice_or_fail(id)
113
+ invoice = find_invoice(id)
114
+ raise 'Invoice not found' if invoice.nil?
115
+ invoice
116
+ end
117
+
118
+ def download_invoice(id, data, storage_path = nil)
119
+ # Coming Soon
120
+ end
121
+
122
+ def invoices(include_pending = false, parameters = {})
123
+ invoices = []
124
+ parameters.merge!({ limit: 24 })
125
+ stripe_invoices = as_stripe_customer.invoices(parameters)
126
+
127
+ unless stripe_invoices.nil?
128
+ stripe_invoices.data.each do |invoice|
129
+ if invoice.paid || include_pending
130
+ invoices << Invoice.new(self, invoice)
131
+ end
132
+ end
133
+ end
134
+
135
+ invoices
136
+ end
137
+
138
+ def invoices_including_pending(parameters = [])
139
+ invoices(true, parameters)
140
+ end
141
+
142
+ def update_card(token)
143
+ customer = as_stripe_customer
144
+ token = Stripe::Token.retrieve(token)
145
+ return if token.card.id === customer.default_source
146
+
147
+ card = customer.sources.create(source: token)
148
+ customer.default_source = card.id
149
+ customer.save
150
+
151
+ if customer.default_source
152
+ source = customer.sources.retrieve(customer.default_source)
153
+ end
154
+
155
+ if source
156
+ self.card_brand = source.brand
157
+ self.card_last_four = source.last4
158
+ end
159
+
160
+ self.save
161
+ end
162
+
163
+ def apply_coupon(coupon)
164
+ customer = as_stripe_customer
165
+ customer.coupon = coupon
166
+ customer.save
167
+ end
168
+
169
+ def subscribed_to_plan?(subscription = 'default', plan)
170
+ subscription = get_subscription(subscription)
171
+ return false unless subscription || subscription.valid
172
+ subscription.stripe_plan === plan
173
+ end
174
+
175
+ def on_plan(plan)
176
+ subscription = subscriptions.first
177
+ subscription.stripe_plan === plan && subscription.valid
178
+ end
179
+
180
+ def has_stripe_id
181
+ !!stripe_id
182
+ end
183
+
184
+ def create_as_stripe_customer(token, options = {})
185
+ options.merge!({ email: email })
186
+ customer = Stripe::Customer.create(options)
187
+ self.stripe_id = customer.id
188
+ self.save
189
+ update_card(token) unless token.nil?
190
+ customer
191
+ end
192
+
193
+ def as_stripe_customer
194
+ Stripe::Customer.retrieve(stripe_id)
195
+ end
196
+
197
+ def preferred_currency
198
+ BankTeller::uses_currency
199
+ end
200
+
201
+ def tax_percentage
202
+ 0
203
+ end
204
+ end
@@ -0,0 +1,31 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+ require 'rails/generators/active_record'
4
+
5
+ module BankTeller
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+ extend ActiveRecord::Generators::Migration
9
+
10
+ desc "Install the migrations needed for Bank Teller."
11
+ class_option :provider, type: :string, default: :stripe
12
+ source_root File.expand_path('../templates', __FILE__)
13
+
14
+ def self.next_migration_number(*)
15
+ sleep 1 # Prevents Duplicate Timestamps on FAAAAST Machines
16
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
17
+ end
18
+
19
+ def alter_users
20
+ if ActiveRecord::Base.connection.table_exists?('users')
21
+ migration_template "add_bank_teller_fields_to_users.rb", "db/migrate/add_bank_teller_fields_to_users.rb"
22
+ else
23
+ raise "You must have a users table to install Bank Teller."
24
+ end
25
+ end
26
+
27
+ def create_subscriptions
28
+ migration_template "create_subscriptions.rb", "db/migrate/create_subscriptions.rb"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ class AddBankTellerFieldsToUsers < ActiveRecord::Migration
2
+ def self.up
3
+ unless column_exists? :users, :email
4
+ add_column :users, :email, :string
5
+ end
6
+
7
+ unless column_exists? :users, :stripe_id
8
+ add_column :users, :stripe_id, :string
9
+ end
10
+
11
+ unless column_exists? :users, :card_brand
12
+ add_column :users, :card_brand, :string
13
+ end
14
+
15
+ unless column_exists? :users, :card_last_four
16
+ add_column :users, :card_last_four, :string
17
+ end
18
+
19
+ unless column_exists? :users, :trial_ends_at
20
+ add_column :users, :trial_ends_at, :datetime
21
+ end
22
+ end
23
+
24
+ def self.down
25
+ remove_column :users, :stripe_id, :string
26
+ remove_column :users, :card_brand, :string
27
+ remove_column :users, :card_last_four, :string
28
+ remove_column :users, :trial_ends_at, :datetime
29
+ end
30
+ end
@@ -0,0 +1,16 @@
1
+ class CreateSubscriptions < ActiveRecord::Migration
2
+ def change
3
+ create_table :subscriptions do |t|
4
+ t.integer :user_id, null: false
5
+ t.string :name, null: false
6
+ t.string :stripe_id, null: false
7
+ t.string :stripe_plan, null: false
8
+ t.integer :quantity, null: false
9
+ t.string :stripe_plan
10
+ t.datetime :trial_ends_at
11
+ t.datetime :ends_at
12
+
13
+ t.timestamps null: false
14
+ end
15
+ end
16
+ end
data/lib/invoice.rb ADDED
@@ -0,0 +1,115 @@
1
+ class Invoice
2
+ def initialize(user, invoice)
3
+ @user = user
4
+ @invoice = invoice
5
+ end
6
+
7
+ def date(timezone = nil)
8
+ invoice.to_time.in_time_zone(timezone).to_date
9
+ end
10
+
11
+ def total
12
+ format_amount(raw_total)
13
+ end
14
+
15
+ def raw_total
16
+ amount = invoice.total - (raw_starting_balance * -1)
17
+ [0, amount].max
18
+ end
19
+
20
+ def subtotal
21
+ amount = invoice.subtotal - raw_starting_balance
22
+ amount = [0, amount].max
23
+ format_amount(amount)
24
+ end
25
+
26
+ def has_starting_balance?
27
+ raw_starting_balance > 0
28
+ end
29
+
30
+ def starting_balance
31
+ format_amount(raw_starting_balance)
32
+ end
33
+
34
+ def has_discount
35
+ invoice.subtotal > 0 and
36
+ invoice.subtotal != invoice.total and
37
+ !invoice.discount.nil?
38
+ end
39
+
40
+ def discount
41
+ amount = invoice.subtotal - invoice.total
42
+ format_amount(amount)
43
+ end
44
+
45
+ def coupon
46
+ invoice.discount.coupon.id if invoice.discount
47
+ end
48
+
49
+ def discount_is_percentage?
50
+ coupon and invoice.discount.coupon.percent_off
51
+ end
52
+
53
+ def percent_off
54
+ if coupon
55
+ invoice.discount.coupon.percent_off
56
+ else
57
+ 0
58
+ end
59
+ end
60
+
61
+ def amount_off
62
+ amount = invoice.discount.coupon.amount_off || 0
63
+ format_amount(amount)
64
+ end
65
+
66
+ def invoice_items
67
+ invoice_items_by_type('invoiceitem')
68
+ end
69
+
70
+ def subscriptions
71
+ invoice_items_by_type('subscription')
72
+ end
73
+
74
+ def invoice_items_by_type(type)
75
+ line_items = []
76
+
77
+ if lines.data
78
+ lines.data.each do |line|
79
+ if line.type == type
80
+ line_items << InvoiceItem.new(user, line)
81
+ end
82
+ end
83
+ end
84
+
85
+ line_items
86
+ end
87
+
88
+ def format_amount(amount)
89
+ BankTeller::format_amount(amount)
90
+ end
91
+
92
+ def view(data)
93
+ # Coming Soon
94
+ end
95
+
96
+ def pdf(data)
97
+ # Coming Soon
98
+ end
99
+
100
+ def download(data)
101
+ # Coming Soon
102
+ end
103
+
104
+ def raw_starting_balance
105
+ invoice.starting_balance || 0
106
+ end
107
+
108
+ def as_stripe_invoice
109
+ invoice
110
+ end
111
+
112
+ protected
113
+
114
+ attr_accessor :user, :invoice
115
+ end