1
+# frozen_string_literal: true
2
+
3
+# siGit's Stripe integration for siGit Code Pro/Team. Stripe is the source of
4
+# truth; we cache plan + status into Subscription so the cloud gate decides
5
+# without a Stripe round-trip. Entirely separate from Onde Inference's billing.
6
+class StripeService
7
+ # Our plan → the Stripe price lookup_key (prices created in Stripe).
8
+ PRICE_LOOKUP_KEYS = {
9
+ "pro" => "sigit_code_pro_monthly",
10
+ "team" => "sigit_code_team_monthly"
11
+ }.freeze
12
+
13
+ # A subscription's price lookup_key → our plan.
14
+ PLAN_BY_LOOKUP_KEY = {
15
+ "sigit_code_pro_monthly" => :pro,
16
+ "sigit_code_team_monthly" => :team
17
+ }.freeze
18
+
19
+ # Stripe subscription status → our Subscription status enum.
20
+ STATUS_MAP = {
21
+ "active" => :active,
22
+ "trialing" => :trialing,
23
+ "past_due" => :past_due,
24
+ "unpaid" => :past_due,
25
+ "canceled" => :canceled,
26
+ "incomplete_expired" => :canceled,
27
+ "incomplete" => :incomplete,
28
+ "paused" => :incomplete
29
+ }.freeze
30
+
31
+ class << self
32
+ def configured?
33
+ ENV["STRIPE_SECRET_KEY"].present?
34
+ end
35
+
36
+ # Create a Checkout session for a user + plan; returns the redirect URL.
37
+ def checkout_url(user:, plan:, success_url:, cancel_url:)
38
+ price_id = price_id_for(plan)
39
+ raise ArgumentError, "Unknown plan #{plan}" unless price_id
40
+
41
+ existing_customer = user.subscription&.stripe_customer_id
42
+
43
+ session = Stripe::Checkout::Session.create({
44
+ mode: "subscription",
45
+ line_items: [{ price: price_id, quantity: 1 }],
46
+ client_reference_id: user.id.to_s,
47
+ customer: existing_customer,
48
+ customer_email: existing_customer ? nil : user.email,
49
+ allow_promotion_codes: true,
50
+ success_url: success_url,
51
+ cancel_url: cancel_url,
52
+ metadata: { user_id: user.id.to_s },
53
+ subscription_data: { metadata: { user_id: user.id.to_s } }
54
+ }.compact)
55
+
56
+ session.url
57
+ end
58
+
59
+ # Create a Billing Portal session for a user; returns the URL. nil when the
60
+ # user has no Stripe customer yet (never subscribed).
61
+ def portal_url(user:, return_url:)
62
+ customer = user.subscription&.stripe_customer_id
63
+ return nil unless customer
64
+
65
+ Stripe::BillingPortal::Session.create(customer: customer, return_url: return_url).url
66
+ end
67
+
68
+ # Verify + parse a webhook payload into a Stripe::Event.
69
+ def construct_event(payload, signature_header)
70
+ Stripe::Webhook.construct_event(payload, signature_header, ENV.fetch("STRIPE_WEBHOOK_SECRET"))
71
+ end
72
+
73
+ # Upsert the local Subscription from a Stripe subscription object. Returns the
74
+ # Subscription, or nil when the subscription isn't attributable to a user.
75
+ def sync_subscription(stripe_subscription)
76
+ user_id = stripe_subscription.metadata && stripe_subscription.metadata["user_id"]
77
+ user = User.find_by(id: user_id)
78
+ return nil unless user
79
+
80
+ price = stripe_subscription.items&.data&.first&.price
81
+ plan = PLAN_BY_LOOKUP_KEY[price&.lookup_key] || :free
82
+ status = STATUS_MAP[stripe_subscription.status] || :incomplete
83
+ period_end = stripe_subscription.current_period_end
84
+
85
+ subscription = user.subscription || user.build_subscription
86
+ subscription.update!(
87
+ plan: plan,
88
+ status: status,
89
+ stripe_customer_id: stripe_subscription.customer,
90
+ stripe_subscription_id: stripe_subscription.id,
91
+ current_period_end: period_end ? Time.at(period_end) : nil
92
+ )
93
+ subscription
94
+ end
95
+
96
+ private
97
+
98
+ def price_id_for(plan)
99
+ lookup_key = PRICE_LOOKUP_KEYS[plan.to_s]
100
+ return nil unless lookup_key
101
+
102
+ Stripe::Price.list(lookup_keys: [lookup_key], active: true, limit: 1).data.first&.id
103
+ end
104
+ end
105
+end