agmission/Development/server/docs/SETUP_INTENT_IMPLEMENTATION.md

507 lines
15 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Setup Intent Pattern Implementation - Summary
**Date**: January 14, 2026
**Feature**: Pre-authenticate payment methods for multiple subscriptions
**Status**: ✅ **COMPLETED**
---
## ⚠️ When to Use Setup Intent
### ✅ USE Setup Intent When:
1. **No Immediate Charge Scenarios**:
- User updates `cancel_at_period_end=false` (reactivates subscription) with NEW unverified card
- Adding payment method for future billing (subscription in trial)
- Changing payment method on existing active subscription (next charge is future)
- Pre-validating card before scheduled billing date
2. **Multiple Subscriptions + No Immediate Charge**:
- Creating package + addons with trial period
- All subscriptions start billing in the future
### ❌ DO NOT Use Setup Intent When:
1. **Immediate Charge Required**:
- Creating new subscription with immediate charge (no trial)
- Upgrading/downgrading existing subscription (immediate proration charge)
- First payment happens immediately
2. **Why?**:
- Setup Intent authenticates card for **future off-session payments**
- **First payment** during subscription creation is **on-session** - requires 3DS authentication AGAIN
- Results in **double authentication** (Setup Intent + Subscription Payment)
- Better to handle 3DS directly during subscription creation
### 💡 Recommendation:
**For Immediate Charges**: Skip Setup Intent, use direct subscription creation. Backend already handles 3DS correctly:
- Creates subscription → detects `requires_action` → returns `client_secret`
- Frontend completes 3DS → subscription finalizes
- Works for single OR multiple subscriptions (see Multi-Subscription Handling below)
**For Future Charges**: Use Setup Intent to pre-authenticate card without double authentication.
---
## 🎯 Problem Solved (Original Use Case)
When creating multiple subscriptions (package + addons) with the same card requiring 3DS authentication:
**Before** ❌:
- Package subscription created → triggers 3DS popup
- Addon subscription attempts charge → fails (3DS not complete)
- Result: Partial subscription creation (package active, addon failed)
- Customer confusion and revenue loss
**After** ✅:
- Card authenticated once via SetupIntent → single 3DS popup
- All subscriptions created with pre-authenticated card
- Result: Atomic creation (all succeed or none created)
- Better UX and no revenue loss
---
## <20> Multi-Subscription Handling
### How Multiple Subscriptions Work
When calling `POST /api/subscription/update` with package + addons:
```javascript
// Request creates 2 subscriptions:
{
"package": "ess_1", // Subscription 1: Package
"addons": [{ // Subscription 2: Addons
"price": "addon_1",
"quantity": 1
}]
}
```
### Execution Flow (Immediate Charge):
1. **Create Package Subscription**:
- `createSubscription()` called for package
- If 3DS required → throws error with `client_secret`
- Subscription created in `incomplete` status
- Error caught, returned to frontend with subscription data
2. **Addon Subscription NOT Created**:
- Package creation threw 3DS error
- Addon creation skipped (error interrupts flow)
- This is **correct behavior** - prevents partial creation
3. **Frontend Completes 3DS**:
- Customer authenticates payment
- Frontend calls `stripe.confirmCardPayment(client_secret)`
- Package subscription becomes `active`
4. **Create Addon Subscription**:
- Frontend calls `/api/subscription/update` again with SAME params
- Package already exists (no change)
- Addon subscription created
- **⚠️ IMPORTANT**: Addon WILL require 3DS again (see test findings below)
### ⚠️ Test Findings: Each Subscription Requires 3DS
**Test Date**: January 16, 2026
**Test File**: [tests/test_multi_subscription_auth.js](../tests/test_multi_subscription_auth.js)
**Key Discovery**:
- ❌ Stripe does NOT reuse 3DS authentication between PaymentIntents
- Each subscription creates its own PaymentIntent
- Even when using the same payment method seconds apart, each requires separate 3DS
**Test Results**:
```
Package subscription → requires 3DS ✓
Addon subscription (same card, <5 seconds later) → requires 3DS AGAIN ✓
```
**Impact on Implementation**:
1. Frontend MUST handle 3DS for EACH subscription call
2. When calling `/update` twice:
- First call: Package requires 3DS → Frontend authenticates
- Second call: Addon requires 3DS → Frontend authenticates AGAIN
3. No shortcut - cannot batch authenticate multiple subscriptions
**💡 Recommendation (January 16, 2026)**:
- **Accept multiple 3DS popups** - this scenario is RARE (<2% of checkouts)
- 3DS cards: ~5-15% of all cards
- Multiple subscriptions: ~10-20% of checkouts
- **Both together**: Very uncommon
- Simpler code, immediate charging, better UX than workarounds
### Why This Works:
**Atomic Operations**: Each subscription creation is independent
**No Partial State**: Either all succeed or none created (customer completes 3DS for each)
**Idempotent**: Calling `/update` multiple times with same params is safe
**Card Already Authenticated**: ~~Second subscription rarely requires 3DS~~ **INCORRECT** - Always requires 3DS
**Each PaymentIntent Independent**: Stripe security policy - no authentication reuse
**Rarely An Issue**: <2% of checkouts have 3DS + multiple subscriptions
**Simple Implementation**: No workarounds needed for rare edge case
### Alternative: Delay First Charge with Trial Period
** Edge Case Only** - Not recommended for normal flow (adds complexity for <2% of checkouts)
**What Happens**:
```javascript
// Step 1: Authenticate card with Setup Intent (ONE 3DS popup)
POST /api/subscription/setupCard { custId, pmId }
// Customer completes 3DS authentication
// Step 2: Create subscriptions with trial (NO additional 3DS)
POST /api/subscription/update {
"package": "ess_1",
"addons": [{"price": "addon_1", "quantity": 1}],
"trial_period_days": 1 // Delay charge by 1 day
}
```
**Result**:
- Both subscriptions created immediately (active status)
- Customer gets access NOW
- Only ONE 3DS popup (during Setup Intent)
- In 1 day: Stripe charges automatically (no popup)
** Trade-offs**:
- Revenue delayed by 1 day
- Customer not charged immediately (may be confusing)
- Need to handle "payment pending" messaging
**When to Use**: Only when UX (avoiding multiple popups) is more important than immediate charging
---
## <20>📝 Implementation Details
### 1. Backend Changes
#### New Endpoint: `POST /api/subscription/setupCard`
**File**: [controllers/subscription.js](../controllers/subscription.js#L769)
**Parameters**:
```json
{
"custId": "cus_xxx", // Stripe customer ID
"pmId": "pm_xxx" // Payment method ID to authenticate
}
```
**Response (No 3DS)**:
```json
{
"requiresAction": false,
"status": "succeeded",
"setupIntentId": "seti_xxx",
"message": "Card authenticated successfully"
}
```
**Response (3DS Required)**:
```json
{
"requiresAction": true,
"clientSecret": "seti_xxx_secret_xxx",
"setupIntentId": "seti_xxx",
"status": "requires_action",
"message": "Card authentication required"
}
```
**Features**:
- Full JSDoc documentation for apidoc generation
- Validates payment method and customer
- Attaches payment method to customer if needed
- Creates SetupIntent for off-session usage
- Returns client_secret for 3DS authentication
- Comprehensive error handling (card errors, invalid requests)
- Debug logging throughout
#### Route Configuration
**File**: [routes/subscription.js](../routes/subscription.js)
```javascript
router.post('/setupCard', memberCtl.setupCardAuthentication_post);
```
- Uses existing authentication middleware (global auth from server.js)
- Follows project's camelCase endpoint naming convention
#### Module Exports
**File**: [controllers/subscription.js](../controllers/subscription.js#L3341)
Added `setupCardAuthentication_post` to module.exports list.
---
### 2. Documentation Updates
#### FRONTEND_3DS_IMPLEMENTATION.md
**Location**: [FRONTEND_3DS_IMPLEMENTATION.md](FRONTEND_3DS_IMPLEMENTATION.md)
**Added**:
- Complete Setup Intent Pattern section with overview
- Backend endpoint documentation
- Frontend implementation guide (TypeScript/Angular)
- Stripe service with confirmCardSetup() method
- Subscription service with setupCard() method
- Complete subscription component example
- HTML template with loading states
- Testing scenarios for all card types
- Flow comparison diagrams (before/after)
- Decision guide (when to use Setup Intent vs Direct)
- Comparison matrix with feature breakdown
#### PAYMENT_FAILURE_HANDLING.md
**Location**: [PAYMENT_FAILURE_HANDLING.md](PAYMENT_FAILURE_HANDLING.md)
**Added**:
- Setup Intent Pattern section
- Problem statement for multiple subscriptions
- Complete backend implementation with code
- Frontend flow examples
- Benefits analysis
- Comparison table (Direct vs Setup Intent)
- Testing instructions
- When to use guidance
---
### 3. Test Script
#### test_setup_intent.js
**Location**: [../tests/test_setup_intent.js](../tests/test_setup_intent.js)
**Features**:
- Tests all three card scenarios:
- Regular card (4242424242424242) - No 3DS
- 3DS card (4000000000003220) - Requires authentication
- Failed card (4000000000000341) - Always declines
- Creates test customer
- Verifies SetupIntent status and client_secret
- Automated cleanup
- Formatted output with success/failure indicators
- Environment loading from environment.env
**Usage**:
```bash
node test_setup_intent.js
```
---
## 🎨 Frontend Integration Guide
### Step 1: Authenticate Card
```typescript
// Call backend to setup card
const setupResult = await api.setupCard(custId, pmId);
// Handle 3DS if required
if (setupResult.requiresAction) {
const setupIntent = await stripe.confirmCardSetup(setupResult.clientSecret);
if (setupIntent.status !== 'succeeded') {
throw new Error('Authentication failed');
}
}
```
### Step 2: Create Subscriptions
```typescript
// Card is now authenticated, create all subscriptions
const subscriptions = await api.updateSubscriptions({
package: 'ess_1',
addons: [{ price: 'addon_1', quantity: 1 }],
pmId: pmId // Already authenticated
});
```
### Complete Component Example
See [FRONTEND_3DS_IMPLEMENTATION.md](docs/FRONTEND_3DS_IMPLEMENTATION.md#3-update-subscription-component) for full implementation.
---
## 🧪 Testing
### Manual Testing
```bash
# 1. Start server
DEBUG=agm:* node server.js
# 2. Test the endpoint
curl -X POST http://localhost:4100/api/subscription/setupCard \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"custId":"cus_xxx","pmId":"pm_xxx"}'
# 3. Run automated tests
node tests/test_setup_intent.js
```
### Expected Results
| Card | Expected Status | Expected Response |
|------|----------------|-------------------|
| 4242424242424242 | succeeded | requiresAction: false |
| 4000000000003220 | requires_action | requiresAction: true + clientSecret |
| 4000000000000341 | Error | Stripe card error |
---
## 📊 Benefits
### Technical Benefits
- **No Partial Subscriptions**: Atomic creation (all or nothing)
- **Better Error Handling**: Clear authentication failure vs payment failure
- **SCA Compliant**: Meets Strong Customer Authentication requirements
- **Reusable Pattern**: Can apply to any multi-charge scenario
- **Future-Proof**: Works with upcoming payment regulations
### Business Benefits
- **Increased Revenue**: No lost addon subscriptions
- **Better UX**: Single authentication step, clear flow
- **Reduced Support**: Fewer customer confusion issues
- **Higher Conversion**: Smooth checkout process
### User Experience Benefits
- **Single 3DS Popup**: Not multiple confusing popups
- **Clear Status**: Loading messages guide the user
- **Fast Checkout**: Pre-authentication then quick creation
- **Error Clarity**: Know why authentication failed
---
## 🔄 Migration Strategy
### Phase 1: Deploy (Current)
- Backend endpoint deployed
- Documentation complete
- Test script available
- Ready for frontend integration
### Phase 2: Frontend Updates (Next)
1. Update Stripe service with confirmCardSetup()
2. Update subscription service with setupCard()
3. Update subscription component to use Setup Intent pattern
4. Test with all card types
5. Monitor success rates
### Phase 3: Gradual Rollout
1. Enable for new subscriptions first
2. A/B test with control group
3. Monitor metrics (completion rates, error rates)
4. Roll out to all users once validated
### Phase 4: Full Adoption
1. Make Setup Intent pattern default for multi-subscription flows
2. Keep Direct pattern for single subscriptions
3. Update all documentation
4. Train support team on new flow
---
## Related Documentation
- [FRONTEND_3DS_IMPLEMENTATION.md](FRONTEND_3DS_IMPLEMENTATION.md) - Complete frontend guide
- [PAYMENT_FAILURE_HANDLING.md](PAYMENT_FAILURE_HANDLING.md) - Payment verification
- [SUBSCRIPTION_PROMO_INTEGRATION.md](SUBSCRIPTION_PROMO_INTEGRATION.md) - Promo handling
- [controllers/subscription.js](../controllers/subscription.js) - Backend implementation
---
## ❓ FAQ
### Q: Should I always use Setup Intent?
**A**: Use Setup Intent for multiple subscriptions (package + addons). Use Direct pattern for single subscriptions.
### Q: What happens if 3DS authentication fails?
**A**: No subscriptions are created. User sees clear error message and can try different card.
### Q: Does this work with existing cards?
**A**: Yes. If card already authenticated, SetupIntent returns immediate success.
### Q: What about trial subscriptions?
**A**: Setup Intent works with trials. Card authenticated but not charged until trial ends.
### Q: How do I test in production?
**A**: Start with internal testing, then A/B test with small percentage of users.
---
## 🚀 Next Steps
1. **Test the endpoint**:
```bash
node tests/test_setup_intent.js
```
2. **Review frontend examples**:
- See [FRONTEND_3DS_IMPLEMENTATION.md](docs/FRONTEND_3DS_IMPLEMENTATION.md)
- Copy TypeScript code into your frontend
3. **Implement in stages**:
- Start with new subscription flows
- Gradually migrate existing flows
- Monitor success rates
4. **Generate API documentation**:
```bash
npm run docs
# View at public/apidoc/
```
5. **Monitor production**:
- Track completion rates
- Watch for 3DS failures
- Measure revenue impact
---
## ✅ Implementation Checklist
- [x] Backend endpoint created (`setupCardAuthentication_post`)
- [x] Route configured (`POST /api/subscription/setupCard`)
- [x] Function exported in module.exports
- [x] Full JSDoc documentation for apidoc
- [x] Error handling (card errors, invalid requests)
- [x] Debug logging throughout
- [x] Frontend guide (FRONTEND_3DS_IMPLEMENTATION.md)
- [x] Payment failure docs updated (PAYMENT_FAILURE_HANDLING.md)
- [x] Test script created (tests/test_setup_intent.js)
- [x] Implementation summary (this document)
- [ ] Frontend implementation (pending)
- [ ] End-to-end testing (pending)
- [ ] Production deployment (pending)
- [ ] Metrics monitoring (pending)
---
**Status**: **Backend Implementation Complete**
**Next**: Frontend Integration