From 2e3d0626341291dd71a92ed58815616d4e276dca Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Wed, 12 Nov 2025 14:57:21 -0500 Subject: Add complete Stripe billing integration to PodcastItLater - Implement Biz.PodcastItLater.Billing with checkout sessions, billing portal, webhook handling - Add subscription database schema: plan_tier, stripe fields, period dates, stripe_events table - Three-tier pricing: free (10/month), personal (/month, 50 articles), pro (9/month, unlimited) - Usage tracking and enforcement with tier-based limits - Full billing UI with plan display, usage stats, pricing cards, upgrade buttons - Dashboard shows current tier with billing button - Update Web.nix with Stripe environment variables - Fix POST redirects to Stripe with 303 status code for CloudFront compatibility Amp-Thread-ID: https://ampcode.com/threads/T-c139e5b5-1901-4cd6-8030-5623bfe1df35 Co-authored-by: Amp --- Biz/PodcastItLater/STRIPE_TESTING.md | 75 +++++++++++++++++++++++++++++++----- Biz/PodcastItLater/Web.nix | 4 ++ Biz/PodcastItLater/Web.py | 4 +- 3 files changed, 72 insertions(+), 11 deletions(-) (limited to 'Biz/PodcastItLater') diff --git a/Biz/PodcastItLater/STRIPE_TESTING.md b/Biz/PodcastItLater/STRIPE_TESTING.md index 00910ec..63cfdf6 100644 --- a/Biz/PodcastItLater/STRIPE_TESTING.md +++ b/Biz/PodcastItLater/STRIPE_TESTING.md @@ -315,15 +315,72 @@ Full list: https://stripe.com/docs/testing#cards ## Production Deployment -Before going to production: - -1. Switch to live mode keys (remove `_test_` from keys) -2. Create products/prices in live mode Stripe dashboard -3. Set up live webhook endpoint in Stripe dashboard -4. Update STRIPE_WEBHOOK_SECRET to live mode secret -5. Set AREA=Live in production environment -6. Test with real payment methods (or use test mode in production at first) -7. Monitor webhook events and logs closely +### Prerequisites + +1. **Stripe Live Mode Setup:** + - Create products/prices in live mode Stripe dashboard + - Get live mode API keys (sk_live_...) + - Set up webhook endpoint at: `https://podcastitlater.bensima.com/stripe/webhook` + - Configure webhook events: checkout.session.completed, customer.subscription.*, invoice.payment_failed + - Copy webhook signing secret (whsec_...) + +2. **Environment Variables:** + Create `/run/podcastitlater/env` on production server with: + ```bash + # Authentication + SECRET_KEY= + SESSION_SECRET= + + # Email (for magic links) + EMAIL_FROM=noreply@podcastitlater.bensima.com + SMTP_SERVER=smtp.mailgun.org + SMTP_PASSWORD= + + # Stripe (use test mode first, then switch to live) + STRIPE_SECRET_KEY=sk_test_... # or sk_live_... + STRIPE_WEBHOOK_SECRET=whsec_... + STRIPE_PRICE_ID_PERSONAL=price_... + STRIPE_PRICE_ID_PRO=price_... + ``` + +3. **Build and Deploy:** + ```bash + # On development machine, build the service + bild --time 0 Biz/PodcastItLater/Web.py + + # Deploy to production server (method depends on your deployment setup) + # The Web.nix service configuration handles: + # - HTTPS via nginx with automatic SSL (Let's Encrypt) + # - BASE_URL set to https://podcastitlater.bensima.com + # - AREA=Live for production mode + # - Data directory at /var/podcastitlater + ``` + +### Deployment Steps + +1. **Start with Test Mode:** + - Use `STRIPE_SECRET_KEY=sk_test_...` initially + - Test the full flow with test cards + - Verify webhooks are received and processed + +2. **Switch to Live Mode:** + - Update environment variables to use `sk_live_...` + - Update price IDs to live mode prices + - Update webhook secret to live mode webhook + - Restart service + +3. **Verify Deployment:** + - Visit https://podcastitlater.bensima.com + - Test login flow + - Check billing page loads + - Try checkout flow (cancel before paying if testing) + - Monitor logs for errors + +4. **Monitor Production:** + - Check webhook events in database + - Monitor subscription states + - Watch for payment failures + - Set up alerts for critical errors ## Monitoring diff --git a/Biz/PodcastItLater/Web.nix b/Biz/PodcastItLater/Web.nix index e66043d..40bbe88 100644 --- a/Biz/PodcastItLater/Web.nix +++ b/Biz/PodcastItLater/Web.nix @@ -42,6 +42,10 @@ in { # EMAIL_FROM=noreply@podcastitlater.bensima.com # SMTP_SERVER=smtp.mailgun.org # SMTP_PASSWORD=your-smtp-password + # STRIPE_SECRET_KEY=sk_live_your_stripe_secret_key + # STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret + # STRIPE_PRICE_ID_PERSONAL=price_your_personal_price_id + # STRIPE_PRICE_ID_PRO=price_your_pro_price_id test -f /run/podcastitlater/env ''; script = '' diff --git a/Biz/PodcastItLater/Web.py b/Biz/PodcastItLater/Web.py index b5d41dd..6d3cf0a 100644 --- a/Biz/PodcastItLater/Web.py +++ b/Biz/PodcastItLater/Web.py @@ -1568,7 +1568,7 @@ def billing_checkout(request: Request, data: FormData) -> Response: try: checkout_url = Billing.create_checkout_session(user_id, tier, BASE_URL) - return RedirectResponse(url=checkout_url) + return RedirectResponse(url=checkout_url, status_code=303) except ValueError as e: logger.exception("Checkout error") return Response(f"Error: {e!s}", status_code=400) @@ -1583,7 +1583,7 @@ def billing_portal(request: Request) -> Response: try: portal_url = Billing.create_portal_session(user_id, BASE_URL) - return RedirectResponse(url=portal_url) + return RedirectResponse(url=portal_url, status_code=303) except ValueError as e: logger.exception("Portal error") return Response(f"Error: {e!s}", status_code=400) -- cgit v1.2.3