reji 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +14 -0
  3. data/.gitattributes +4 -0
  4. data/.gitignore +15 -0
  5. data/.travis.yml +28 -0
  6. data/Appraisals +17 -0
  7. data/Gemfile +8 -0
  8. data/Gemfile.lock +133 -0
  9. data/LICENSE +20 -0
  10. data/README.md +1285 -0
  11. data/Rakefile +21 -0
  12. data/app/controllers/reji/payment_controller.rb +31 -0
  13. data/app/controllers/reji/webhook_controller.rb +170 -0
  14. data/app/views/payment.html.erb +228 -0
  15. data/app/views/receipt.html.erb +250 -0
  16. data/bin/setup +12 -0
  17. data/config/routes.rb +6 -0
  18. data/gemfiles/rails_5.0.gemfile +13 -0
  19. data/gemfiles/rails_5.1.gemfile +13 -0
  20. data/gemfiles/rails_5.2.gemfile +13 -0
  21. data/gemfiles/rails_6.0.gemfile +13 -0
  22. data/lib/generators/reji/install/install_generator.rb +69 -0
  23. data/lib/generators/reji/install/templates/db/migrate/add_reji_to_users.rb.erb +16 -0
  24. data/lib/generators/reji/install/templates/db/migrate/create_subscription_items.rb.erb +19 -0
  25. data/lib/generators/reji/install/templates/db/migrate/create_subscriptions.rb.erb +22 -0
  26. data/lib/generators/reji/install/templates/reji.rb +36 -0
  27. data/lib/reji.rb +75 -0
  28. data/lib/reji/billable.rb +13 -0
  29. data/lib/reji/concerns/interacts_with_payment_behavior.rb +33 -0
  30. data/lib/reji/concerns/manages_customer.rb +113 -0
  31. data/lib/reji/concerns/manages_invoices.rb +136 -0
  32. data/lib/reji/concerns/manages_payment_methods.rb +202 -0
  33. data/lib/reji/concerns/manages_subscriptions.rb +91 -0
  34. data/lib/reji/concerns/performs_charges.rb +36 -0
  35. data/lib/reji/concerns/prorates.rb +38 -0
  36. data/lib/reji/configuration.rb +59 -0
  37. data/lib/reji/engine.rb +4 -0
  38. data/lib/reji/errors.rb +66 -0
  39. data/lib/reji/invoice.rb +243 -0
  40. data/lib/reji/invoice_line_item.rb +98 -0
  41. data/lib/reji/payment.rb +61 -0
  42. data/lib/reji/payment_method.rb +32 -0
  43. data/lib/reji/subscription.rb +567 -0
  44. data/lib/reji/subscription_builder.rb +206 -0
  45. data/lib/reji/subscription_item.rb +97 -0
  46. data/lib/reji/tax.rb +48 -0
  47. data/lib/reji/version.rb +5 -0
  48. data/reji.gemspec +32 -0
  49. data/spec/dummy/app/models/user.rb +21 -0
  50. data/spec/dummy/application.rb +53 -0
  51. data/spec/dummy/config/database.yml +11 -0
  52. data/spec/dummy/db/schema.rb +40 -0
  53. data/spec/feature/charges_spec.rb +67 -0
  54. data/spec/feature/customer_spec.rb +23 -0
  55. data/spec/feature/invoices_spec.rb +73 -0
  56. data/spec/feature/multiplan_subscriptions_spec.rb +319 -0
  57. data/spec/feature/payment_methods_spec.rb +149 -0
  58. data/spec/feature/pending_updates_spec.rb +77 -0
  59. data/spec/feature/subscriptions_spec.rb +650 -0
  60. data/spec/feature/webhooks_spec.rb +162 -0
  61. data/spec/spec_helper.rb +27 -0
  62. data/spec/support/feature_helpers.rb +39 -0
  63. data/spec/unit/customer_spec.rb +54 -0
  64. data/spec/unit/invoice_line_item_spec.rb +72 -0
  65. data/spec/unit/invoice_spec.rb +192 -0
  66. data/spec/unit/payment_spec.rb +33 -0
  67. data/spec/unit/subscription_spec.rb +103 -0
  68. metadata +237 -0
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'webhooks', type: :request do
6
+ before(:all) do
7
+ @product_id = "#{stripe_prefix}product-1-#{SecureRandom.hex(5)}"
8
+ @plan_id = "#{stripe_prefix}monthly-10-#{SecureRandom.hex(5)}"
9
+
10
+ Stripe::Product.create({
11
+ :id => @product_id,
12
+ :name => 'Rails Reji Test Product',
13
+ :type => 'service',
14
+ })
15
+
16
+ Stripe::Plan.create({
17
+ :id => @plan_id,
18
+ :nickname => 'Monthly $10',
19
+ :currency => 'USD',
20
+ :interval => 'month',
21
+ :billing_scheme => 'per_unit',
22
+ :amount => 1000,
23
+ :product => @product_id,
24
+ })
25
+ end
26
+
27
+ after(:all) do
28
+ delete_stripe_resource(Stripe::Plan.retrieve(@plan_id))
29
+ delete_stripe_resource(Stripe::Product.retrieve(@product_id))
30
+ end
31
+
32
+ it 'test_subscriptions_are_updated' do
33
+ user = create_customer('subscriptions_are_updated', {:stripe_id => 'cus_foo'})
34
+
35
+ subscription = user.subscriptions.create({
36
+ :name => 'main',
37
+ :stripe_id => 'sub_foo',
38
+ :stripe_plan => 'plan_foo',
39
+ :stripe_status => 'active',
40
+ })
41
+
42
+ item = subscription.items.create({
43
+ :stripe_id => 'it_foo',
44
+ :stripe_plan => 'plan_bar',
45
+ :quantity => 1,
46
+ })
47
+
48
+ post '/stripe/webhook', :params => {
49
+ :id => 'foo',
50
+ :type => 'customer.subscription.updated',
51
+ :data => {
52
+ :object => {
53
+ :id => subscription.stripe_id,
54
+ :customer => 'cus_foo',
55
+ :cancel_at_period_end => false,
56
+ :quantity => 5,
57
+ :items => {
58
+ :data => [
59
+ {
60
+ :id => 'bar',
61
+ :plan => {:id => 'plan_foo'},
62
+ :quantity => 10,
63
+ }
64
+ ],
65
+ },
66
+ },
67
+ },
68
+ }.to_json, :headers => { 'CONTENT_TYPE' => 'application/json' }
69
+
70
+ expect(response.status).to eq(200)
71
+
72
+ expect(Reji::Subscription.where({
73
+ :id => subscription.id,
74
+ :user_id => user.id,
75
+ :stripe_id => 'sub_foo',
76
+ :quantity => 5,
77
+ }).exists?).to be true
78
+
79
+ expect(Reji::SubscriptionItem.where({
80
+ :subscription_id => subscription.id,
81
+ :stripe_id => 'bar',
82
+ :stripe_plan => 'plan_foo',
83
+ :quantity => 10,
84
+ }).exists?).to be true
85
+
86
+ expect(Reji::SubscriptionItem.where({
87
+ :id => item.id,
88
+ }).exists?).to be false
89
+ end
90
+
91
+ it 'test_cancelled_subscription_is_properly_reactivated' do
92
+ user = create_customer('cancelled_subscription_is_properly_reactivated')
93
+ subscription = user.new_subscription('main', @plan_id).create('pm_card_visa')
94
+ subscription.cancel
95
+
96
+ expect(subscription.cancelled).to be true
97
+
98
+ post '/stripe/webhook', :params => {
99
+ :id => 'foo',
100
+ :type => 'customer.subscription.updated',
101
+ :data => {
102
+ :object => {
103
+ :id => subscription.stripe_id,
104
+ :customer => user.stripe_id,
105
+ :cancel_at_period_end => false,
106
+ :quantity => 1,
107
+ },
108
+ },
109
+ }.to_json, :headers => { 'CONTENT_TYPE' => 'application/json' }
110
+
111
+ expect(response.status).to eq(200)
112
+
113
+ expect(subscription.reload.cancelled).to be false
114
+ end
115
+
116
+ it 'test_subscription_is_marked_as_cancelled_when_deleted_in_stripe' do
117
+ user = create_customer('subscription_is_marked_as_cancelled_when_deleted_in_stripe')
118
+ subscription = user.new_subscription('main', @plan_id).create('pm_card_visa')
119
+
120
+ expect(subscription.cancelled).to be false
121
+
122
+ post '/stripe/webhook', :params => {
123
+ :id => 'foo',
124
+ :type => 'customer.subscription.deleted',
125
+ :data => {
126
+ :object => {
127
+ :id => subscription.stripe_id,
128
+ :customer => user.stripe_id,
129
+ :quantity => 1,
130
+ },
131
+ },
132
+ }.to_json, :headers => { 'CONTENT_TYPE' => 'application/json' }
133
+
134
+ expect(response.status).to eq(200)
135
+
136
+ expect(subscription.reload.cancelled).to be true
137
+ end
138
+
139
+ it 'test_subscription_is_deleted_when_status_is_incomplete_expired' do
140
+ user = create_customer('subscription_is_deleted_when_status_is_incomplete_expired')
141
+ subscription = user.new_subscription('main', @plan_id).create('pm_card_visa')
142
+
143
+ expect(user.subscriptions.count).to eq(1)
144
+
145
+ post '/stripe/webhook', :params => {
146
+ :id => 'foo',
147
+ :type => 'customer.subscription.updated',
148
+ :data => {
149
+ :object => {
150
+ :id => subscription.stripe_id,
151
+ :customer => user.stripe_id,
152
+ :status => 'incomplete_expired',
153
+ :quantity => 1,
154
+ },
155
+ },
156
+ }.to_json, :headers => { 'CONTENT_TYPE' => 'application/json' }
157
+
158
+ expect(response.status).to eq(200)
159
+
160
+ expect(user.reload.subscriptions.empty?).to be true
161
+ end
162
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ ENV['RAILS_ENV'] ||= 'test'
4
+
5
+ require 'rails/all'
6
+ require 'dummy/application'
7
+
8
+ require 'rspec/rails'
9
+
10
+ Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
11
+
12
+ Dummy::Application.initialize!
13
+
14
+ ActiveRecord::Migration.maintain_test_schema!
15
+
16
+ ActiveRecord::Schema.verbose = false
17
+ load 'dummy/db/schema.rb'
18
+
19
+ RSpec.configure do |config|
20
+ config.use_transactional_fixtures = true
21
+
22
+ config.infer_spec_type_from_file_location!
23
+
24
+ %i(request).each do |type|
25
+ config.include(Reji::Test::FeatureHelpers, type: type)
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'reji'
4
+
5
+ Reji.configure do |config|
6
+ config.secret = ENV['STRIPE_SECRET']
7
+ end
8
+
9
+ Stripe.api_key = ENV['STRIPE_SECRET']
10
+
11
+ module Reji
12
+ module Test
13
+ module FeatureHelpers
14
+ def stripe_prefix
15
+ 'cashier-test-'
16
+ end
17
+
18
+ sleep(2)
19
+
20
+ protected
21
+
22
+ def delete_stripe_resource(resource)
23
+ begin
24
+ resource.delete
25
+ rescue Stripe::InvalidRequestError => e
26
+ #
27
+ end
28
+ end
29
+
30
+ def create_customer(description = 'cuong', options = {})
31
+ User.create({
32
+ :email => "#{description}@reji-test.com",
33
+ }.merge(options))
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'customer', type: :unit do
6
+ it 'test_customer_can_be_put_on_a_generic_trial' do
7
+ user = User.new
8
+
9
+ expect(user.on_generic_trial).to be false
10
+
11
+ user.trial_ends_at = Time.now + 1.day
12
+
13
+ expect(user.on_generic_trial).to be true
14
+
15
+ user.trial_ends_at = Time.now - 5.days
16
+
17
+ expect(user.on_generic_trial).to be false
18
+ end
19
+
20
+ it 'test_we_can_determine_if_it_has_a_payment_method' do
21
+ user = User.new
22
+
23
+ user.card_brand = 'visa'
24
+
25
+ expect(user.has_default_payment_method).to be true
26
+
27
+ user = User.new
28
+
29
+ expect(user.has_default_payment_method).to be false
30
+ end
31
+
32
+ it 'test_default_payment_method_returns_nil_when_the_user_is_not_a_customer_yet' do
33
+ user = User.new
34
+
35
+ expect(user.default_payment_method).to be_nil
36
+ end
37
+
38
+ it 'test_stripe_customer_method_throws_exception_when_stripe_id_is_not_set' do
39
+ user = User.new
40
+
41
+ expect {
42
+ user.as_stripe_customer
43
+ }.to raise_error(Reji::InvalidCustomerError)
44
+ end
45
+
46
+ it 'test_stripe_customer_cannot_be_created_when_stripe_id_is_already_set' do
47
+ user = User.new
48
+ user.stripe_id = 'foo'
49
+
50
+ expect {
51
+ user.create_as_stripe_customer
52
+ }.to raise_error(Reji::CustomerAlreadyCreatedError)
53
+ end
54
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Reji::InvoiceLineItem, type: :unit do
6
+ it 'can_calculate_the_inclusive_tax_percentage' do
7
+ customer = User.new
8
+ customer.stripe_id = 'foo'
9
+
10
+ stripe_invoice = Stripe::Invoice.new
11
+ stripe_invoice.customer_tax_exempt = 'none'
12
+ stripe_invoice.customer = 'foo'
13
+
14
+ invoice = Reji::Invoice.new(customer, stripe_invoice)
15
+
16
+ stripe_invoice_line_item = Stripe::InvoiceLineItem.new
17
+ stripe_invoice_line_item.tax_amounts = [
18
+ {:inclusive => true, :tax_rate => self.inclusive_tax_rate(5.0)},
19
+ {:inclusive => true, :tax_rate => self.inclusive_tax_rate(15.0)},
20
+ {:inclusive => false, :tax_rate => self.inclusive_tax_rate(21.0)},
21
+ ]
22
+
23
+ item = Reji::InvoiceLineItem.new(invoice, stripe_invoice_line_item)
24
+
25
+ expect(item.inclusive_tax_percentage).to eq(20)
26
+ end
27
+
28
+ it 'can_calculate_the_exclusive_tax_percentage' do
29
+ customer = User.new
30
+ customer.stripe_id = 'foo'
31
+
32
+ stripe_invoice = Stripe::Invoice.new
33
+ stripe_invoice.customer_tax_exempt = 'none'
34
+ stripe_invoice.customer = 'foo'
35
+
36
+ invoice = Reji::Invoice.new(customer, stripe_invoice)
37
+
38
+ stripe_invoice_line_item = Stripe::InvoiceLineItem.new
39
+ stripe_invoice_line_item.tax_amounts = [
40
+ {:inclusive => true, :tax_rate => self.inclusive_tax_rate(5.0)},
41
+ {:inclusive => false, :tax_rate => self.exclusive_tax_rate(15.0)},
42
+ {:inclusive => false, :tax_rate => self.exclusive_tax_rate(21.0)},
43
+ ]
44
+
45
+ item = Reji::InvoiceLineItem.new(invoice, stripe_invoice_line_item)
46
+
47
+ result = item.exclusive_tax_percentage
48
+
49
+ expect(result).to eq(36)
50
+ end
51
+
52
+ protected
53
+
54
+ # Get a test inclusive Tax Rate.
55
+ def inclusive_tax_rate(percentage)
56
+ self.tax_rate(percentage)
57
+ end
58
+
59
+ # Get a test exclusive Tax Rate.
60
+ def exclusive_tax_rate(percentage)
61
+ self.tax_rate(percentage, false)
62
+ end
63
+
64
+ # Get a test exclusive Tax Rate.
65
+ def tax_rate(percentage, inclusive = true)
66
+ inclusive_tax_rate = Stripe::TaxRate.new
67
+ inclusive_tax_rate.inclusive = inclusive
68
+ inclusive_tax_rate.percentage = percentage
69
+
70
+ inclusive_tax_rate
71
+ end
72
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'invoice', type: :unit do
6
+ it 'can_return_the_invoice_date' do
7
+ stripe_invoice = Stripe::Invoice.new
8
+ stripe_invoice.customer = 'foo'
9
+ stripe_invoice.created = 1560541724
10
+
11
+ user = User.new
12
+ user.stripe_id = 'foo'
13
+
14
+ invoice = Reji::Invoice.new(user, stripe_invoice)
15
+
16
+ date = invoice.date
17
+
18
+ expect(invoice.date.to_i).to eq(1560541724)
19
+ end
20
+
21
+ it 'can_return_its_total' do
22
+ stripe_invoice = Stripe::Invoice.new
23
+ stripe_invoice.customer = 'foo'
24
+ stripe_invoice.total = 1000
25
+ stripe_invoice.currency = 'USD'
26
+
27
+ user = User.new
28
+ user.stripe_id = 'foo'
29
+
30
+ invoice = Reji::Invoice.new(user, stripe_invoice)
31
+
32
+ expect(invoice.total).to eq('$10.00')
33
+ end
34
+
35
+ it 'can_return_its_raw_total' do
36
+ stripe_invoice = Stripe::Invoice.new
37
+ stripe_invoice.customer = 'foo'
38
+ stripe_invoice.total = 1000
39
+ stripe_invoice.currency = 'USD'
40
+
41
+ user = User.new
42
+ user.stripe_id = 'foo'
43
+
44
+ invoice = Reji::Invoice.new(user, stripe_invoice)
45
+
46
+ expect(invoice.raw_total).to eq(1000)
47
+ end
48
+
49
+ it 'returns_a_lower_total_when_there_was_a_starting_balance' do
50
+ stripe_invoice = Stripe::Invoice.new
51
+ stripe_invoice.customer = 'foo'
52
+ stripe_invoice.total = 1000
53
+ stripe_invoice.currency = 'USD'
54
+ stripe_invoice.starting_balance = -450
55
+
56
+ user = User.new
57
+ user.stripe_id = 'foo'
58
+
59
+ invoice = Reji::Invoice.new(user, stripe_invoice)
60
+
61
+ expect(invoice.total).to eq('$5.50')
62
+ end
63
+
64
+ it 'can_return_its_subtotal' do
65
+ stripe_invoice = Stripe::Invoice.new
66
+ stripe_invoice.customer = 'foo'
67
+ stripe_invoice.subtotal = 500
68
+ stripe_invoice.currency = 'USD'
69
+
70
+ user = User.new
71
+ user.stripe_id = 'foo'
72
+
73
+ invoice = Reji::Invoice.new(user, stripe_invoice)
74
+
75
+ expect(invoice.subtotal).to eq('$5.00')
76
+ end
77
+
78
+ it 'can_determine_when_the_customer_has_a_starting_balance' do
79
+ stripe_invoice = Stripe::Invoice.new
80
+ stripe_invoice.customer = 'foo'
81
+ stripe_invoice.starting_balance = -450
82
+
83
+ user = User.new
84
+ user.stripe_id = 'foo'
85
+
86
+ invoice = Reji::Invoice.new(user, stripe_invoice)
87
+
88
+ expect(invoice.has_starting_balance).to be true
89
+ end
90
+
91
+ it 'can_determine_when_the_customer_does_not_have_a_starting_balance' do
92
+ stripe_invoice = Stripe::Invoice.new
93
+ stripe_invoice.customer = 'foo'
94
+ stripe_invoice.starting_balance = 0
95
+
96
+ user = User.new
97
+ user.stripe_id = 'foo'
98
+
99
+ invoice = Reji::Invoice.new(user, stripe_invoice)
100
+
101
+ expect(invoice.has_starting_balance).to be false
102
+ end
103
+
104
+ it 'can_return_its_starting_balance' do
105
+ stripe_invoice = Stripe::Invoice.new
106
+ stripe_invoice.customer = 'foo'
107
+ stripe_invoice.starting_balance = -450
108
+ stripe_invoice.currency = 'USD'
109
+
110
+ user = User.new
111
+ user.stripe_id = 'foo'
112
+
113
+ invoice = Reji::Invoice.new(user, stripe_invoice)
114
+
115
+ expect(invoice.starting_balance).to eq('$-4.50')
116
+ end
117
+
118
+ it 'can_return_its_raw_starting_balance' do
119
+ stripe_invoice = Stripe::Invoice.new
120
+ stripe_invoice.customer = 'foo'
121
+ stripe_invoice.starting_balance = -450
122
+ stripe_invoice.currency = 'USD'
123
+
124
+ user = User.new
125
+ user.stripe_id = 'foo'
126
+
127
+ invoice = Reji::Invoice.new(user, stripe_invoice)
128
+
129
+ expect(invoice.raw_starting_balance).to eq(-450)
130
+ end
131
+
132
+ it 'can_determine_if_it_has_a_discount_applied' do
133
+ coupon = Stripe::Coupon.new
134
+ coupon.amount_off = 50
135
+
136
+ discount = Stripe::Discount.new
137
+ discount.coupon = coupon
138
+
139
+ stripe_invoice = Stripe::Invoice.new
140
+ stripe_invoice.customer = 'foo'
141
+ stripe_invoice.subtotal = 450
142
+ stripe_invoice.total = 500
143
+ stripe_invoice.discount = discount
144
+
145
+ user = User.new
146
+ user.stripe_id = 'foo'
147
+
148
+ invoice = Reji::Invoice.new(user, stripe_invoice)
149
+
150
+ expect(invoice.has_discount).to be true
151
+ end
152
+
153
+ it 'can_return_its_tax' do
154
+ stripe_invoice = Stripe::Invoice.new
155
+ stripe_invoice.customer = 'foo'
156
+ stripe_invoice.tax = 50
157
+ stripe_invoice.currency = 'USD'
158
+
159
+ user = User.new
160
+ user.stripe_id = 'foo'
161
+
162
+ invoice = Reji::Invoice.new(user, stripe_invoice)
163
+
164
+ expect(invoice.tax).to eq('$0.50')
165
+ end
166
+
167
+ it 'can_determine_if_the_customer_was_exempt_from_taxes' do
168
+ stripe_invoice = Stripe::Invoice.new
169
+ stripe_invoice.customer = 'foo'
170
+ stripe_invoice.customer_tax_exempt = 'exempt'
171
+
172
+ user = User.new
173
+ user.stripe_id = 'foo'
174
+
175
+ invoice = Reji::Invoice.new(user, stripe_invoice)
176
+
177
+ expect(invoice.is_tax_exempt).to be true
178
+ end
179
+
180
+ it 'can_determine_if_reverse_charge_applies' do
181
+ stripe_invoice = Stripe::Invoice.new
182
+ stripe_invoice.customer = 'foo'
183
+ stripe_invoice.customer_tax_exempt = 'reverse'
184
+
185
+ user = User.new
186
+ user.stripe_id = 'foo'
187
+
188
+ invoice = Reji::Invoice.new(user, stripe_invoice)
189
+
190
+ expect(invoice.reverse_charge_applies).to be true
191
+ end
192
+ end