feat: visual polish, nav login link, pricing badge fix, cursor fix, button contrast
- Hero mockup: enhanced 3D perspective and shadow - Testimonials: illustrated SVG avatars - Growth pricing card: visual prominence (scale, gradient, badge) - Most Popular badge: repositioned to avoid overlapping heading - Nav: added Log In link next to Start Free Trial - Fixed btn-primary text colour on anchor tags (white on blue) - Fixed cursor: default on all non-interactive elements - Disabled user-select on non-form content to prevent text caret
This commit is contained in:
268
BILLING_API_EXAMPLES.md
Normal file
268
BILLING_API_EXAMPLES.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# TenderRadar Billing API Examples
|
||||
|
||||
Quick reference for testing billing endpoints. Replace `AUTH_TOKEN` with a real JWT token from `/api/auth/login`.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# 1. Register a user
|
||||
curl -X POST http://localhost:3456/api/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "test@example.com",
|
||||
"password": "testpass123",
|
||||
"company_name": "Test Corp"
|
||||
}'
|
||||
|
||||
# Response includes: user object and JWT token
|
||||
# Save the token for subsequent calls:
|
||||
export AUTH_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
```
|
||||
|
||||
## 1. Create Checkout Session
|
||||
|
||||
Start the subscription flow by creating a checkout session:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3456/api/billing/checkout \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $AUTH_TOKEN" \
|
||||
-d '{
|
||||
"plan": "starter",
|
||||
"successUrl": "https://app.example.com/billing/success",
|
||||
"cancelUrl": "https://app.example.com/billing/cancel"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"sessionId": "cs_test_abc123...",
|
||||
"url": "https://checkout.stripe.com/pay/cs_test_abc123..."
|
||||
}
|
||||
```
|
||||
|
||||
**Next steps:**
|
||||
1. Open the `url` in a browser
|
||||
2. Complete the payment form (test card: 4242 4242 4242 4242)
|
||||
3. User is redirected to `successUrl`
|
||||
4. Stripe sends webhook to `/api/billing/webhook`
|
||||
|
||||
## 2. Get Subscription Status
|
||||
|
||||
Check the current subscription status (after completing checkout):
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:3456/api/billing/subscription \
|
||||
-H "Authorization: Bearer $AUTH_TOKEN"
|
||||
```
|
||||
|
||||
**Response (with active subscription):**
|
||||
```json
|
||||
{
|
||||
"subscription": {
|
||||
"id": 1,
|
||||
"user_id": 5,
|
||||
"stripe_customer_id": "cus_ABC123...",
|
||||
"stripe_subscription_id": "sub_ABC123...",
|
||||
"plan": "starter",
|
||||
"status": "active",
|
||||
"trial_start": "2026-02-14T12:49:00.000Z",
|
||||
"trial_end": "2026-02-28T12:49:00.000Z",
|
||||
"current_period_start": "2026-02-14T12:49:00.000Z",
|
||||
"current_period_end": "2026-03-14T12:49:00.000Z",
|
||||
"cancel_at_period_end": false,
|
||||
"created_at": "2026-02-14T12:49:00.000Z",
|
||||
"updated_at": "2026-02-14T12:49:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (no subscription):**
|
||||
```json
|
||||
{
|
||||
"subscription": null,
|
||||
"message": "No active subscription. User is on free tier."
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Create Customer Portal Session
|
||||
|
||||
Allow users to manage their subscription (upgrade, downgrade, cancel):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3456/api/billing/portal \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $AUTH_TOKEN" \
|
||||
-d '{
|
||||
"returnUrl": "https://app.example.com/account/billing"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"url": "https://billing.stripe.com/session/cs_test_abc123..."
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
1. Open the `url` in a browser (user must be logged into Stripe or their payment method)
|
||||
2. User can upgrade/downgrade plans, update payment method, or cancel
|
||||
3. After managing subscription, redirected back to `returnUrl`
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### Scenario 1: New User Signup → Checkout
|
||||
|
||||
```bash
|
||||
# 1. Register
|
||||
TOKEN=$(curl -s -X POST http://localhost:3456/api/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "newuser@example.com",
|
||||
"password": "pass123",
|
||||
"company_name": "New Corp"
|
||||
}' | jq -r '.token')
|
||||
|
||||
# 2. Create checkout session
|
||||
curl -X POST http://localhost:3456/api/billing/checkout \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"plan": "growth",
|
||||
"successUrl": "https://app.example.com/success",
|
||||
"cancelUrl": "https://app.example.com/cancel"
|
||||
}' | jq '.url'
|
||||
|
||||
# 3. Open URL in browser and complete payment
|
||||
# (Use test card 4242 4242 4242 4242, any future expiry, any 3-digit CVC)
|
||||
|
||||
# 4. Check subscription status (after webhook processes)
|
||||
curl -X GET http://localhost:3456/api/billing/subscription \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
### Scenario 2: Upgrade Plan
|
||||
|
||||
```bash
|
||||
# 1. User is on "starter" plan, wants to upgrade to "pro"
|
||||
|
||||
# 2. Create new checkout session (Stripe recognizes customer, handles proration)
|
||||
curl -X POST http://localhost:3456/api/billing/checkout \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"plan": "pro",
|
||||
"successUrl": "https://app.example.com/success",
|
||||
"cancelUrl": "https://app.example.com/cancel"
|
||||
}'
|
||||
|
||||
# 3. Complete payment
|
||||
# 4. Webhook updates subscription to "pro" plan
|
||||
```
|
||||
|
||||
### Scenario 3: Manage via Customer Portal
|
||||
|
||||
```bash
|
||||
# User goes to billing page in app and clicks "Manage Subscription"
|
||||
|
||||
# Create portal session
|
||||
curl -X POST http://localhost:3456/api/billing/portal \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"returnUrl": "https://app.example.com/account/billing"
|
||||
}' | jq '.url'
|
||||
|
||||
# Open URL in browser
|
||||
# User can downgrade, upgrade, or cancel without returning to app
|
||||
```
|
||||
|
||||
## Testing with Stripe CLI (Local Webhooks)
|
||||
|
||||
To test webhooks locally without exposing your server:
|
||||
|
||||
```bash
|
||||
# 1. Install Stripe CLI (if not already installed)
|
||||
# macOS: brew install stripe/stripe-cli/stripe
|
||||
# Linux/Windows: See https://stripe.com/docs/stripe-cli
|
||||
|
||||
# 2. Login to your Stripe account
|
||||
stripe login
|
||||
|
||||
# 3. Start listening and forwarding to local server
|
||||
stripe listen --forward-to localhost:3456/api/billing/webhook
|
||||
|
||||
# Output will show: Ready! Your webhook signing secret is: whsec_test_abc123...
|
||||
# Update .env: STRIPE_WEBHOOK_SECRET=whsec_test_abc123...
|
||||
|
||||
# 4. In another terminal, trigger test events:
|
||||
stripe trigger checkout.session.completed
|
||||
|
||||
# Webhook will be forwarded to localhost:3456/api/billing/webhook
|
||||
```
|
||||
|
||||
## Test Cards
|
||||
|
||||
Stripe provides test cards for different scenarios:
|
||||
|
||||
| Card Number | Result | Expiry | CVC |
|
||||
|---|---|---|---|
|
||||
| 4242 4242 4242 4242 | Successful charge | Any future | Any 3 digits |
|
||||
| 4000 0025 0000 3155 | Insufficient funds | Any future | Any 3 digits |
|
||||
| 4000 0000 0000 0002 | Card declined | Any future | Any 3 digits |
|
||||
| 4000 0000 0000 0010 | Address verification failed | Any future | Any 3 digits |
|
||||
|
||||
## Error Responses
|
||||
|
||||
### Missing Authorization Token
|
||||
```bash
|
||||
curl -X GET http://localhost:3456/api/billing/subscription
|
||||
|
||||
# Response:
|
||||
{"error": "No token provided"}
|
||||
```
|
||||
|
||||
### Invalid Plan
|
||||
```bash
|
||||
curl -X POST http://localhost:3456/api/billing/checkout \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"plan": "invalid_plan", ...}'
|
||||
|
||||
# Response:
|
||||
{"error": "Invalid plan: invalid_plan"}
|
||||
```
|
||||
|
||||
### No Active Subscription
|
||||
```bash
|
||||
curl -X POST http://localhost:3456/api/billing/portal \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"returnUrl": "..."}'
|
||||
|
||||
# Response (if user not subscribed):
|
||||
{"error": "No subscription found for user"}
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
Check server logs for detailed webhook processing:
|
||||
|
||||
```bash
|
||||
# Terminal running the server shows:
|
||||
# Processing webhook event: checkout.session.completed
|
||||
# Subscription created for user 5 on plan starter
|
||||
```
|
||||
|
||||
Database query to check subscription status:
|
||||
|
||||
```bash
|
||||
# Connect to PostgreSQL
|
||||
psql -U tenderpilot -d tenderpilot -h localhost
|
||||
|
||||
# Check subscriptions
|
||||
SELECT * FROM subscriptions WHERE user_id = 5;
|
||||
|
||||
# Check user tier
|
||||
SELECT id, email, tier FROM users WHERE id = 5;
|
||||
```
|
||||
Reference in New Issue
Block a user