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