profitable 0.4.0 → 0.5.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.
@@ -8,11 +8,13 @@ module Profitable
8
8
  amount = data['price']
9
9
  return 0 if amount.nil?
10
10
 
11
+ # Some processor payloads provide amounts as strings, so coerce before
12
+ # multiplying by quantity to avoid string repetition bugs.
11
13
  quantity = subscription.quantity || 1
12
14
  interval = data['billing_period_unit']
13
15
  interval_count = data['billing_period_frequency'] || 1
14
16
 
15
- normalize_to_monthly(amount * quantity, interval, interval_count)
17
+ normalize_to_monthly(amount.to_f * quantity, interval, interval_count)
16
18
  end
17
19
  end
18
20
  end
@@ -18,11 +18,12 @@ module Profitable
18
18
  amount = price_data.dig('unit_price', 'amount')
19
19
  next if amount.nil?
20
20
 
21
+ # Paddle can also serialize amounts as strings; coerce before applying quantity.
21
22
  item_quantity = item['quantity'] || 1
22
23
  interval = price_data.dig('billing_cycle', 'interval')
23
24
  interval_count = price_data.dig('billing_cycle', 'frequency')
24
25
 
25
- total_mrr += normalize_to_monthly(amount * item_quantity, interval, interval_count)
26
+ total_mrr += normalize_to_monthly(amount.to_f * item_quantity, interval, interval_count)
26
27
  end
27
28
 
28
29
  total_mrr
@@ -8,11 +8,12 @@ module Profitable
8
8
  amount = data['recurring_price']
9
9
  return 0 if amount.nil?
10
10
 
11
+ # Paddle Classic payloads may expose string amounts; coerce before quantity math.
11
12
  quantity = subscription.quantity || 1
12
13
  interval = data['recurring_interval']
13
14
  interval_count = 1 # Paddle Classic doesn't have interval_count
14
15
 
15
- normalize_to_monthly(amount * quantity, interval, interval_count)
16
+ normalize_to_monthly(amount.to_f * quantity, interval, interval_count)
16
17
  end
17
18
  end
18
19
  end
@@ -18,6 +18,10 @@ module Profitable
18
18
  price_data = item['price'] || item
19
19
  next if price_data.nil?
20
20
 
21
+ # Metered items are usage-based, so they do not have a fixed run-rate that
22
+ # belongs in MRR/ARR style metrics. Keep only licensed recurring items here.
23
+ next if price_data.dig('recurring', 'usage_type') == 'metered'
24
+
21
25
  amount = price_data['unit_amount']
22
26
  next if amount.nil?
23
27
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profitable
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/profitable.rb CHANGED
@@ -12,520 +12,4 @@ require "pay"
12
12
  require "active_support/core_ext/numeric/conversions"
13
13
  require "action_view"
14
14
 
15
- module Profitable
16
- # Subscription status constants (at module level so MrrCalculator can reference them)
17
- EXCLUDED_STATUSES = ['trialing', 'paused'].freeze
18
- CHURNED_STATUSES = ['canceled', 'ended'].freeze
19
-
20
- class << self
21
- include ActionView::Helpers::NumberHelper
22
- include Profitable::JsonHelpers
23
-
24
- DEFAULT_PERIOD = 30.days
25
- MRR_MILESTONES = [5, 10, 20, 30, 50, 75, 100, 200, 300, 400, 500, 1_000, 2_000, 3_000, 5_000, 10_000, 20_000, 30_000, 50_000, 83_333, 100_000, 250_000, 500_000, 1_000_000, 5_000_000, 10_000_000, 25_000_000, 50_000_000, 75_000_000, 100_000_000]
26
-
27
- def mrr
28
- NumericResult.new(MrrCalculator.calculate)
29
- end
30
-
31
- def arr
32
- NumericResult.new(calculate_arr)
33
- end
34
-
35
- def churn(in_the_last: DEFAULT_PERIOD)
36
- NumericResult.new(calculate_churn(in_the_last), :percentage)
37
- end
38
-
39
- def all_time_revenue
40
- NumericResult.new(calculate_all_time_revenue)
41
- end
42
-
43
- def revenue_in_period(in_the_last: DEFAULT_PERIOD)
44
- NumericResult.new(calculate_revenue_in_period(in_the_last))
45
- end
46
-
47
- def recurring_revenue_in_period(in_the_last: DEFAULT_PERIOD)
48
- NumericResult.new(calculate_recurring_revenue_in_period(in_the_last))
49
- end
50
-
51
- def recurring_revenue_percentage(in_the_last: DEFAULT_PERIOD)
52
- NumericResult.new(calculate_recurring_revenue_percentage(in_the_last), :percentage)
53
- end
54
-
55
- def estimated_valuation(multiplier = nil, at: nil, multiple: nil)
56
- actual_multiplier = multiplier || at || multiple || 3
57
- NumericResult.new(calculate_estimated_valuation(actual_multiplier))
58
- end
59
-
60
- def total_customers
61
- NumericResult.new(calculate_total_customers, :integer)
62
- end
63
-
64
- def total_subscribers
65
- NumericResult.new(calculate_total_subscribers, :integer)
66
- end
67
-
68
- def active_subscribers
69
- NumericResult.new(calculate_active_subscribers, :integer)
70
- end
71
-
72
- def new_customers(in_the_last: DEFAULT_PERIOD)
73
- NumericResult.new(calculate_new_customers(in_the_last), :integer)
74
- end
75
-
76
- def new_subscribers(in_the_last: DEFAULT_PERIOD)
77
- NumericResult.new(calculate_new_subscribers(in_the_last), :integer)
78
- end
79
-
80
- def churned_customers(in_the_last: DEFAULT_PERIOD)
81
- NumericResult.new(calculate_churned_customers(in_the_last), :integer)
82
- end
83
-
84
- def new_mrr(in_the_last: DEFAULT_PERIOD)
85
- NumericResult.new(calculate_new_mrr(in_the_last))
86
- end
87
-
88
- def churned_mrr(in_the_last: DEFAULT_PERIOD)
89
- NumericResult.new(calculate_churned_mrr(in_the_last))
90
- end
91
-
92
- def average_revenue_per_customer
93
- NumericResult.new(calculate_average_revenue_per_customer)
94
- end
95
-
96
- def lifetime_value
97
- NumericResult.new(calculate_lifetime_value)
98
- end
99
-
100
- def mrr_growth(in_the_last: DEFAULT_PERIOD)
101
- NumericResult.new(calculate_mrr_growth(in_the_last))
102
- end
103
-
104
- def mrr_growth_rate(in_the_last: DEFAULT_PERIOD)
105
- NumericResult.new(calculate_mrr_growth_rate(in_the_last), :percentage)
106
- end
107
-
108
- def time_to_next_mrr_milestone
109
- current_mrr = (mrr.to_i) / 100 # Convert cents to dollars
110
- return "Unable to calculate. No MRR yet." if current_mrr <= 0
111
-
112
- next_milestone = MRR_MILESTONES.find { |milestone| milestone > current_mrr }
113
- return "Congratulations! You've reached the highest milestone." unless next_milestone
114
-
115
- monthly_growth_rate = calculate_mrr_growth_rate / 100
116
- return "Unable to calculate. Need more data or positive growth." if monthly_growth_rate <= 0
117
-
118
- # Convert monthly growth rate to daily growth rate
119
- daily_growth_rate = (1 + monthly_growth_rate) ** (1.0 / 30) - 1
120
- return "Unable to calculate. Growth rate too small." if daily_growth_rate <= 0
121
-
122
- # Calculate the number of days to reach the next milestone
123
- days_to_milestone = (Math.log(next_milestone.to_f / current_mrr) / Math.log(1 + daily_growth_rate)).ceil
124
-
125
- target_date = Time.current + days_to_milestone.days
126
-
127
- "#{days_to_milestone} days left to $#{number_with_delimiter(next_milestone)} MRR (#{target_date.strftime('%b %d, %Y')})"
128
- end
129
-
130
- def monthly_summary(months: 12)
131
- calculate_monthly_summary(months)
132
- end
133
-
134
- def daily_summary(days: 30)
135
- calculate_daily_summary(days)
136
- end
137
-
138
- def period_data(in_the_last: DEFAULT_PERIOD)
139
- calculate_period_data(in_the_last)
140
- end
141
-
142
- private
143
-
144
- # Helper to load subscriptions with processor info from customer
145
- def subscriptions_with_processor(scope = Pay::Subscription.all)
146
- scope
147
- .includes(:customer)
148
- .select('pay_subscriptions.*, pay_customers.processor as customer_processor')
149
- .joins(:customer)
150
- end
151
-
152
- def paid_charges
153
- # Pay gem v10+ stores charge data in `object` column, older versions used `data`
154
- # We check both columns for backwards compatibility using database-agnostic JSON extraction
155
- #
156
- # Performance note: The COALESCE pattern may prevent index usage on some databases.
157
- # This is an acceptable tradeoff for backwards compatibility with Pay < 10.
158
- # For high-volume scenarios, consider adding a composite index or upgrading to Pay 10+
159
- # where only the `object` column is used.
160
-
161
- # Build JSON extraction SQL for both object and data columns
162
- paid_object = json_extract('pay_charges.object', 'paid')
163
- paid_data = json_extract('pay_charges.data', 'paid')
164
- status_object = json_extract('pay_charges.object', 'status')
165
- status_data = json_extract('pay_charges.data', 'status')
166
-
167
- Pay::Charge
168
- .where("pay_charges.amount > 0")
169
- .where(<<~SQL.squish, 'false', 'succeeded')
170
- (
171
- (COALESCE(#{paid_object}, #{paid_data}) IS NULL
172
- OR COALESCE(#{paid_object}, #{paid_data}) != ?)
173
- )
174
- AND
175
- (
176
- COALESCE(#{status_object}, #{status_data}) = ?
177
- OR COALESCE(#{status_object}, #{status_data}) IS NULL
178
- )
179
- SQL
180
- end
181
-
182
- def calculate_all_time_revenue
183
- paid_charges.sum(:amount)
184
- end
185
-
186
- def calculate_arr
187
- (mrr.to_f * 12).round
188
- end
189
-
190
- def calculate_estimated_valuation(multiplier = 3)
191
- multiplier = parse_multiplier(multiplier)
192
- (calculate_arr * multiplier).round
193
- end
194
-
195
- def parse_multiplier(input)
196
- case input
197
- when Numeric
198
- input.to_f
199
- when String
200
- if input.end_with?('x')
201
- input.chomp('x').to_f
202
- else
203
- input.to_f
204
- end
205
- else
206
- 3.0 # Default multiplier if input is invalid
207
- end.clamp(0.1, 100) # Ensure multiplier is within a reasonable range
208
- end
209
-
210
- def calculate_churn(period = DEFAULT_PERIOD)
211
- calculate_churn_rate_for_period(period.ago, Time.current)
212
- end
213
-
214
- def calculate_churned_customers(period = DEFAULT_PERIOD)
215
- calculate_churned_subscribers_in_period(period.ago, Time.current)
216
- end
217
-
218
- def calculate_churned_mrr(period = DEFAULT_PERIOD)
219
- calculate_churned_mrr_in_period(period.ago, Time.current)
220
- end
221
-
222
- def calculate_new_mrr(period = DEFAULT_PERIOD)
223
- calculate_new_mrr_in_period(period.ago, Time.current)
224
- end
225
-
226
- def calculate_revenue_in_period(period)
227
- paid_charges.where(created_at: period.ago..Time.current).sum(:amount)
228
- end
229
-
230
- def calculate_recurring_revenue_in_period(period)
231
- paid_charges
232
- .joins('INNER JOIN pay_subscriptions ON pay_charges.subscription_id = pay_subscriptions.id')
233
- .where(created_at: period.ago..Time.current)
234
- .sum(:amount)
235
- end
236
-
237
- def calculate_recurring_revenue_percentage(period)
238
- total_revenue = calculate_revenue_in_period(period)
239
- recurring_revenue = calculate_recurring_revenue_in_period(period)
240
-
241
- return 0 if total_revenue.zero?
242
-
243
- ((recurring_revenue.to_f / total_revenue) * 100).round(2)
244
- end
245
-
246
- def calculate_total_customers
247
- Pay::Customer.joins(:charges)
248
- .merge(paid_charges)
249
- .distinct
250
- .count
251
- end
252
-
253
- def calculate_total_subscribers
254
- Pay::Customer.joins(:subscriptions).distinct.count
255
- end
256
-
257
- def calculate_active_subscribers
258
- Pay::Customer.joins(:subscriptions)
259
- .where(pay_subscriptions: { status: 'active' })
260
- .distinct
261
- .count
262
- end
263
-
264
- def actual_customers
265
- Pay::Customer.joins("LEFT JOIN pay_subscriptions ON pay_customers.id = pay_subscriptions.customer_id")
266
- .joins("LEFT JOIN pay_charges ON pay_customers.id = pay_charges.customer_id")
267
- .where("pay_subscriptions.id IS NOT NULL OR pay_charges.amount > 0")
268
- .distinct
269
- end
270
-
271
- def calculate_new_customers(period)
272
- actual_customers.where(created_at: period.ago..Time.current).count
273
- end
274
-
275
- def calculate_new_subscribers(period)
276
- calculate_new_subscribers_in_period(period.ago, Time.current)
277
- end
278
-
279
- def calculate_average_revenue_per_customer
280
- paying_customers = calculate_total_customers
281
- return 0 if paying_customers.zero?
282
- (all_time_revenue.to_f / paying_customers).round
283
- end
284
-
285
- def calculate_lifetime_value
286
- # LTV = Monthly ARPU / Monthly Churn Rate
287
- # where ARPU (Average Revenue Per User) = MRR / active subscribers
288
- subscribers = calculate_active_subscribers
289
- return 0 if subscribers.zero?
290
-
291
- monthly_arpu = mrr.to_f / subscribers # in cents
292
- churn_rate = churn.to_f / 100 # monthly churn as decimal (e.g., 5% = 0.05)
293
- return 0 if churn_rate.zero?
294
-
295
- (monthly_arpu / churn_rate).round # LTV in cents
296
- end
297
-
298
- def calculate_mrr_growth(period = DEFAULT_PERIOD)
299
- new_mrr = calculate_new_mrr(period)
300
- churned_mrr = calculate_churned_mrr(period)
301
- new_mrr - churned_mrr
302
- end
303
-
304
- def calculate_mrr_growth_rate(period = DEFAULT_PERIOD)
305
- end_date = Time.current
306
- start_date = end_date - period
307
-
308
- start_mrr = calculate_mrr_at(start_date)
309
- end_mrr = calculate_mrr_at(end_date)
310
-
311
- return 0 if start_mrr == 0
312
- ((end_mrr.to_f - start_mrr) / start_mrr * 100).round(2)
313
- end
314
-
315
- def calculate_mrr_at(date)
316
- # Find subscriptions that were active AT the given date:
317
- # - Created before or on that date
318
- # - Not ended before that date (ends_at is nil OR ends_at > date)
319
- # - Not paused at that date
320
- # - Not in trialing status (trials don't count as MRR)
321
- subscriptions_with_processor(
322
- Pay::Subscription
323
- .where('pay_subscriptions.created_at <= ?', date)
324
- .where('pay_subscriptions.ends_at IS NULL OR pay_subscriptions.ends_at > ?', date)
325
- .where('pay_subscriptions.pause_starts_at IS NULL OR pay_subscriptions.pause_starts_at > ?', date)
326
- .where.not(status: EXCLUDED_STATUSES)
327
- ).sum do |subscription|
328
- MrrCalculator.process_subscription(subscription)
329
- end
330
- end
331
-
332
- def calculate_period_data(period)
333
- period_start = period.ago
334
- period_end = Time.current
335
-
336
- new_customers_count = actual_customers.where(created_at: period_start..period_end).count
337
- churned_count = calculate_churned_subscribers_in_period(period_start, period_end)
338
- new_mrr_val = calculate_new_mrr_in_period(period_start, period_end)
339
- churned_mrr_val = calculate_churned_mrr_in_period(period_start, period_end)
340
- revenue_val = paid_charges.where(created_at: period_start..period_end).sum(:amount)
341
-
342
- # Churn rate (reuses churned_count)
343
- total_at_start = Pay::Subscription
344
- .where('pay_subscriptions.created_at < ?', period_start)
345
- .where('pay_subscriptions.ends_at IS NULL OR pay_subscriptions.ends_at > ?', period_start)
346
- .where.not(status: EXCLUDED_STATUSES)
347
- .distinct
348
- .count('customer_id')
349
- churn_rate = total_at_start > 0 ? (churned_count.to_f / total_at_start * 100).round(1) : 0
350
-
351
- {
352
- new_customers: NumericResult.new(new_customers_count, :integer),
353
- churned_customers: NumericResult.new(churned_count, :integer),
354
- churn: NumericResult.new(churn_rate, :percentage),
355
- new_mrr: NumericResult.new(new_mrr_val),
356
- churned_mrr: NumericResult.new(churned_mrr_val),
357
- mrr_growth: NumericResult.new(new_mrr_val - churned_mrr_val),
358
- revenue: NumericResult.new(revenue_val)
359
- }
360
- end
361
-
362
- # Batched: loads all data in 5 queries then groups by month in Ruby
363
- def calculate_monthly_summary(months_count)
364
- overall_start = (months_count - 1).months.ago.beginning_of_month
365
- overall_end = Time.current.end_of_month
366
-
367
- # Bulk load all data for the full range
368
- new_sub_records = Pay::Subscription
369
- .where(created_at: overall_start..overall_end)
370
- .where.not(status: EXCLUDED_STATUSES)
371
- .pluck(:customer_id, :created_at)
372
-
373
- churned_sub_records = Pay::Subscription
374
- .where(status: CHURNED_STATUSES)
375
- .where(ends_at: overall_start..overall_end)
376
- .pluck(:customer_id, :ends_at)
377
-
378
- new_mrr_subs = subscriptions_with_processor(
379
- Pay::Subscription
380
- .where(status: 'active')
381
- .where(created_at: overall_start..overall_end)
382
- ).to_a
383
-
384
- churned_mrr_subs = subscriptions_with_processor(
385
- Pay::Subscription
386
- .where(status: CHURNED_STATUSES)
387
- .where(ends_at: overall_start..overall_end)
388
- ).to_a
389
-
390
- churn_base_records = Pay::Subscription
391
- .where('pay_subscriptions.created_at < ?', overall_end)
392
- .where('pay_subscriptions.ends_at IS NULL OR pay_subscriptions.ends_at > ?', overall_start)
393
- .where.not(status: EXCLUDED_STATUSES)
394
- .pluck(:customer_id, :created_at, :ends_at)
395
-
396
- # Group by month in Ruby
397
- summary = []
398
- (months_count - 1).downto(0) do |months_ago|
399
- month_start = months_ago.months.ago.beginning_of_month
400
- month_end = month_start.end_of_month
401
-
402
- new_count = new_sub_records
403
- .select { |_, created_at| created_at >= month_start && created_at <= month_end }
404
- .map(&:first).uniq.count
405
-
406
- churned_count = churned_sub_records
407
- .select { |_, ends_at| ends_at >= month_start && ends_at <= month_end }
408
- .map(&:first).uniq.count
409
-
410
- new_mrr_amount = new_mrr_subs
411
- .select { |s| s.created_at >= month_start && s.created_at <= month_end }
412
- .sum { |s| MrrCalculator.process_subscription(s) }
413
-
414
- churned_mrr_amount = churned_mrr_subs
415
- .select { |s| s.ends_at >= month_start && s.ends_at <= month_end }
416
- .sum { |s| MrrCalculator.process_subscription(s) }
417
-
418
- total_at_start = churn_base_records
419
- .select { |_, created_at, ends_at| created_at < month_start && (ends_at.nil? || ends_at > month_start) }
420
- .map(&:first).uniq.count
421
-
422
- churn_rate = total_at_start > 0 ? (churned_count.to_f / total_at_start * 100).round(1) : 0
423
-
424
- summary << {
425
- month: month_start.strftime('%Y-%m'),
426
- month_date: month_start,
427
- new_subscribers: new_count,
428
- churned_subscribers: churned_count,
429
- net_subscribers: new_count - churned_count,
430
- new_mrr: new_mrr_amount,
431
- churned_mrr: churned_mrr_amount,
432
- net_mrr: new_mrr_amount - churned_mrr_amount,
433
- churn_rate: churn_rate
434
- }
435
- end
436
-
437
- summary
438
- end
439
-
440
- # Batched: loads all data in 2 queries then groups by day in Ruby
441
- def calculate_daily_summary(days_count)
442
- overall_start = (days_count - 1).days.ago.beginning_of_day
443
- overall_end = Time.current.end_of_day
444
-
445
- new_sub_records = Pay::Subscription
446
- .where(created_at: overall_start..overall_end)
447
- .where.not(status: EXCLUDED_STATUSES)
448
- .pluck(:customer_id, :created_at)
449
-
450
- churned_sub_records = Pay::Subscription
451
- .where(status: CHURNED_STATUSES)
452
- .where(ends_at: overall_start..overall_end)
453
- .pluck(:customer_id, :ends_at)
454
-
455
- summary = []
456
- (days_count - 1).downto(0) do |days_ago|
457
- day_start = days_ago.days.ago.beginning_of_day
458
- day_end = day_start.end_of_day
459
-
460
- new_count = new_sub_records
461
- .select { |_, created_at| created_at >= day_start && created_at <= day_end }
462
- .map(&:first).uniq.count
463
-
464
- churned_count = churned_sub_records
465
- .select { |_, ends_at| ends_at >= day_start && ends_at <= day_end }
466
- .map(&:first).uniq.count
467
-
468
- summary << {
469
- date: day_start.to_date,
470
- new_subscribers: new_count,
471
- churned_subscribers: churned_count
472
- }
473
- end
474
-
475
- summary
476
- end
477
-
478
- # Consolidated methods that work with any date range
479
- def calculate_new_subscribers_in_period(period_start, period_end)
480
- Pay::Customer.joins(:subscriptions)
481
- .where(pay_subscriptions: { created_at: period_start..period_end })
482
- .where.not(pay_subscriptions: { status: EXCLUDED_STATUSES })
483
- .distinct
484
- .count
485
- end
486
-
487
- def calculate_churned_subscribers_in_period(period_start, period_end)
488
- Pay::Subscription
489
- .where(status: CHURNED_STATUSES)
490
- .where(ends_at: period_start..period_end)
491
- .distinct
492
- .count('customer_id')
493
- end
494
-
495
- def calculate_new_mrr_in_period(period_start, period_end)
496
- subscriptions_with_processor(
497
- Pay::Subscription
498
- .where(status: 'active')
499
- .where(created_at: period_start..period_end)
500
- ).sum do |subscription|
501
- MrrCalculator.process_subscription(subscription)
502
- end
503
- end
504
-
505
- def calculate_churned_mrr_in_period(period_start, period_end)
506
- subscriptions_with_processor(
507
- Pay::Subscription
508
- .where(status: CHURNED_STATUSES)
509
- .where(ends_at: period_start..period_end)
510
- ).sum do |subscription|
511
- MrrCalculator.process_subscription(subscription)
512
- end
513
- end
514
-
515
- def calculate_churn_rate_for_period(period_start, period_end)
516
- # Count subscribers who were active AT the start of the period
517
- total_subscribers_start = Pay::Subscription
518
- .where('pay_subscriptions.created_at < ?', period_start)
519
- .where('pay_subscriptions.ends_at IS NULL OR pay_subscriptions.ends_at > ?', period_start)
520
- .where.not(status: EXCLUDED_STATUSES)
521
- .distinct
522
- .count('customer_id')
523
-
524
- churned = calculate_churned_subscribers_in_period(period_start, period_end)
525
- return 0 if total_subscribers_start == 0
526
-
527
- (churned.to_f / total_subscribers_start * 100).round(1)
528
- end
529
-
530
- end
531
- end
15
+ require_relative "profitable/metrics"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: profitable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-02-10 00:00:00.000000000 Z
10
+ date: 2026-03-19 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: pay
@@ -59,6 +59,7 @@ files:
59
59
  - app/views/layouts/profitable/application.html.erb
60
60
  - app/views/profitable/dashboard/index.html.erb
61
61
  - config/routes.rb
62
+ - context7.json
62
63
  - gemfiles/pay_10.0.gemfile
63
64
  - gemfiles/pay_11.0.gemfile
64
65
  - gemfiles/pay_7.3.gemfile
@@ -70,6 +71,7 @@ files:
70
71
  - lib/profitable/engine.rb
71
72
  - lib/profitable/error.rb
72
73
  - lib/profitable/json_helpers.rb
74
+ - lib/profitable/metrics.rb
73
75
  - lib/profitable/mrr_calculator.rb
74
76
  - lib/profitable/numeric_result.rb
75
77
  - lib/profitable/processors/base.rb