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,250 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+
7
+ <title>Invoice</title>
8
+
9
+ <style>
10
+ body {
11
+ background: #fff none;
12
+ font-size: 12px;
13
+ }
14
+
15
+ h2 {
16
+ font-size: 28px;
17
+ color: #ccc;
18
+ }
19
+
20
+ .container {
21
+ padding-top: 30px;
22
+ }
23
+
24
+ .invoice-head td {
25
+ padding: 0 8px;
26
+ }
27
+
28
+ .table th {
29
+ vertical-align: bottom;
30
+ font-weight: bold;
31
+ padding: 8px;
32
+ line-height: 20px;
33
+ text-align: left;
34
+ border-bottom: 1px solid #ddd;
35
+ }
36
+
37
+ .table tr.row td {
38
+ border-bottom: 1px solid #ddd;
39
+ }
40
+
41
+ .table td {
42
+ padding: 8px;
43
+ line-height: 20px;
44
+ text-align: left;
45
+ vertical-align: top;
46
+ }
47
+ </style>
48
+ </head>
49
+ <body>
50
+ <div class="container">
51
+ <table style="margin-left: auto; margin-right: auto;" width="550">
52
+ <tr>
53
+ <td width="160">
54
+ &nbsp;
55
+ </td>
56
+
57
+ <!-- Organization Name / Image -->
58
+ <td align="right">
59
+ <strong><%= local_assigns[:header] ? header : vendor %></strong>
60
+ </td>
61
+ </tr>
62
+
63
+ <tr valign="top">
64
+ <td style="font-size: 28px; color: #ccc;">
65
+ Receipt
66
+ </td>
67
+
68
+ <!-- Organization Name / Date -->
69
+ <td>
70
+ <br><br>
71
+ <strong>To:</strong> <%= owner.stripe_email ? owner.stripe_email : owner.name %>
72
+ <br>
73
+ <strong>Date:</strong> <%= invoice.date %>
74
+ </td>
75
+ </tr>
76
+
77
+ <tr valign="top">
78
+ <!-- Organization Details -->
79
+ <td style="font-size:9px;">
80
+ <%= vendor %><br>
81
+
82
+ <% if local_assigns[:street] %>
83
+ <%= street %><br>
84
+ <% end %>
85
+
86
+ <% if local_assigns[:location] %>
87
+ <%= location %><br>
88
+ <% end %>
89
+
90
+ <% if local_assigns[:phone] %>
91
+ <strong>T</strong> <%= phone %><br>
92
+ <% end %>
93
+
94
+ <% if local_assigns[:vendor_vat] %>
95
+ <%= vendor_vat %><br>
96
+ <% end %>
97
+
98
+ <% if local_assigns[:url] %>
99
+ <a href="<%= url %>"><%= url %></a>
100
+ <% end %>
101
+ </td>
102
+ <td>
103
+ <!-- Invoice Info -->
104
+ <p>
105
+ <strong>Product:</strong> <%= product %><br>
106
+ <strong>Invoice Number:</strong> <%= local_assigns[:id] ? id : invoice.number %><br>
107
+ </p>
108
+
109
+ <% if local_assigns[:vat] %>
110
+ <p><%= vat %></p>
111
+ <% end %>
112
+
113
+ <br><br>
114
+
115
+ <!-- Invoice Table -->
116
+ <table width="100%" class="table" border="0">
117
+ <tr>
118
+ <th align="left">Description</th>
119
+ <th align="right">Date</th>
120
+
121
+ <% if invoice.has_tax %>
122
+ <th align="right">Tax</th>
123
+ <% end %>
124
+
125
+ <th align="right">Amount</th>
126
+ </tr>
127
+
128
+ <!-- Display The Invoice Items -->
129
+ <% invoice.invoice_items.each do |item| %>
130
+ <tr class="row">
131
+ <td colspan="2"><%= item.description %></td>
132
+ <% if invoice.has_tax %>
133
+ <td>
134
+ <% if item.inclusive_tax_percentage > 0 %>
135
+ <%= item.inclusive_tax_percentage %>% incl.
136
+ <% end %>
137
+
138
+ <% if item.has_both_inclusive_and_exclusive_tax > 0 %>
139
+ +
140
+ <% end %>
141
+
142
+ <% if item.exclusive_tax_percentage > 0 %>
143
+ <%= item.exclusive_tax_percentage %>%
144
+ <% end %>
145
+ </td>
146
+ <% end %>
147
+ <td><%= item.total %></td>
148
+ </tr>
149
+ <% end %>
150
+
151
+ <!-- Display The Subscriptions -->
152
+ <% invoice.subscriptions.each do |subscription| %>
153
+ <tr class="row">
154
+ <td>Subscription <%= subscription.quantity %></td>
155
+ <td>
156
+ <%= subscription.start_date %>
157
+ <%= subscription.end_date %>
158
+ </td>
159
+ <% if invoice.has_tax %>
160
+ <td>
161
+ <% if subscription.inclusive_tax_percentage > 0 %>
162
+ <%= subscription.inclusive_tax_percentage %>% incl.
163
+ <% end %>
164
+
165
+ <% if subscription.has_both_inclusive_and_exclusive_tax > 0 %>
166
+ +
167
+ <% end %>
168
+
169
+ <% if subscription.exclusive_tax_percentage > 0 %>
170
+ <%= subscription.exclusive_tax_percentage %>%
171
+ <% end %>
172
+ </td>
173
+ <% end %>
174
+ <td><%= subscription.total %></td>
175
+ </tr>
176
+ <% end %>
177
+
178
+ <!-- Display The Subtotal -->
179
+ <% if invoice.has_discount || invoice.has_tax || invoice.has_starting_balance %>
180
+ <tr>
181
+ <td colspan="<%= invoice.has_tax ? 3 : 2 %>" style="text-align: right;">Subtotal</td>
182
+ <td><%= invoice.subtotal %></td>
183
+ </tr>
184
+ <% end %>
185
+
186
+ <!-- Display The Discount -->
187
+ <% if invoice.has_discount %>
188
+ <tr>
189
+ <td colspan="<%= invoice.has_tax ? 3 : 2 %>" style="text-align: right;">
190
+ <% if invoice.discount_is_percentage %>
191
+ <%= invoice.coupon %> (<%= invoice.percent_off %>% Off)
192
+ <% else %>
193
+ <%= invoice.coupon %> (<%= invoice.amount_off %> Off)
194
+ <% end %>
195
+ </td>
196
+
197
+ <td>-<%= invoice.discount %></td>
198
+ </tr>
199
+ <% end %>
200
+
201
+ <!-- Display The Taxes -->
202
+ <% unless invoice.is_not_tax_exempt %>
203
+ <tr>
204
+ <td colspan="<%= invoice.has_tax ? 3 : 2 %>" style="text-align: right;">
205
+ <% if invoice.is_tax_exempt %>
206
+ Tax is exempted
207
+ <% else %>
208
+ Tax to be paid on reverse charge basis
209
+ <% end %>
210
+ </td>
211
+ <td></td>
212
+ </tr>
213
+ <% else %>
214
+ <% invoice.taxes.each do |tax| %>
215
+ <tr>
216
+ <td colspan="3" style="text-align: right;">
217
+ <%= tax.display_name %> <%= tax.jurisdiction ? ' - '.tax.jurisdiction : '' %>
218
+ (<%= tax.percentage %>%<%= tax.is_inclusive ? ' incl.' : '' %>)
219
+ </td>
220
+ <td><%= tax.amount %></td>
221
+ </tr>
222
+ <% end %>
223
+ <% end %>
224
+
225
+ <!-- Starting Balance -->
226
+ <% if invoice.has_starting_balance %>
227
+ <tr>
228
+ <td colspan="<%= invoice.has_tax ? 3 : 2 %>" style="text-align: right;">
229
+ Customer Balance
230
+ </td>
231
+ <td><%= invoice.starting_balance %></td>
232
+ </tr>
233
+ <% end %>
234
+
235
+ <!-- Display The Final Total -->
236
+ <tr>
237
+ <td colspan="<%= invoice.has_tax ? 3 : 2 %>" style="text-align: right;">
238
+ <strong>Total</strong>
239
+ </td>
240
+ <td>
241
+ <strong><%= invoice.total %></strong>
242
+ </td>
243
+ </tr>
244
+ </table>
245
+ </td>
246
+ </tr>
247
+ </table>
248
+ </div>
249
+ </body>
250
+ </html>
@@ -0,0 +1,12 @@
1
+ #!/bin/sh
2
+
3
+ set -e
4
+
5
+ # Install required gems, including Appraisal, which helps us test against
6
+ # multiple Rails versions
7
+ gem install bundler --conservative
8
+ bundle check || bundle install
9
+
10
+ if [ -z "$CI" ]; then
11
+ bundle exec appraisal install
12
+ fi
@@ -0,0 +1,6 @@
1
+ Rails.application.routes.draw do
2
+ scope 'stripe', as: 'stripe' do
3
+ get 'payment/:id', to: 'reji/payment#show', as: 'payment'
4
+ post 'webhook', to: 'reji/webhook#handle_webhook', as: 'webhook'
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "money"
7
+ gem "pry", require: false
8
+ gem "rspec-rails", "~> 3.1"
9
+ gem "stripe"
10
+ gem "sqlite3", "~> 1.3.13"
11
+ gem "railties", "~> 5.0"
12
+
13
+ gemspec path: "../"
@@ -0,0 +1,13 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "money"
7
+ gem "pry", require: false
8
+ gem "rspec-rails"
9
+ gem "stripe"
10
+ gem "sqlite3"
11
+ gem "railties", "~> 5.1"
12
+
13
+ gemspec path: "../"
@@ -0,0 +1,13 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "money"
7
+ gem "pry", require: false
8
+ gem "rspec-rails"
9
+ gem "stripe"
10
+ gem "sqlite3"
11
+ gem "railties", "~> 5.2"
12
+
13
+ gemspec path: "../"
@@ -0,0 +1,13 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "money"
7
+ gem "pry", require: false
8
+ gem "rspec-rails"
9
+ gem "stripe"
10
+ gem "sqlite3"
11
+ gem "railties", "~> 6.0"
12
+
13
+ gemspec path: "../"
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/base'
4
+ require 'rails/generators/active_record'
5
+
6
+ module Reji
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ source_root(File.expand_path('../templates', __FILE__))
12
+
13
+ def create_reji_initializer
14
+ copy_file('reji.rb', 'config/initializers/reji.rb')
15
+ end
16
+
17
+ def create_reji_migration
18
+ copy_migration('add_reji_to_users')
19
+ copy_migration('create_subscriptions')
20
+ copy_migration('create_subscription_items')
21
+ end
22
+
23
+ private
24
+
25
+ def copy_migration(migration_name, config = {})
26
+ unless migration_exists?(migration_name)
27
+ migration_template(
28
+ "db/migrate/#{migration_name}.rb.erb",
29
+ "db/migrate/#{migration_name}.rb",
30
+ config.merge(migration_version: migration_version),
31
+ )
32
+ end
33
+ end
34
+
35
+ def migration_exists?(name)
36
+ existing_migrations.include?(name)
37
+ end
38
+
39
+ def existing_migrations
40
+ @existing_migrations ||= Dir.glob('db/migrate/*.rb').map do |file|
41
+ migration_name_without_timestamp(file)
42
+ end
43
+ end
44
+
45
+ def migration_name_without_timestamp(file)
46
+ file.sub(%r{^.*(db/migrate/)(?:\d+_)?}, '')
47
+ end
48
+
49
+ # for generating a timestamp when using `create_migration`
50
+ def self.next_migration_number(dir)
51
+ ActiveRecord::Generators::Base.next_migration_number(dir)
52
+ end
53
+
54
+ def migration_version
55
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
56
+ end
57
+
58
+ def migration_primary_key_type_string
59
+ if configured_key_type
60
+ ", id: :#{configured_key_type}"
61
+ end
62
+ end
63
+
64
+ def configured_key_type
65
+ Rails.configuration.generators.active_record[:primary_key_type]
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,16 @@
1
+ class AddRejiToUsers < ActiveRecord::Migration<%= migration_version %>
2
+ def self.up
3
+ change_table :users do |t|
4
+ t.string :stripe_id
5
+ t.string :card_brand
6
+ t.string :card_last_four, limit: 4
7
+ t.timestamp :trial_ends_at
8
+ end
9
+ end
10
+
11
+ def self.down
12
+ change_table :users do |t|
13
+ t.remove :stripe_id, :card_brand, :card_last_four, :trial_ends_at
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ class CreateSubscriptionItems < ActiveRecord::Migration<%= migration_version %>
2
+ def self.up
3
+ create_table :subscription_items<%= migration_primary_key_type_string %> do |t|
4
+ t.bigint :subscription_id, null: false
5
+ t.string :stripe_id, null: false
6
+ t.string :stripe_plan, null: false
7
+ t.integer :quantity, null: false
8
+
9
+ t.timestamps
10
+
11
+ t.index :stripe_id
12
+ t.index [:subscription_id, :stripe_plan], unique: true
13
+ end
14
+ end
15
+
16
+ def self.down
17
+ drop_tables(:subscription_items, {:if_exists => true})
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ class CreateSubscriptions < ActiveRecord::Migration<%= migration_version %>
2
+ def self.up
3
+ create_table :subscriptions<%= migration_primary_key_type_string %> do |t|
4
+ t.bigint :user_id, null: false
5
+ t.string :name, null: false
6
+ t.string :stripe_id, null: false
7
+ t.string :stripe_status, null: false
8
+ t.string :stripe_plan
9
+ t.integer :quantity
10
+ t.timestamp :trial_ends_at
11
+ t.timestamp :ends_at
12
+
13
+ t.timestamps
14
+
15
+ t.index [:user_id, :stripe_status]
16
+ end
17
+ end
18
+
19
+ def self.down
20
+ drop_tables(:subscriptions, {:if_exists => true})
21
+ end
22
+ end