reji 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +14 -0
- data/.gitattributes +4 -0
- data/.gitignore +15 -0
- data/.travis.yml +28 -0
- data/Appraisals +17 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +133 -0
- data/LICENSE +20 -0
- data/README.md +1285 -0
- data/Rakefile +21 -0
- data/app/controllers/reji/payment_controller.rb +31 -0
- data/app/controllers/reji/webhook_controller.rb +170 -0
- data/app/views/payment.html.erb +228 -0
- data/app/views/receipt.html.erb +250 -0
- data/bin/setup +12 -0
- data/config/routes.rb +6 -0
- data/gemfiles/rails_5.0.gemfile +13 -0
- data/gemfiles/rails_5.1.gemfile +13 -0
- data/gemfiles/rails_5.2.gemfile +13 -0
- data/gemfiles/rails_6.0.gemfile +13 -0
- data/lib/generators/reji/install/install_generator.rb +69 -0
- data/lib/generators/reji/install/templates/db/migrate/add_reji_to_users.rb.erb +16 -0
- data/lib/generators/reji/install/templates/db/migrate/create_subscription_items.rb.erb +19 -0
- data/lib/generators/reji/install/templates/db/migrate/create_subscriptions.rb.erb +22 -0
- data/lib/generators/reji/install/templates/reji.rb +36 -0
- data/lib/reji.rb +75 -0
- data/lib/reji/billable.rb +13 -0
- data/lib/reji/concerns/interacts_with_payment_behavior.rb +33 -0
- data/lib/reji/concerns/manages_customer.rb +113 -0
- data/lib/reji/concerns/manages_invoices.rb +136 -0
- data/lib/reji/concerns/manages_payment_methods.rb +202 -0
- data/lib/reji/concerns/manages_subscriptions.rb +91 -0
- data/lib/reji/concerns/performs_charges.rb +36 -0
- data/lib/reji/concerns/prorates.rb +38 -0
- data/lib/reji/configuration.rb +59 -0
- data/lib/reji/engine.rb +4 -0
- data/lib/reji/errors.rb +66 -0
- data/lib/reji/invoice.rb +243 -0
- data/lib/reji/invoice_line_item.rb +98 -0
- data/lib/reji/payment.rb +61 -0
- data/lib/reji/payment_method.rb +32 -0
- data/lib/reji/subscription.rb +567 -0
- data/lib/reji/subscription_builder.rb +206 -0
- data/lib/reji/subscription_item.rb +97 -0
- data/lib/reji/tax.rb +48 -0
- data/lib/reji/version.rb +5 -0
- data/reji.gemspec +32 -0
- data/spec/dummy/app/models/user.rb +21 -0
- data/spec/dummy/application.rb +53 -0
- data/spec/dummy/config/database.yml +11 -0
- data/spec/dummy/db/schema.rb +40 -0
- data/spec/feature/charges_spec.rb +67 -0
- data/spec/feature/customer_spec.rb +23 -0
- data/spec/feature/invoices_spec.rb +73 -0
- data/spec/feature/multiplan_subscriptions_spec.rb +319 -0
- data/spec/feature/payment_methods_spec.rb +149 -0
- data/spec/feature/pending_updates_spec.rb +77 -0
- data/spec/feature/subscriptions_spec.rb +650 -0
- data/spec/feature/webhooks_spec.rb +162 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/feature_helpers.rb +39 -0
- data/spec/unit/customer_spec.rb +54 -0
- data/spec/unit/invoice_line_item_spec.rb +72 -0
- data/spec/unit/invoice_spec.rb +192 -0
- data/spec/unit/payment_spec.rb +33 -0
- data/spec/unit/subscription_spec.rb +103 -0
- 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
|
+
|
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>
|
data/bin/setup
ADDED
@@ -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
|
data/config/routes.rb
ADDED
@@ -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,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
|