mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-02 20:15:22 +02:00
New onboarding, trials, Stripe integration (#2185)
* New onboarding, trials, Stripe integration * Fix tests * Lint fixes * Fix subscription endpoints
This commit is contained in:
parent
79b4a3769b
commit
a51c4d2cba
53 changed files with 847 additions and 372 deletions
|
@ -2,7 +2,7 @@ class Demo::Generator
|
|||
COLORS = %w[#e99537 #4da568 #6471eb #db5a54 #df4e92 #c44fe9 #eb5429 #61c9ea #805dee #6ad28a]
|
||||
|
||||
# Builds a semi-realistic mirror of what production data might look like
|
||||
def reset_and_clear_data!(family_names)
|
||||
def reset_and_clear_data!(family_names, require_onboarding: false)
|
||||
puts "Clearing existing data..."
|
||||
|
||||
destroy_everything!
|
||||
|
@ -10,7 +10,7 @@ class Demo::Generator
|
|||
puts "Data cleared"
|
||||
|
||||
family_names.each_with_index do |family_name, index|
|
||||
create_family_and_user!(family_name, "user#{index == 0 ? "" : index + 1}@maybe.local")
|
||||
create_family_and_user!(family_name, "user#{index == 0 ? "" : index + 1}@maybe.local", require_onboarding: require_onboarding)
|
||||
end
|
||||
|
||||
puts "Users reset"
|
||||
|
@ -152,7 +152,7 @@ class Demo::Generator
|
|||
Security::Price.destroy_all
|
||||
end
|
||||
|
||||
def create_family_and_user!(family_name, user_email, currency: "USD")
|
||||
def create_family_and_user!(family_name, user_email, currency: "USD", require_onboarding: false)
|
||||
base_uuid = "d99e3c6e-d513-4452-8f24-dc263f8528c0"
|
||||
id = Digest::UUID.uuid_v5(base_uuid, family_name)
|
||||
|
||||
|
@ -160,7 +160,7 @@ class Demo::Generator
|
|||
id: id,
|
||||
name: family_name,
|
||||
currency: currency,
|
||||
stripe_subscription_status: "active",
|
||||
stripe_subscription_status: require_onboarding ? nil : "active",
|
||||
locale: "en",
|
||||
country: "US",
|
||||
timezone: "America/New_York",
|
||||
|
@ -173,7 +173,7 @@ class Demo::Generator
|
|||
last_name: "User",
|
||||
role: "admin",
|
||||
password: "password",
|
||||
onboarded_at: Time.current
|
||||
onboarded_at: require_onboarding ? nil : Time.current
|
||||
|
||||
family.users.create! \
|
||||
email: "member_#{user_email}",
|
||||
|
@ -181,7 +181,7 @@ class Demo::Generator
|
|||
last_name: "User",
|
||||
role: "member",
|
||||
password: "password",
|
||||
onboarded_at: Time.current
|
||||
onboarded_at: require_onboarding ? nil : Time.current
|
||||
end
|
||||
|
||||
def create_rules!(family)
|
||||
|
|
|
@ -131,6 +131,18 @@ class Family < ApplicationRecord
|
|||
stripe_subscription_status == "active"
|
||||
end
|
||||
|
||||
def trialing?
|
||||
!subscribed? && trial_started_at.present? && trial_started_at <= 14.days.from_now
|
||||
end
|
||||
|
||||
def trial_remaining_days
|
||||
(14 - (Time.current - trial_started_at).to_i / 86400).to_i
|
||||
end
|
||||
|
||||
def existing_customer?
|
||||
stripe_customer_id.present?
|
||||
end
|
||||
|
||||
def requires_data_provider?
|
||||
# If family has any trades, they need a provider for historical prices
|
||||
return true if trades.any?
|
||||
|
|
|
@ -19,6 +19,15 @@ class Provider::Registry
|
|||
end
|
||||
|
||||
private
|
||||
def stripe
|
||||
secret_key = ENV["STRIPE_SECRET_KEY"]
|
||||
webhook_secret = ENV["STRIPE_WEBHOOK_SECRET"]
|
||||
|
||||
return nil unless secret_key.present? && webhook_secret.present?
|
||||
|
||||
Provider::Stripe.new(secret_key:, webhook_secret:)
|
||||
end
|
||||
|
||||
def synth
|
||||
api_key = ENV.fetch("SYNTH_API_KEY", Setting.synth_api_key)
|
||||
|
||||
|
|
68
app/models/provider/stripe.rb
Normal file
68
app/models/provider/stripe.rb
Normal file
|
@ -0,0 +1,68 @@
|
|||
class Provider::Stripe
|
||||
def initialize(secret_key:, webhook_secret:)
|
||||
@client = Stripe::StripeClient.new(
|
||||
secret_key,
|
||||
stripe_version: "2025-04-30.basil"
|
||||
)
|
||||
@webhook_secret = webhook_secret
|
||||
end
|
||||
|
||||
def process_event(event_id)
|
||||
event = retrieve_event(event_id)
|
||||
|
||||
case event.type
|
||||
when /^customer\.subscription\./
|
||||
SubscriptionEventProcessor.new(client).process(event)
|
||||
when /^customer\./
|
||||
CustomerEventProcessor.new(client).process(event)
|
||||
else
|
||||
Rails.logger.info "Unhandled event type: #{event.type}"
|
||||
end
|
||||
end
|
||||
|
||||
def process_webhook_later(webhook_body, sig_header)
|
||||
thin_event = client.parse_thin_event(webhook_body, sig_header, webhook_secret)
|
||||
|
||||
if thin_event.type.start_with?("customer.")
|
||||
StripeEventHandlerJob.perform_later(thin_event.id)
|
||||
else
|
||||
Rails.logger.info "Unhandled event type: #{thin_event.type}"
|
||||
end
|
||||
end
|
||||
|
||||
def create_customer(email:, metadata: {})
|
||||
client.v1.customers.create(
|
||||
email: email,
|
||||
metadata: metadata
|
||||
)
|
||||
end
|
||||
|
||||
def get_checkout_session_url(price_id:, customer_id: nil, success_url: nil, cancel_url: nil)
|
||||
client.v1.checkout.sessions.create(
|
||||
customer: customer_id,
|
||||
line_items: [ { price: price_id, quantity: 1 } ],
|
||||
mode: "subscription",
|
||||
allow_promotion_codes: true,
|
||||
success_url: success_url,
|
||||
cancel_url: cancel_url
|
||||
).url
|
||||
end
|
||||
|
||||
def get_billing_portal_session_url(customer_id:, return_url: nil)
|
||||
client.v1.billing_portal.sessions.create(
|
||||
customer: customer_id,
|
||||
return_url: return_url
|
||||
).url
|
||||
end
|
||||
|
||||
def retrieve_checkout_session(session_id)
|
||||
client.v1.checkout.sessions.retrieve(session_id)
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :client, :webhook_secret
|
||||
|
||||
def retrieve_event(event_id)
|
||||
client.v2.core.events.retrieve(event_id)
|
||||
end
|
||||
end
|
20
app/models/provider/stripe/customer_event_processor.rb
Normal file
20
app/models/provider/stripe/customer_event_processor.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
class Provider::Stripe::CustomerEventProcessor < Provider::Stripe::EventProcessor
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
def process
|
||||
raise Error, "Family not found for Stripe customer ID: #{customer_id}" unless family
|
||||
|
||||
family.update(
|
||||
stripe_customer_id: customer_id
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
def family
|
||||
Family.find_by(stripe_customer_id: customer_id)
|
||||
end
|
||||
|
||||
def customer_id
|
||||
event_data.id
|
||||
end
|
||||
end
|
17
app/models/provider/stripe/event_processor.rb
Normal file
17
app/models/provider/stripe/event_processor.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
class Provider::Stripe::EventProcessor
|
||||
def initialize(event:, client:)
|
||||
@event = event
|
||||
@client = client
|
||||
end
|
||||
|
||||
def process
|
||||
raise NotImplementedError, "Subclasses must implement the process method"
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :event, :client
|
||||
|
||||
def event_data
|
||||
event.data.object
|
||||
end
|
||||
end
|
29
app/models/provider/stripe/subscription_event_processor.rb
Normal file
29
app/models/provider/stripe/subscription_event_processor.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
class Provider::Stripe::SubscriptionEventProcessor < Provider::Stripe::EventProcessor
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
def process
|
||||
raise Error, "Family not found for Stripe customer ID: #{customer_id}" unless family
|
||||
|
||||
family.update(
|
||||
stripe_plan_id: plan_id,
|
||||
stripe_subscription_status: subscription_status
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
def family
|
||||
Family.find_by(stripe_customer_id: customer_id)
|
||||
end
|
||||
|
||||
def customer_id
|
||||
event_data.customer
|
||||
end
|
||||
|
||||
def plan_id
|
||||
event_data.plan.id
|
||||
end
|
||||
|
||||
def subscription_status
|
||||
event_data.status
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue