freedge/backend/package.json
ordinarthur b0e9425ed5 feat(stripe): on-demand sync fallback when webhooks are missed
Problem: if stripe listen is not running (dev) or the webhook secret is
misconfigured, a successful checkout leaves the user stuck on the free
plan in the DB even though Stripe knows they're subscribed.

Solution: 3 recovery mechanisms.

1. Backend: POST /stripe/sync (auth required)
   Fetches the current user's subscriptions from Stripe by customer ID,
   picks the most recent active/trialing/past_due one, and applies it to
   the User row via the same applySubscriptionToUser helper used by the
   webhook. If no active sub exists, downgrades to free. Returns the
   current plan state.

2. Frontend: CheckoutSuccess now calls /stripe/sync first (instant,
   reliable) before falling back to polling /stripe/subscription. This
   fixes the 'just paid but still free' bug even with no webhook setup.

3. Frontend: 'Rafraîchir' button on the Profile free-plan upgrade banner
   (ghost style with RefreshCw spinning icon). Tooltip hints at its
   purpose. Users who paid but see the free state can click it to
   self-heal in one click.

4. Backend script: scripts/sync-subscription.ts
   - npm run stripe:sync -- user@example.com  (sync one user by email)
   - npm run stripe:sync -- --all             (sync every user with a
                                                stripeId, useful after
                                                a prod webhook outage)
   Colored output with ✓ / ✗ / ↷ status per user.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:07:02 +02:00

53 lines
1.5 KiB
JSON

{
"name": "recipe-app-backend",
"version": "1.0.0",
"description": "Backend Freedge (TypeScript)",
"main": "dist/server.js",
"scripts": {
"start": "node dist/server.js",
"dev": "tsx watch src/server.ts",
"build": "tsc",
"typecheck": "tsc --noEmit",
"migrate": "prisma migrate dev",
"studio": "prisma studio",
"stripe:setup": "tsx scripts/setup-stripe.ts",
"stripe:setup:write": "tsx scripts/setup-stripe.ts --write-env",
"stripe:listen": "stripe listen --forward-to localhost:3000/stripe/webhook",
"stripe:sync": "tsx scripts/sync-subscription.ts",
"lint": "eslint src",
"format": "prettier --write \"src/**/*.{ts,json}\""
},
"dependencies": {
"@fastify/cors": "^8.5.0",
"@fastify/helmet": "^11.1.1",
"@fastify/jwt": "^7.0.0",
"@fastify/multipart": "^8.0.0",
"@fastify/rate-limit": "^9.1.0",
"@fastify/static": "^7.0.4",
"@prisma/client": "^5.0.0",
"bcrypt": "^5.1.1",
"dotenv": "^16.3.1",
"fastify": "^4.19.0",
"fastify-plugin": "^4.5.0",
"google-auth-library": "^9.15.1",
"minio": "^8.0.5",
"nodemailer": "^6.10.0",
"openai": "^4.0.0",
"resend": "^4.1.2",
"stripe": "^12.12.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/bcrypt": "^5.0.2",
"@types/node": "^22.13.9",
"@types/nodemailer": "^6.4.17",
"eslint": "^9.21.0",
"globals": "^15.15.0",
"prettier": "^3.3.0",
"prisma": "^5.0.0",
"tsx": "^4.19.0",
"typescript": "^5.7.2",
"typescript-eslint": "^8.24.1"
}
}