Skip to content
This repository was archived by the owner on Dec 16, 2025. It is now read-only.

Conversation

@jorgemanrubia
Copy link
Member

@jorgemanrubia jorgemanrubia commented Dec 10, 2025


before_action :set_account, only: %i[ edit update ]

def index
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a simple admin screen where you can modify the card count in a given account. It is intended mostly to test this in staging.

class Admin::StatsController < AdminController
layout "public"

def show
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to the gem, but unrelated to the billing system.

end

private
def dispatch_stripe_event(event)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @zachasme I would love your eyes on how we are handling stripe webhooks and on the flow in general.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great @jorgemanrubia!

I only see one potential issue that have bitten me in the past: Stripe doesn’t guarantee the delivery of events in the order that they’re generated (WTF?). If I recall correctly, a successful checkout session will generate:

  • checkout.session.completed
  • customer.subscription.created
  • customer.subscription.updated

almost always in that order. Additionally, event data will correspond to the time it was dispatched, so if you handle created after updated you could end up in a bad state.

I have found the best solution is not to rely on any data provided in event payloads, except identifiers. I would recommended something like:

when "checkout.session.completed"
  sync_subscription(event.data.object.subscription) if event.data.object.mode == "subscription"
when "customer.subscription.updated"
when "customer.subscription.deleted"
  sync_subscription(event.data.object.id)

#...

def sync_subscription(stripe_id)
  stripe_subscription = Stripe::Subscription.retrieve(stripe_id)
  if subscription = find_subscription_by_customer(stripe_subscription.customer)
    cancellation_reason = stripe_subscription.cancellation_details.reason
    subscription.update! #...
  end
end

I also think stripe dispatches customer.subscription.deleted when payment fails, in which case you can use stripe_subscription.cancellation_details.reason instead of listening for invoice.payment_failed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @zachasme, super useful, I will tackle this tomorrow.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @zachasme could you have a second look at the changes 🙏?

@jorgemanrubia jorgemanrubia force-pushed the billing branch 9 times, most recently from b855ebc to 7df55bc Compare December 11, 2025 20:44
jorgemanrubia added a commit that referenced this pull request Dec 12, 2025
Comment on lines +18 to +21
when "checkout.session.completed"
sync_new_subscription(event.data.object.subscription, plan_key: event.data.object.metadata["plan_key"]) if event.data.object.mode == "subscription"
when "customer.subscription.updated", "customer.subscription.deleted"
sync_subscription(event.data.object.id)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think there's going to be an issue if checkout.session.completed comes in last, since the subscription plan_key is being validated for presence. I'd try and stick to calling sync_subscription directly from all branches, and read the plan_key from subscription metadata in all cases.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, the subscription is already created with a plan_key before checkout, so probably not an issue. But generally I still think we should avoid branching logic based on event type.

Comment on lines +28 to +39
secrets_escaped = `kamal secrets fetch \
--adapter 1password \
--account basecamp \
--from "Deploy/Fizzy" \
"Development/STRIPE_SECRET_KEY" \
"Development/STRIPE_MONTHLY_V1_PRICE_ID" 2>/dev/null`

secrets_json = secrets_escaped.gsub(/\\(.)/, '\1')
secrets = JSON.parse(secrets_json)

stripe_secret_key = secrets["Deploy/Fizzy/Development/STRIPE_SECRET_KEY"]
stripe_price_id = secrets["Deploy/Fizzy/Development/STRIPE_MONTHLY_V1_PRICE_ID"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jorgemanrubia curious if you looked at using op run for handling the secrets, rather than Kamal?

It ought to take care of some of these details for you, and would let you move the secret & env names into a file like .stripe-env rather than have them inside the script. Then you could exec both the stripe listen and bin/dev with the necessary secrets in place.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevinmcconnell thanks, that would be an improvement indeed. I will track this one in Basecamp, as I want to finish up other stuff this week first.

@jorgemanrubia jorgemanrubia merged commit 8eaa692 into main Dec 16, 2025
3 checks passed
@jorgemanrubia jorgemanrubia deleted the billing branch December 16, 2025 15:27
jeremy pushed a commit to basecamp/fizzy that referenced this pull request Dec 16, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants