rsb-entitlements 0.9.1
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/LICENSE +15 -0
- data/README.md +73 -0
- data/Rakefile +25 -0
- data/app/controllers/rsb/entitlements/admin/payment_requests_controller.rb +112 -0
- data/app/controllers/rsb/entitlements/admin/plans_controller.rb +91 -0
- data/app/controllers/rsb/entitlements/admin/usage_counters_controller.rb +69 -0
- data/app/jobs/rsb/entitlements/application_job.rb +8 -0
- data/app/jobs/rsb/entitlements/entitlement_expiration_job.rb +15 -0
- data/app/jobs/rsb/entitlements/payment_request_expiration_job.rb +31 -0
- data/app/models/concerns/rsb/entitlements/entitleable.rb +210 -0
- data/app/models/rsb/entitlements/application_record.rb +10 -0
- data/app/models/rsb/entitlements/entitlement.rb +68 -0
- data/app/models/rsb/entitlements/payment_request.rb +70 -0
- data/app/models/rsb/entitlements/plan.rb +83 -0
- data/app/models/rsb/entitlements/usage_counter.rb +64 -0
- data/app/services/rsb/entitlements/usage_counter_service.rb +94 -0
- data/app/views/rsb/entitlements/admin/payment_requests/index.html.erb +98 -0
- data/app/views/rsb/entitlements/admin/payment_requests/show.html.erb +137 -0
- data/app/views/rsb/entitlements/admin/plans/_form.html.erb +202 -0
- data/app/views/rsb/entitlements/admin/plans/edit.html.erb +9 -0
- data/app/views/rsb/entitlements/admin/plans/index.html.erb +74 -0
- data/app/views/rsb/entitlements/admin/plans/new.html.erb +9 -0
- data/app/views/rsb/entitlements/admin/plans/show.html.erb +94 -0
- data/app/views/rsb/entitlements/admin/usage_counters/index.html.erb +110 -0
- data/app/views/rsb/entitlements/admin/usage_counters/trend.html.erb +57 -0
- data/config/locales/admin.en.yml +25 -0
- data/db/migrate/20260208200001_create_rsb_entitlements_plans.rb +21 -0
- data/db/migrate/20260208200002_create_rsb_entitlements_entitlements.rb +23 -0
- data/db/migrate/20260208200003_create_rsb_entitlements_usage_counters.rb +21 -0
- data/db/migrate/20260208200004_create_rsb_entitlements_payment_requests.rb +37 -0
- data/db/migrate/20260213000001_rework_usage_counters_to_ledger.rb +81 -0
- data/lib/generators/rsb/entitlements/install/install_generator.rb +26 -0
- data/lib/rsb/entitlements/configuration.rb +19 -0
- data/lib/rsb/entitlements/engine.rb +134 -0
- data/lib/rsb/entitlements/payment_provider/base.rb +148 -0
- data/lib/rsb/entitlements/payment_provider/wire.rb +188 -0
- data/lib/rsb/entitlements/period_key_calculator.rb +57 -0
- data/lib/rsb/entitlements/provider_definition.rb +43 -0
- data/lib/rsb/entitlements/provider_registry.rb +145 -0
- data/lib/rsb/entitlements/settings_schema.rb +47 -0
- data/lib/rsb/entitlements/test_helper.rb +114 -0
- data/lib/rsb/entitlements/version.rb +9 -0
- data/lib/rsb/entitlements.rb +39 -0
- metadata +116 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<%= form_with model: plan, url: url, method: method, local: true, scope: :plan do |f| %>
|
|
2
|
+
<% if plan.errors.any? %>
|
|
3
|
+
<div class="mb-4 p-4 bg-rsb-danger-bg border border-rsb-danger rounded-rsb">
|
|
4
|
+
<strong class="text-rsb-danger-text"><%= pluralize(plan.errors.count, "error") %> prohibited this plan from being saved:</strong>
|
|
5
|
+
<ul class="mt-2 pl-5 list-disc text-rsb-danger-text text-sm">
|
|
6
|
+
<% plan.errors.full_messages.each do |message| %>
|
|
7
|
+
<li><%= message %></li>
|
|
8
|
+
<% end %>
|
|
9
|
+
</ul>
|
|
10
|
+
</div>
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
<%# Basic Fields %>
|
|
14
|
+
<div class="mb-4">
|
|
15
|
+
<%= f.label :name, class: "block text-sm font-medium mb-1" %>
|
|
16
|
+
<%= f.text_field :name, required: true, placeholder: "Pro Plan", id: "plan_name_field",
|
|
17
|
+
class: "w-full px-3 py-2 border border-rsb-border rounded-rsb text-sm bg-rsb-card focus:outline-none focus:ring-2 focus:ring-rsb-primary" %>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="mb-4">
|
|
21
|
+
<%= f.label :slug, class: "block text-sm font-medium mb-1" %>
|
|
22
|
+
<%= f.text_field :slug, required: true, placeholder: "pro-plan", id: "plan_slug_field",
|
|
23
|
+
class: "w-full px-3 py-2 border border-rsb-border rounded-rsb text-sm bg-rsb-card focus:outline-none focus:ring-2 focus:ring-rsb-primary" %>
|
|
24
|
+
<p class="mt-1 text-xs text-rsb-muted">Auto-generated from name. Use lowercase letters, numbers, hyphens, and underscores only.</p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="mb-4">
|
|
28
|
+
<%= f.label :interval, class: "block text-sm font-medium mb-1" %>
|
|
29
|
+
<%= f.select :interval, [["Monthly", "monthly"], ["Yearly", "yearly"], ["Lifetime", "lifetime"], ["One-time", "one_time"]],
|
|
30
|
+
{}, class: "w-full px-3 py-2 border border-rsb-border rounded-rsb text-sm bg-rsb-card focus:outline-none focus:ring-2 focus:ring-rsb-primary" %>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="mb-4">
|
|
34
|
+
<%= f.label :price_cents, "Price (cents)", class: "block text-sm font-medium mb-1" %>
|
|
35
|
+
<%= f.number_field :price_cents, required: true, min: 0, placeholder: "2900",
|
|
36
|
+
class: "w-full px-3 py-2 border border-rsb-border rounded-rsb text-sm bg-rsb-card focus:outline-none focus:ring-2 focus:ring-rsb-primary" %>
|
|
37
|
+
<p class="mt-1 text-xs text-rsb-muted">Enter price in cents. Example: 2900 = $29.00</p>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="mb-4">
|
|
41
|
+
<%= f.label :currency, class: "block text-sm font-medium mb-1" %>
|
|
42
|
+
<%= f.text_field :currency, required: true, placeholder: "usd",
|
|
43
|
+
class: "w-full px-3 py-2 border border-rsb-border rounded-rsb text-sm bg-rsb-card focus:outline-none focus:ring-2 focus:ring-rsb-primary" %>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="mb-4">
|
|
47
|
+
<label class="flex items-center gap-2">
|
|
48
|
+
<%= f.check_box :active, class: "rounded border-rsb-border" %>
|
|
49
|
+
<span class="text-sm">Active plan</span>
|
|
50
|
+
</label>
|
|
51
|
+
<p class="mt-1 text-xs text-rsb-muted">Make this plan available for selection.</p>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<%# Features Section (dynamic) %>
|
|
55
|
+
<div class="mb-4">
|
|
56
|
+
<label class="block text-sm font-semibold mb-2">Features</label>
|
|
57
|
+
<div id="features-container" class="space-y-2">
|
|
58
|
+
<% if plan.features.present? && plan.features.any? %>
|
|
59
|
+
<% plan.features.each do |key, value| %>
|
|
60
|
+
<div class="flex items-center gap-2">
|
|
61
|
+
<input type="text" value="<%= key %>" placeholder="Feature name" readonly
|
|
62
|
+
class="flex-1 px-3 py-2 border border-rsb-border rounded-rsb text-sm bg-rsb-bg">
|
|
63
|
+
<label class="flex items-center gap-1 whitespace-nowrap">
|
|
64
|
+
<input type="hidden" name="plan[features][<%= key %>]" value="false">
|
|
65
|
+
<input type="checkbox" name="plan[features][<%= key %>]" value="true" <%= 'checked' if value %> class="rounded border-rsb-border">
|
|
66
|
+
<span class="text-sm">Enabled</span>
|
|
67
|
+
</label>
|
|
68
|
+
<button type="button" onclick="this.parentElement.remove()"
|
|
69
|
+
class="px-2 py-1 bg-rsb-danger text-white rounded-rsb text-xs font-medium hover:opacity-90 transition-colors">Remove</button>
|
|
70
|
+
</div>
|
|
71
|
+
<% end %>
|
|
72
|
+
<% end %>
|
|
73
|
+
</div>
|
|
74
|
+
<button type="button" id="add-feature-btn"
|
|
75
|
+
class="mt-2 inline-flex items-center px-3 py-1.5 border border-rsb-border text-rsb-text rounded-rsb text-xs font-medium hover:bg-rsb-bg transition-colors">+ Add Feature</button>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<%# Limits Section (dynamic) %>
|
|
79
|
+
<div class="mb-4">
|
|
80
|
+
<label class="block text-sm font-semibold mb-2">Limits</label>
|
|
81
|
+
<div id="limits-container" class="space-y-2">
|
|
82
|
+
<% if plan.limits.present? && plan.limits.any? %>
|
|
83
|
+
<% plan.limits.each do |key, value| %>
|
|
84
|
+
<div class="flex items-center gap-2">
|
|
85
|
+
<input type="text" value="<%= key %>" placeholder="Limit name" readonly
|
|
86
|
+
class="flex-1 px-3 py-2 border border-rsb-border rounded-rsb text-sm bg-rsb-bg">
|
|
87
|
+
<input type="number" name="plan[limits][<%= key %>]" value="<%= value %>" placeholder="Value"
|
|
88
|
+
class="w-30 px-3 py-2 border border-rsb-border rounded-rsb text-sm">
|
|
89
|
+
<button type="button" onclick="this.parentElement.remove()"
|
|
90
|
+
class="px-2 py-1 bg-rsb-danger text-white rounded-rsb text-xs font-medium hover:opacity-90 transition-colors">Remove</button>
|
|
91
|
+
</div>
|
|
92
|
+
<% end %>
|
|
93
|
+
<% end %>
|
|
94
|
+
</div>
|
|
95
|
+
<button type="button" id="add-limit-btn"
|
|
96
|
+
class="mt-2 inline-flex items-center px-3 py-1.5 border border-rsb-border text-rsb-text rounded-rsb text-xs font-medium hover:bg-rsb-bg transition-colors">+ Add Limit</button>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<%# Description (from metadata) %>
|
|
100
|
+
<div class="mb-4">
|
|
101
|
+
<label for="plan_metadata_description" class="block text-sm font-medium mb-1">Description</label>
|
|
102
|
+
<textarea name="plan[metadata][description]" id="plan_metadata_description" rows="3"
|
|
103
|
+
placeholder="Describe this plan..."
|
|
104
|
+
class="w-full px-3 py-2 border border-rsb-border rounded-rsb text-sm bg-rsb-card focus:outline-none focus:ring-2 focus:ring-rsb-primary"><%= plan.metadata&.dig("description") %></textarea>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<%# Actions %>
|
|
108
|
+
<div class="flex gap-3 mt-6">
|
|
109
|
+
<button type="submit"
|
|
110
|
+
class="px-4 py-2 bg-rsb-primary text-rsb-primary-text rounded-rsb text-sm font-medium hover:bg-rsb-primary-hover transition-colors"><%= plan.new_record? ? "Create Plan" : "Update Plan" %></button>
|
|
111
|
+
<a href="/admin/plans"
|
|
112
|
+
class="px-4 py-2 border border-rsb-border text-rsb-text rounded-rsb text-sm hover:bg-rsb-bg transition-colors">Cancel</a>
|
|
113
|
+
</div>
|
|
114
|
+
<% end %>
|
|
115
|
+
|
|
116
|
+
<script>
|
|
117
|
+
// Auto-generate slug from name
|
|
118
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
119
|
+
var nameField = document.getElementById('plan_name_field');
|
|
120
|
+
var slugField = document.getElementById('plan_slug_field');
|
|
121
|
+
|
|
122
|
+
if (nameField && slugField) {
|
|
123
|
+
nameField.addEventListener('input', function() {
|
|
124
|
+
if (!slugField.dataset.manuallyEdited) {
|
|
125
|
+
var slug = this.value
|
|
126
|
+
.toLowerCase()
|
|
127
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
128
|
+
.replace(/^-|-$/g, '');
|
|
129
|
+
slugField.value = slug;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
slugField.addEventListener('input', function() {
|
|
134
|
+
slugField.dataset.manuallyEdited = 'true';
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Add feature row
|
|
140
|
+
document.getElementById('add-feature-btn').addEventListener('click', function() {
|
|
141
|
+
var container = document.getElementById('features-container');
|
|
142
|
+
var timestamp = Date.now();
|
|
143
|
+
var row = document.createElement('div');
|
|
144
|
+
row.className = 'flex items-center gap-2';
|
|
145
|
+
row.innerHTML =
|
|
146
|
+
'<input type="text" id="feature-key-' + timestamp + '" placeholder="Feature name (e.g., sso)" ' +
|
|
147
|
+
'class="flex-1 px-3 py-2 border border-rsb-border rounded-rsb text-sm">' +
|
|
148
|
+
'<label class="flex items-center gap-1 whitespace-nowrap">' +
|
|
149
|
+
'<input type="hidden" id="feature-hidden-' + timestamp + '" value="false">' +
|
|
150
|
+
'<input type="checkbox" id="feature-value-' + timestamp + '" value="true" class="rounded border-rsb-border">' +
|
|
151
|
+
'<span class="text-sm">Enabled</span>' +
|
|
152
|
+
'</label>' +
|
|
153
|
+
'<button type="button" onclick="this.parentElement.remove()" ' +
|
|
154
|
+
'class="px-2 py-1 bg-rsb-danger text-white rounded-rsb text-xs font-medium hover:opacity-90 transition-colors">Remove</button>';
|
|
155
|
+
container.appendChild(row);
|
|
156
|
+
|
|
157
|
+
// When the key input changes, update the name attributes
|
|
158
|
+
var keyInput = row.querySelector('#feature-key-' + timestamp);
|
|
159
|
+
var hiddenInput = row.querySelector('#feature-hidden-' + timestamp);
|
|
160
|
+
var checkboxInput = row.querySelector('#feature-value-' + timestamp);
|
|
161
|
+
|
|
162
|
+
keyInput.addEventListener('input', function() {
|
|
163
|
+
var key = this.value.trim();
|
|
164
|
+
if (key) {
|
|
165
|
+
hiddenInput.name = 'plan[features][' + key + ']';
|
|
166
|
+
checkboxInput.name = 'plan[features][' + key + ']';
|
|
167
|
+
} else {
|
|
168
|
+
hiddenInput.name = '';
|
|
169
|
+
checkboxInput.name = '';
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Add limit row
|
|
175
|
+
document.getElementById('add-limit-btn').addEventListener('click', function() {
|
|
176
|
+
var container = document.getElementById('limits-container');
|
|
177
|
+
var timestamp = Date.now();
|
|
178
|
+
var row = document.createElement('div');
|
|
179
|
+
row.className = 'flex items-center gap-2';
|
|
180
|
+
row.innerHTML =
|
|
181
|
+
'<input type="text" id="limit-key-' + timestamp + '" placeholder="Limit name (e.g., api_calls)" ' +
|
|
182
|
+
'class="flex-1 px-3 py-2 border border-rsb-border rounded-rsb text-sm">' +
|
|
183
|
+
'<input type="number" id="limit-value-' + timestamp + '" placeholder="Value" ' +
|
|
184
|
+
'class="w-30 px-3 py-2 border border-rsb-border rounded-rsb text-sm">' +
|
|
185
|
+
'<button type="button" onclick="this.parentElement.remove()" ' +
|
|
186
|
+
'class="px-2 py-1 bg-rsb-danger text-white rounded-rsb text-xs font-medium hover:opacity-90 transition-colors">Remove</button>';
|
|
187
|
+
container.appendChild(row);
|
|
188
|
+
|
|
189
|
+
// When the key input changes, update the name attribute on the value input
|
|
190
|
+
var keyInput = row.querySelector('#limit-key-' + timestamp);
|
|
191
|
+
var valueInput = row.querySelector('#limit-value-' + timestamp);
|
|
192
|
+
|
|
193
|
+
keyInput.addEventListener('input', function() {
|
|
194
|
+
var key = this.value.trim();
|
|
195
|
+
if (key) {
|
|
196
|
+
valueInput.name = 'plan[limits][' + key + ']';
|
|
197
|
+
} else {
|
|
198
|
+
valueInput.name = '';
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
</script>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<div class="flex justify-between items-center mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold">Edit <%= @plan.name %></h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6">
|
|
6
|
+
<%= render "form", plan: @plan,
|
|
7
|
+
url: "/admin/plans/#{@plan.id}",
|
|
8
|
+
method: :patch %>
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<div class="flex justify-between items-center mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold">Plans</h1>
|
|
3
|
+
<div class="flex gap-2">
|
|
4
|
+
<a href="/admin/plans/new"
|
|
5
|
+
class="inline-flex items-center gap-1 px-4 py-2 bg-rsb-primary text-rsb-primary-text rounded-rsb text-sm font-medium hover:bg-rsb-primary-hover transition-colors">New Plan</a>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm overflow-hidden">
|
|
10
|
+
<% if @plans.any? %>
|
|
11
|
+
<div class="overflow-x-auto">
|
|
12
|
+
<table class="w-full">
|
|
13
|
+
<thead>
|
|
14
|
+
<tr>
|
|
15
|
+
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">Name</th>
|
|
16
|
+
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">Slug</th>
|
|
17
|
+
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">Interval</th>
|
|
18
|
+
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">Price</th>
|
|
19
|
+
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">Features</th>
|
|
20
|
+
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">Limits</th>
|
|
21
|
+
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">Status</th>
|
|
22
|
+
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border"></th>
|
|
23
|
+
</tr>
|
|
24
|
+
</thead>
|
|
25
|
+
<tbody>
|
|
26
|
+
<% @plans.each do |plan| %>
|
|
27
|
+
<tr class="hover:bg-rsb-bg border-b border-rsb-border last:border-b-0">
|
|
28
|
+
<td class="px-4 py-3 text-sm">
|
|
29
|
+
<a href="/admin/plans/<%= plan.id %>" class="text-rsb-primary hover:underline"><%= plan.name %></a>
|
|
30
|
+
</td>
|
|
31
|
+
<td class="px-4 py-3 text-sm text-rsb-muted"><%= plan.slug %></td>
|
|
32
|
+
<td class="px-4 py-3 text-sm">
|
|
33
|
+
<span class="inline-block px-2.5 py-0.5 rounded-rsb bg-rsb-info-bg text-rsb-info-text text-xs font-medium"><%= plan.interval.titleize %></span>
|
|
34
|
+
</td>
|
|
35
|
+
<td class="px-4 py-3 text-sm">$<%= "%.2f" % (plan.price_cents / 100.0) %> <%= plan.currency.upcase %></td>
|
|
36
|
+
<td class="px-4 py-3 text-sm"><%= pluralize(plan.features.size, "feature") %></td>
|
|
37
|
+
<td class="px-4 py-3 text-sm"><%= pluralize(plan.limits.size, "limit") %></td>
|
|
38
|
+
<td class="px-4 py-3 text-sm">
|
|
39
|
+
<% if plan.active? %>
|
|
40
|
+
<span class="inline-block px-2.5 py-0.5 rounded-rsb bg-rsb-success-bg text-rsb-success-text text-xs font-medium">Active</span>
|
|
41
|
+
<% else %>
|
|
42
|
+
<span class="inline-block px-2.5 py-0.5 rounded-rsb bg-rsb-danger-bg text-rsb-danger-text text-xs font-medium">Inactive</span>
|
|
43
|
+
<% end %>
|
|
44
|
+
</td>
|
|
45
|
+
<td class="px-4 py-3 text-sm text-right">
|
|
46
|
+
<a href="/admin/plans/<%= plan.id %>/edit"
|
|
47
|
+
class="inline-flex items-center gap-1 px-3 py-1.5 border border-rsb-border text-rsb-text rounded-rsb text-xs font-medium hover:bg-rsb-bg transition-colors">Edit</a>
|
|
48
|
+
</td>
|
|
49
|
+
</tr>
|
|
50
|
+
<% end %>
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<%# Pagination %>
|
|
56
|
+
<div class="flex justify-between items-center px-4 py-3 border-t border-rsb-border">
|
|
57
|
+
<span class="text-sm text-rsb-muted">Page <%= @current_page + 1 %></span>
|
|
58
|
+
<div class="flex gap-2">
|
|
59
|
+
<% if @current_page > 0 %>
|
|
60
|
+
<a href="/admin/plans?page=<%= @current_page - 1 %>"
|
|
61
|
+
class="inline-flex items-center px-3 py-1.5 border border-rsb-border text-rsb-text rounded-rsb text-xs font-medium hover:bg-rsb-bg transition-colors">Previous</a>
|
|
62
|
+
<% end %>
|
|
63
|
+
<% if @plans.size == @per_page %>
|
|
64
|
+
<a href="/admin/plans?page=<%= @current_page + 1 %>"
|
|
65
|
+
class="inline-flex items-center px-3 py-1.5 border border-rsb-border text-rsb-text rounded-rsb text-xs font-medium hover:bg-rsb-bg transition-colors">Next</a>
|
|
66
|
+
<% end %>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<% else %>
|
|
70
|
+
<div class="p-8 text-center">
|
|
71
|
+
<p class="text-rsb-muted text-sm">No plans created yet. Create your first subscription plan to get started.</p>
|
|
72
|
+
</div>
|
|
73
|
+
<% end %>
|
|
74
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<div class="flex justify-between items-center mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold">New Plan</h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6">
|
|
6
|
+
<%= render "form", plan: @plan,
|
|
7
|
+
url: "/admin/plans",
|
|
8
|
+
method: :post %>
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<div class="flex justify-between items-center mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold"><%= @plan.name %></h1>
|
|
3
|
+
<div class="flex gap-2">
|
|
4
|
+
<a href="/admin/plans/<%= @plan.id %>/edit"
|
|
5
|
+
class="inline-flex items-center gap-1 px-4 py-2 bg-rsb-primary text-rsb-primary-text rounded-rsb text-sm font-medium hover:bg-rsb-primary-hover transition-colors">Edit</a>
|
|
6
|
+
<a href="/admin/plans"
|
|
7
|
+
class="inline-flex items-center gap-1 px-4 py-2 border border-rsb-border rounded-rsb text-sm hover:bg-rsb-bg transition-colors">Back</a>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<%# Plan Details %>
|
|
12
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6 mb-4">
|
|
13
|
+
<h3 class="text-base font-semibold mb-4">Plan Information</h3>
|
|
14
|
+
<div class="grid grid-cols-[200px_1fr] gap-4">
|
|
15
|
+
<strong class="text-sm text-rsb-muted">Name</strong>
|
|
16
|
+
<div><%= @plan.name %></div>
|
|
17
|
+
|
|
18
|
+
<strong class="text-sm text-rsb-muted">Slug</strong>
|
|
19
|
+
<div><code class="px-1.5 py-0.5 bg-rsb-bg rounded-rsb-sm text-sm"><%= @plan.slug %></code></div>
|
|
20
|
+
|
|
21
|
+
<strong class="text-sm text-rsb-muted">Interval</strong>
|
|
22
|
+
<div>
|
|
23
|
+
<span class="inline-block px-2.5 py-0.5 rounded-rsb bg-rsb-info-bg text-rsb-info-text text-xs font-medium"><%= @plan.interval.titleize %></span>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<strong class="text-sm text-rsb-muted">Price</strong>
|
|
27
|
+
<div>$<%= "%.2f" % (@plan.price_cents / 100.0) %> <%= @plan.currency.upcase %></div>
|
|
28
|
+
|
|
29
|
+
<strong class="text-sm text-rsb-muted">Status</strong>
|
|
30
|
+
<div>
|
|
31
|
+
<% if @plan.active? %>
|
|
32
|
+
<span class="inline-block px-2.5 py-0.5 rounded-rsb bg-rsb-success-bg text-rsb-success-text text-xs font-medium">Active</span>
|
|
33
|
+
<% else %>
|
|
34
|
+
<span class="inline-block px-2.5 py-0.5 rounded-rsb bg-rsb-danger-bg text-rsb-danger-text text-xs font-medium">Inactive</span>
|
|
35
|
+
<% end %>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<%# Features %>
|
|
41
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6 mb-4">
|
|
42
|
+
<h3 class="text-base font-semibold mb-4">Features</h3>
|
|
43
|
+
<% if @plan.features.any? %>
|
|
44
|
+
<div class="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-3">
|
|
45
|
+
<% @plan.features.each do |key, value| %>
|
|
46
|
+
<div class="flex items-center gap-2">
|
|
47
|
+
<span class="text-sm"><%= key.titleize %></span>
|
|
48
|
+
<% if value %>
|
|
49
|
+
<span class="inline-block px-2.5 py-0.5 rounded-rsb bg-rsb-success-bg text-rsb-success-text text-xs font-medium">Enabled</span>
|
|
50
|
+
<% else %>
|
|
51
|
+
<span class="inline-block px-2.5 py-0.5 rounded-rsb bg-rsb-danger-bg text-rsb-danger-text text-xs font-medium">Disabled</span>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|
|
55
|
+
</div>
|
|
56
|
+
<% else %>
|
|
57
|
+
<p class="text-rsb-muted text-sm">No features configured.</p>
|
|
58
|
+
<% end %>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<%# Limits %>
|
|
62
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6 mb-4">
|
|
63
|
+
<h3 class="text-base font-semibold mb-4">Limits</h3>
|
|
64
|
+
<% if @plan.limits.any? %>
|
|
65
|
+
<div class="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-3">
|
|
66
|
+
<% @plan.limits.each do |key, value| %>
|
|
67
|
+
<div>
|
|
68
|
+
<div class="text-sm text-rsb-muted"><%= key.titleize %></div>
|
|
69
|
+
<div class="text-xl font-semibold"><%= number_with_delimiter(value) %></div>
|
|
70
|
+
</div>
|
|
71
|
+
<% end %>
|
|
72
|
+
</div>
|
|
73
|
+
<% else %>
|
|
74
|
+
<p class="text-rsb-muted text-sm">No limits configured.</p>
|
|
75
|
+
<% end %>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<%# Metadata %>
|
|
79
|
+
<% if @plan.metadata.present? && @plan.metadata.any? %>
|
|
80
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6 mb-4">
|
|
81
|
+
<h3 class="text-base font-semibold mb-4">Metadata</h3>
|
|
82
|
+
<pre class="p-4 bg-rsb-bg rounded-rsb text-sm overflow-x-auto"><%= JSON.pretty_generate(@plan.metadata) %></pre>
|
|
83
|
+
</div>
|
|
84
|
+
<% end %>
|
|
85
|
+
|
|
86
|
+
<%# Danger Zone %>
|
|
87
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6">
|
|
88
|
+
<h3 class="text-base font-semibold text-rsb-danger mb-4">Danger Zone</h3>
|
|
89
|
+
<%= button_to "Delete Plan",
|
|
90
|
+
"/admin/plans/#{@plan.id}",
|
|
91
|
+
method: :delete,
|
|
92
|
+
data: { turbo_confirm: "Are you sure you want to delete this plan? This action cannot be undone." },
|
|
93
|
+
class: "px-4 py-2 bg-rsb-danger text-white rounded-rsb text-sm font-medium hover:opacity-90 transition-colors" %>
|
|
94
|
+
</div>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<div class="flex justify-between items-center mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold">Usage Monitoring</h1>
|
|
3
|
+
<a href="/admin/usage_counters/trend" class="rsb-btn rsb-btn-secondary text-sm">
|
|
4
|
+
Trend Charts
|
|
5
|
+
</a>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<%# Filters %>
|
|
9
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg p-4 mb-4">
|
|
10
|
+
<form method="get" action="/admin/usage_counters" class="flex flex-wrap gap-4 items-end">
|
|
11
|
+
<div>
|
|
12
|
+
<label class="block text-xs font-semibold text-rsb-muted mb-1">Metric</label>
|
|
13
|
+
<select name="metric" class="rsb-input text-sm">
|
|
14
|
+
<option value="">All</option>
|
|
15
|
+
<% @available_metrics.each do |m| %>
|
|
16
|
+
<option value="<%= m %>" <%= "selected" if params[:metric] == m %>><%= m %></option>
|
|
17
|
+
<% end %>
|
|
18
|
+
</select>
|
|
19
|
+
</div>
|
|
20
|
+
<div>
|
|
21
|
+
<label class="block text-xs font-semibold text-rsb-muted mb-1">Period Key</label>
|
|
22
|
+
<input type="text" name="period_key" value="<%= params[:period_key] %>" placeholder="e.g., 2026-02" class="rsb-input text-sm">
|
|
23
|
+
</div>
|
|
24
|
+
<div>
|
|
25
|
+
<label class="block text-xs font-semibold text-rsb-muted mb-1">Countable Type</label>
|
|
26
|
+
<select name="countable_type" class="rsb-input text-sm">
|
|
27
|
+
<option value="">All</option>
|
|
28
|
+
<% @available_types.each do |t| %>
|
|
29
|
+
<option value="<%= t %>" <%= "selected" if params[:countable_type] == t %>><%= t %></option>
|
|
30
|
+
<% end %>
|
|
31
|
+
</select>
|
|
32
|
+
</div>
|
|
33
|
+
<div>
|
|
34
|
+
<button type="submit" class="rsb-btn rsb-btn-primary text-sm">Filter</button>
|
|
35
|
+
<a href="/admin/usage_counters" class="rsb-btn rsb-btn-ghost text-sm ml-2">Clear</a>
|
|
36
|
+
</div>
|
|
37
|
+
</form>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<%# Table %>
|
|
41
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm overflow-hidden">
|
|
42
|
+
<% if @usage_counters.any? %>
|
|
43
|
+
<div class="overflow-x-auto">
|
|
44
|
+
<table class="w-full">
|
|
45
|
+
<thead>
|
|
46
|
+
<tr>
|
|
47
|
+
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">
|
|
48
|
+
<a href="/admin/usage_counters?<%= { metric: params[:metric], period_key: params[:period_key], countable_type: params[:countable_type], sort: "period_key", direction: params[:sort] == "period_key" && params[:direction] != "asc" ? "asc" : "desc" }.compact.to_query %>">Period</a>
|
|
49
|
+
</th>
|
|
50
|
+
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">Countable</th>
|
|
51
|
+
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">Metric</th>
|
|
52
|
+
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">
|
|
53
|
+
<a href="/admin/usage_counters?<%= { metric: params[:metric], period_key: params[:period_key], countable_type: params[:countable_type], sort: "current_value", direction: params[:sort] == "current_value" && params[:direction] != "asc" ? "asc" : "desc" }.compact.to_query %>">Value</a>
|
|
54
|
+
</th>
|
|
55
|
+
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">Limit</th>
|
|
56
|
+
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">% Used</th>
|
|
57
|
+
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">
|
|
58
|
+
<a href="/admin/usage_counters?<%= { metric: params[:metric], period_key: params[:period_key], countable_type: params[:countable_type], sort: "created_at", direction: params[:sort] == "created_at" && params[:direction] != "asc" ? "asc" : "desc" }.compact.to_query %>">Created</a>
|
|
59
|
+
</th>
|
|
60
|
+
</tr>
|
|
61
|
+
</thead>
|
|
62
|
+
<tbody>
|
|
63
|
+
<% @usage_counters.each do |counter| %>
|
|
64
|
+
<% pct = counter.limit && counter.limit > 0 ? (counter.current_value * 100.0 / counter.limit).round(1) : nil %>
|
|
65
|
+
<tr class="hover:bg-rsb-bg border-b border-rsb-border last:border-b-0">
|
|
66
|
+
<td class="px-4 py-3 text-sm font-mono"><%= counter.period_key == "__cumulative__" ? "Cumulative" : counter.period_key %></td>
|
|
67
|
+
<td class="px-4 py-3 text-sm"><%= counter.countable_type %>#<%= counter.countable_id %></td>
|
|
68
|
+
<td class="px-4 py-3 text-sm"><span class="rsb-badge"><%= counter.metric %></span></td>
|
|
69
|
+
<td class="px-4 py-3 text-sm font-mono"><%= counter.current_value %></td>
|
|
70
|
+
<td class="px-4 py-3 text-sm font-mono"><%= counter.limit || "∞" %></td>
|
|
71
|
+
<td class="px-4 py-3 text-sm">
|
|
72
|
+
<% if pct %>
|
|
73
|
+
<div class="flex items-center gap-2">
|
|
74
|
+
<div class="w-16 h-2 bg-rsb-bg rounded-full overflow-hidden">
|
|
75
|
+
<div class="h-full rounded-full <%= pct >= 90 ? 'bg-red-500' : pct >= 70 ? 'bg-yellow-500' : 'bg-green-500' %>" style="width: <%= [pct, 100].min %>%"></div>
|
|
76
|
+
</div>
|
|
77
|
+
<span class="text-xs text-rsb-muted"><%= pct %>%</span>
|
|
78
|
+
</div>
|
|
79
|
+
<% else %>
|
|
80
|
+
<span class="text-rsb-muted">—</span>
|
|
81
|
+
<% end %>
|
|
82
|
+
</td>
|
|
83
|
+
<td class="px-4 py-3 text-sm text-rsb-muted"><%= counter.created_at.strftime("%Y-%m-%d %H:%M") %></td>
|
|
84
|
+
</tr>
|
|
85
|
+
<% end %>
|
|
86
|
+
</tbody>
|
|
87
|
+
</table>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<%# Pagination %>
|
|
91
|
+
<% total_pages = (@total_count.to_f / @per_page).ceil %>
|
|
92
|
+
<% if total_pages > 1 %>
|
|
93
|
+
<div class="px-4 py-3 border-t border-rsb-border flex justify-between items-center text-sm text-rsb-muted">
|
|
94
|
+
<span>Showing <%= (@page - 1) * @per_page + 1 %>–<%= [@page * @per_page, @total_count].min %> of <%= @total_count %></span>
|
|
95
|
+
<div class="flex gap-2">
|
|
96
|
+
<% if @page > 1 %>
|
|
97
|
+
<a href="/admin/usage_counters?<%= { metric: params[:metric], period_key: params[:period_key], countable_type: params[:countable_type], sort: params[:sort], direction: params[:direction], page: @page - 1 }.compact.to_query %>" class="rsb-btn rsb-btn-ghost text-xs">Prev</a>
|
|
98
|
+
<% end %>
|
|
99
|
+
<% if @page < total_pages %>
|
|
100
|
+
<a href="/admin/usage_counters?<%= { metric: params[:metric], period_key: params[:period_key], countable_type: params[:countable_type], sort: params[:sort], direction: params[:direction], page: @page + 1 }.compact.to_query %>" class="rsb-btn rsb-btn-ghost text-xs">Next</a>
|
|
101
|
+
<% end %>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<% end %>
|
|
105
|
+
<% else %>
|
|
106
|
+
<div class="p-8 text-center">
|
|
107
|
+
<p class="text-rsb-muted text-sm">No usage counters found.</p>
|
|
108
|
+
</div>
|
|
109
|
+
<% end %>
|
|
110
|
+
</div>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<div class="flex justify-between items-center mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold">Usage Trend</h1>
|
|
3
|
+
<a href="/admin/usage_counters" class="rsb-btn rsb-btn-secondary text-sm">
|
|
4
|
+
Back to Overview
|
|
5
|
+
</a>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<%# Metric selector %>
|
|
9
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg p-4 mb-4">
|
|
10
|
+
<form method="get" action="/admin/usage_counters/trend" class="flex flex-wrap gap-4 items-end">
|
|
11
|
+
<div>
|
|
12
|
+
<label class="block text-xs font-semibold text-rsb-muted mb-1">Metric</label>
|
|
13
|
+
<select name="metric" class="rsb-input text-sm">
|
|
14
|
+
<option value="">Select a metric</option>
|
|
15
|
+
<% @available_metrics.each do |m| %>
|
|
16
|
+
<option value="<%= m %>" <%= "selected" if @metric == m %>><%= m %></option>
|
|
17
|
+
<% end %>
|
|
18
|
+
</select>
|
|
19
|
+
</div>
|
|
20
|
+
<div>
|
|
21
|
+
<label class="block text-xs font-semibold text-rsb-muted mb-1">Countable Type</label>
|
|
22
|
+
<input type="text" name="countable_type" value="<%= params[:countable_type] %>" placeholder="Optional" class="rsb-input text-sm">
|
|
23
|
+
</div>
|
|
24
|
+
<div>
|
|
25
|
+
<label class="block text-xs font-semibold text-rsb-muted mb-1">Countable ID</label>
|
|
26
|
+
<input type="text" name="countable_id" value="<%= params[:countable_id] %>" placeholder="Optional" class="rsb-input text-sm">
|
|
27
|
+
</div>
|
|
28
|
+
<div>
|
|
29
|
+
<button type="submit" class="rsb-btn rsb-btn-primary text-sm">View Trend</button>
|
|
30
|
+
</div>
|
|
31
|
+
</form>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<%# Chart %>
|
|
35
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6">
|
|
36
|
+
<% if @metric.blank? %>
|
|
37
|
+
<div class="text-center py-8">
|
|
38
|
+
<p class="text-rsb-muted text-sm">Select a metric to view its usage trend.</p>
|
|
39
|
+
</div>
|
|
40
|
+
<% elsif @trend_data.blank? || @trend_data.empty? %>
|
|
41
|
+
<div class="text-center py-8">
|
|
42
|
+
<p class="text-rsb-muted text-sm">No data found for metric "<%= @metric %>".</p>
|
|
43
|
+
</div>
|
|
44
|
+
<% else %>
|
|
45
|
+
<h2 class="text-lg font-semibold mb-4"><%= @metric %> — Last <%= @trend_data.size %> periods</h2>
|
|
46
|
+
<div class="flex items-end gap-1" style="height: 200px;">
|
|
47
|
+
<% @trend_data.each do |period_key, value| %>
|
|
48
|
+
<% bar_pct = @max_value > 0 ? (value.to_f / @max_value * 100).round(1) : 0 %>
|
|
49
|
+
<div class="flex-1 flex flex-col items-center justify-end h-full">
|
|
50
|
+
<span class="text-xs text-rsb-muted mb-1"><%= value %></span>
|
|
51
|
+
<div class="w-full bg-blue-500 rounded-t" style="height: <%= bar_pct %>%; min-height: 2px;" title="<%= period_key %>: <%= value %>"></div>
|
|
52
|
+
<span class="text-xs text-rsb-muted mt-1 truncate w-full text-center" style="font-size: 0.6rem;"><%= period_key == "__cumulative__" ? "Cum." : period_key.gsub("2026-", "") %></span>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|
|
55
|
+
</div>
|
|
56
|
+
<% end %>
|
|
57
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
en:
|
|
2
|
+
rsb:
|
|
3
|
+
admin:
|
|
4
|
+
resources:
|
|
5
|
+
plans:
|
|
6
|
+
label: "Plans"
|
|
7
|
+
columns:
|
|
8
|
+
price_cents: "Price"
|
|
9
|
+
entitlements:
|
|
10
|
+
label: "Entitlements"
|
|
11
|
+
columns:
|
|
12
|
+
entitleable_type: "Type"
|
|
13
|
+
entitleable_id: "Owner ID"
|
|
14
|
+
payment_requests:
|
|
15
|
+
label: "Payment Requests"
|
|
16
|
+
columns:
|
|
17
|
+
requestable_type: "Type"
|
|
18
|
+
requestable_id: "Owner ID"
|
|
19
|
+
provider_key: "Provider"
|
|
20
|
+
amount_cents: "Amount"
|
|
21
|
+
resolved_by: "Resolved By"
|
|
22
|
+
resolved_at: "Resolved At"
|
|
23
|
+
admin_note: "Admin Note"
|
|
24
|
+
expires_at: "Expires At"
|
|
25
|
+
provider_ref: "Provider Ref"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRSBEntitlementsPlans < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :rsb_entitlements_plans do |t|
|
|
6
|
+
t.string :name, null: false
|
|
7
|
+
t.string :slug, null: false
|
|
8
|
+
t.string :interval, null: false
|
|
9
|
+
t.integer :price_cents, null: false, default: 0
|
|
10
|
+
t.string :currency, null: false, default: 'usd'
|
|
11
|
+
t.json :features, default: {}
|
|
12
|
+
t.json :limits, default: {}
|
|
13
|
+
t.json :metadata, default: {}
|
|
14
|
+
t.boolean :active, default: true
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :rsb_entitlements_plans, :slug, unique: true
|
|
19
|
+
add_index :rsb_entitlements_plans, :active
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRSBEntitlementsEntitlements < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :rsb_entitlements_entitlements do |t|
|
|
6
|
+
t.references :entitleable, polymorphic: true, null: false
|
|
7
|
+
t.references :plan, null: false, foreign_key: { to_table: :rsb_entitlements_plans }
|
|
8
|
+
t.string :status, null: false, default: 'pending'
|
|
9
|
+
t.string :provider, null: false
|
|
10
|
+
t.string :provider_ref
|
|
11
|
+
t.datetime :activated_at
|
|
12
|
+
t.datetime :expires_at
|
|
13
|
+
t.datetime :revoked_at
|
|
14
|
+
t.string :revoke_reason
|
|
15
|
+
t.json :metadata, default: {}
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
add_index :rsb_entitlements_entitlements, :status
|
|
20
|
+
add_index :rsb_entitlements_entitlements, :provider
|
|
21
|
+
add_index :rsb_entitlements_entitlements, :expires_at
|
|
22
|
+
end
|
|
23
|
+
end
|