1+ # frozen_string_literal: true
2+ require "test_helper"
3+
4+ class EdgeCasesTest < Minitest ::Test
5+ def setup
6+ super
7+ @customer = Pay ::Customer . create! ( processor : "stripe" )
8+ end
9+
10+ def create_subscription ( attrs = { } )
11+ defaults = {
12+ customer : @customer ,
13+ processor : "stripe" ,
14+ status : "active" ,
15+ quantity : 1 ,
16+ current_period_start : Time . now - 15 . days ,
17+ current_period_end : Time . now + 15 . days ,
18+ data : { "subscription_items" => [ { "price" => { "unit_amount" => 1000 , "recurring" => { "interval" => "month" , "interval_count" => 1 } } } ] }
19+ }
20+ Pay ::Subscription . create! ( defaults . merge ( attrs ) )
21+ end
22+
23+ def test_estimated_valuation_clamps_multiplier_bounds
24+ create_subscription # mrr 1000, arr 12000 cents
25+
26+ # Below lower bound -> clamp to 0.1x
27+ v = Profitable . estimated_valuation ( at : "0x" )
28+ assert_equal ( 12_000 * 0.1 ) . round , v
29+
30+ # Above upper bound -> clamp to 100x
31+ v = Profitable . estimated_valuation ( at : "200x" )
32+ assert_equal ( 12_000 * 100 ) . round , v
33+
34+ # Invalid type -> default 3x
35+ v = Profitable . estimated_valuation ( at : :wtf )
36+ assert_equal ( 12_000 * 3 ) . round , v
37+ end
38+
39+ def test_time_to_next_mrr_milestone_no_more_milestones
40+ # Set current mrr to highest milestone or beyond
41+ Profitable . stub ( :mrr , Profitable ::NumericResult . new ( 100_000_000 * 100 ) ) do
42+ msg = Profitable . time_to_next_mrr_milestone
43+ assert_match /Congratulations!/ , msg
44+ end
45+ end
46+
47+ def test_time_to_next_mrr_milestone_unable_without_positive_growth
48+ Profitable . stub ( :mrr , Profitable ::NumericResult . new ( 1_000 * 100 ) ) do
49+ Profitable . stub ( :calculate_mrr_growth_rate , 0 ) do
50+ msg = Profitable . time_to_next_mrr_milestone
51+ assert_match /Unable to calculate/ , msg
52+ end
53+ Profitable . stub ( :calculate_mrr_growth_rate , -10 ) do
54+ msg = Profitable . time_to_next_mrr_milestone
55+ assert_match /Unable to calculate/ , msg
56+ end
57+ end
58+ end
59+
60+ def test_recurring_revenue_percentage_zero_when_no_total_revenue
61+ assert_equal 0 , Profitable . recurring_revenue_percentage ( in_the_last : 30 . days )
62+ end
63+
64+ def test_churn_is_zero_when_no_subscribers_at_start
65+ assert_equal 0 , Profitable . churn ( in_the_last : 30 . days )
66+ end
67+
68+ def test_new_mrr_counts_subscriptions_created_at_period_start
69+ start = 10 . days
70+ t = Time . now
71+ Timecop . freeze ( t - start ) do
72+ # created exactly at start boundary (inclusive)
73+ create_subscription ( created_at : t - start )
74+ end
75+ # for newly created at start, prorated days should be full period
76+ val = Profitable . new_mrr ( in_the_last : start )
77+ assert_operator val , :>= , 0
78+ end
79+
80+ def test_mrr_at_includes_subscriptions_created_at_date
81+ t = Time . now
82+ Timecop . freeze ( t - 1 . day ) do
83+ create_subscription ( created_at : t - 1 . day )
84+ end
85+ # created_at == date should be included
86+ val = Profitable . send ( :calculate_mrr_at , t - 1 . day )
87+ assert_operator val , :> , 0
88+ end
89+
90+ def test_churned_mrr_ignores_subscriptions_ending_in_future
91+ Timecop . freeze ( Time . now - 10 . days ) do
92+ create_subscription
93+ end
94+ sub = Pay ::Subscription . last
95+ sub . update! ( status : "canceled" , ends_at : Time . now + 10 . days )
96+ assert_equal 0 , Profitable . churned_mrr ( in_the_last : 30 . days )
97+ end
98+
99+ def test_average_revenue_per_customer_zero_when_no_paying_customers
100+ assert_equal 0 , Profitable . average_revenue_per_customer
101+ end
102+
103+ def test_lifetime_value_zero_when_no_customers_or_zero_churn
104+ # no customers
105+ assert_equal 0 , Profitable . lifetime_value
106+
107+ # add customers but zero churn
108+ Timecop . freeze ( Time . now - 35 . days ) do
109+ create_subscription
110+ end
111+ # churn rate computed as zero
112+ Profitable . stub ( :churn , Profitable ::NumericResult . new ( 0 , :percentage ) ) do
113+ assert_equal 0 , Profitable . lifetime_value
114+ end
115+ end
116+
117+ def test_normalize_to_monthly_handles_nil_and_case_insensitive_intervals
118+ base = Profitable ::Processors ::Base . new ( nil )
119+ assert_equal 0 , base . send ( :normalize_to_monthly , nil , "month" , 1 )
120+ assert_equal 0 , base . send ( :normalize_to_monthly , 1000 , nil , 1 )
121+ assert_equal 0 , base . send ( :normalize_to_monthly , 1000 , "month" , nil )
122+
123+ assert_in_delta 1000.0 , base . send ( :normalize_to_monthly , 1000 , "Month" , 1 ) , 0.001
124+ assert_in_delta 41.6667 , base . send ( :normalize_to_monthly , 1000 , "year" , 2 ) , 0.001
125+ assert_in_delta 30_000.0 , base . send ( :normalize_to_monthly , 1000 , "day" , 1 ) , 0.001
126+ assert_in_delta 4_000.0 , base . send ( :normalize_to_monthly , 1000 , "week" , 1 ) , 0.001
127+ end
128+
129+ def test_negative_amounts_are_clamped_to_zero_in_mrr
130+ data = {
131+ "subscription_items" => [
132+ { "price" => { "unit_amount" => -500 , "recurring" => { "interval" => "month" , "interval_count" => 1 } } }
133+ ]
134+ }
135+ sub = Pay ::Subscription . create! (
136+ customer : @customer ,
137+ processor : "stripe" ,
138+ status : "active" ,
139+ quantity : 1 ,
140+ current_period_start : Time . now - 15 . days ,
141+ current_period_end : Time . now + 15 . days ,
142+ data : data
143+ )
144+ assert_equal 0 , Profitable ::MrrCalculator . process_subscription ( sub )
145+ end
146+ end
0 commit comments