mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-05 05:25:24 +02:00
Subscription tests and domain (#2209)
* Save work * Subscriptions and trials domain * Store family ID on customer * Remove indirection of stripe calls * Test simplifications * Update brakeman * Fix stripe tests in CI * Update billing page to show subscription details * Remove legacy columns * Complete billing settings page * Fix hardcoded plan name * Handle subscriptions for self hosting mode * Lint fixes
This commit is contained in:
parent
8c10e87387
commit
5da4bb6dc3
40 changed files with 1041 additions and 233 deletions
|
@ -1,9 +1,8 @@
|
|||
class Provider::Stripe
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
def initialize(secret_key:, webhook_secret:)
|
||||
@client = Stripe::StripeClient.new(
|
||||
secret_key,
|
||||
stripe_version: "2025-04-30.basil"
|
||||
)
|
||||
@client = Stripe::StripeClient.new(secret_key)
|
||||
@webhook_secret = webhook_secret
|
||||
end
|
||||
|
||||
|
@ -12,56 +11,77 @@ class Provider::Stripe
|
|||
|
||||
case event.type
|
||||
when /^customer\.subscription\./
|
||||
SubscriptionEventProcessor.new(event: event, client: client).process
|
||||
when /^customer\./
|
||||
CustomerEventProcessor.new(event: event, client: client).process
|
||||
SubscriptionEventProcessor.new(event).process
|
||||
else
|
||||
Rails.logger.info "Unhandled event type: #{event.type}"
|
||||
Rails.logger.warn "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
|
||||
StripeEventHandlerJob.perform_later(thin_event.id)
|
||||
end
|
||||
|
||||
def create_customer(email:, metadata: {})
|
||||
client.v1.customers.create(
|
||||
email: email,
|
||||
metadata: metadata
|
||||
def create_checkout_session(plan:, family_id:, family_email:, success_url:, cancel_url:)
|
||||
customer = client.v1.customers.create(
|
||||
email: family_email,
|
||||
metadata: {
|
||||
family_id: family_id
|
||||
}
|
||||
)
|
||||
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 } ],
|
||||
session = client.v1.checkout.sessions.create(
|
||||
customer: customer.id,
|
||||
line_items: [ { price: price_id_for(plan), quantity: 1 } ],
|
||||
mode: "subscription",
|
||||
allow_promotion_codes: true,
|
||||
success_url: success_url,
|
||||
cancel_url: cancel_url
|
||||
).url
|
||||
)
|
||||
|
||||
NewCheckoutSession.new(url: session.url, customer_id: customer.id)
|
||||
end
|
||||
|
||||
def get_billing_portal_session_url(customer_id:, return_url: nil)
|
||||
def get_checkout_result(session_id)
|
||||
session = client.v1.checkout.sessions.retrieve(session_id)
|
||||
|
||||
unless session.status == "complete" && session.payment_status == "paid"
|
||||
raise Error, "Checkout session not complete"
|
||||
end
|
||||
|
||||
CheckoutSessionResult.new(success?: true, subscription_id: session.subscription)
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
Rails.logger.error "Error fetching checkout result for session #{session_id}: #{e.message}"
|
||||
CheckoutSessionResult.new(success?: false, subscription_id: nil)
|
||||
end
|
||||
|
||||
def create_billing_portal_session_url(customer_id:, return_url:)
|
||||
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)
|
||||
def update_customer_metadata(customer_id:, metadata:)
|
||||
client.v1.customers.update(customer_id, metadata: metadata)
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :client, :webhook_secret
|
||||
|
||||
NewCheckoutSession = Data.define(:url, :customer_id)
|
||||
CheckoutSessionResult = Data.define(:success?, :subscription_id)
|
||||
|
||||
def price_id_for(plan)
|
||||
prices = {
|
||||
monthly: ENV["STRIPE_MONTHLY_PRICE_ID"],
|
||||
annual: ENV["STRIPE_ANNUAL_PRICE_ID"]
|
||||
}
|
||||
|
||||
prices[plan.to_sym || :monthly]
|
||||
end
|
||||
|
||||
def retrieve_event(event_id)
|
||||
client.v1.events.retrieve(event_id)
|
||||
end
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
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
|
|
@ -1,7 +1,6 @@
|
|||
class Provider::Stripe::EventProcessor
|
||||
def initialize(event:, client:)
|
||||
def initialize(event)
|
||||
@event = event
|
||||
@client = client
|
||||
end
|
||||
|
||||
def process
|
||||
|
@ -9,7 +8,7 @@ class Provider::Stripe::EventProcessor
|
|||
end
|
||||
|
||||
private
|
||||
attr_reader :event, :client
|
||||
attr_reader :event
|
||||
|
||||
def event_data
|
||||
event.data.object
|
||||
|
|
|
@ -2,28 +2,28 @@ class Provider::Stripe::SubscriptionEventProcessor < Provider::Stripe::EventProc
|
|||
Error = Class.new(StandardError)
|
||||
|
||||
def process
|
||||
raise Error, "Family not found for Stripe customer ID: #{customer_id}" unless family
|
||||
raise Error, "Family not found for Stripe customer ID: #{subscription.customer}" unless family
|
||||
|
||||
family.update(
|
||||
stripe_plan_id: plan_id,
|
||||
stripe_subscription_status: subscription_status
|
||||
family.subscription.update(
|
||||
stripe_id: subscription.id,
|
||||
status: subscription.status,
|
||||
interval: subscription_details.plan.interval,
|
||||
amount: subscription_details.plan.amount / 100.0, # Stripe returns cents, we report dollars
|
||||
currency: subscription_details.plan.currency.upcase,
|
||||
current_period_ends_at: Time.at(subscription_details.current_period_end)
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
def family
|
||||
Family.find_by(stripe_customer_id: customer_id)
|
||||
Family.find_by(stripe_customer_id: subscription.customer)
|
||||
end
|
||||
|
||||
def customer_id
|
||||
event_data.customer
|
||||
def subscription_details
|
||||
event_data.items.data.first
|
||||
end
|
||||
|
||||
def plan_id
|
||||
event_data.plan.id
|
||||
end
|
||||
|
||||
def subscription_status
|
||||
event_data.status
|
||||
def subscription
|
||||
event_data
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue