reji 1.0.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.
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