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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/Gemfile +4 -0
- data/README.html +1065 -0
- data/README.md +156 -0
- data/Rakefile +2 -0
- data/bank_teller.gemspec +32 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/bank_teller/engine.rb +5 -0
- data/lib/bank_teller/version.rb +3 -0
- data/lib/bank_teller.rb +70 -0
- data/lib/billable.rb +204 -0
- data/lib/generators/bank_teller/install_generator.rb +31 -0
- data/lib/generators/bank_teller/templates/add_bank_teller_fields_to_users.rb +30 -0
- data/lib/generators/bank_teller/templates/create_subscriptions.rb +16 -0
- data/lib/invoice.rb +115 -0
- data/lib/invoice_item.rb +38 -0
- data/lib/subscription.rb +118 -0
- data/lib/subscription_builder.rb +79 -0
- metadata +106 -0
data/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Bank Teller
|
|
2
|
+
[](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
data/bank_teller.gemspec
ADDED
|
@@ -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
data/lib/bank_teller.rb
ADDED
|
@@ -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
|