agmission/Development/server/docs/SETUP_INTENT_IMPLEMENTATION.md

15 KiB
Raw Blame History

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

<EFBFBD> Multi-Subscription Handling

How Multiple Subscriptions Work

When calling POST /api/subscription/update with package + addons:

// 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

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:

// 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


<EFBFBD>📝 Implementation Details

1. Backend Changes

New Endpoint: POST /api/subscription/setupCard

File: controllers/subscription.js

Parameters:

{
  "custId": "cus_xxx",  // Stripe customer ID
  "pmId": "pm_xxx"      // Payment method ID to authenticate
}

Response (No 3DS):

{
  "requiresAction": false,
  "status": "succeeded",
  "setupIntentId": "seti_xxx",
  "message": "Card authenticated successfully"
}

Response (3DS Required):

{
  "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

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

Added setupCardAuthentication_post to module.exports list.


2. Documentation Updates

FRONTEND_3DS_IMPLEMENTATION.md

Location: 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

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

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:

node test_setup_intent.js

🎨 Frontend Integration Guide

Step 1: Authenticate Card

// 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

// 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 for full implementation.


🧪 Testing

Manual Testing

# 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


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:

    node tests/test_setup_intent.js
    
  2. Review frontend examples:

  3. Implement in stages:

    • Start with new subscription flows
    • Gradually migrate existing flows
    • Monitor success rates
  4. Generate API documentation:

    npm run docs
    # View at public/apidoc/
    
  5. Monitor production:

    • Track completion rates
    • Watch for 3DS failures
    • Measure revenue impact

Implementation Checklist

  • Backend endpoint created (setupCardAuthentication_post)
  • Route configured (POST /api/subscription/setupCard)
  • Function exported in module.exports
  • Full JSDoc documentation for apidoc
  • Error handling (card errors, invalid requests)
  • Debug logging throughout
  • Frontend guide (FRONTEND_3DS_IMPLEMENTATION.md)
  • Payment failure docs updated (PAYMENT_FAILURE_HANDLING.md)
  • Test script created (tests/test_setup_intent.js)
  • 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