-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsetupIntent.js
More file actions
164 lines (156 loc) · 6.32 KB
/
setupIntent.js
File metadata and controls
164 lines (156 loc) · 6.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/**
* @fileoverview Stripe Setup Intent Controller
*
* This controller handles creation of Stripe setup intents for payment method
* collection. Setup intents allow users to securely add and save payment methods
* (credit cards) without immediately charging them. The controller automatically
* creates Stripe customers for users who don't have one yet.
*
* KEY FEATURES
*
* AUTOMATIC CUSTOMER CREATION
* - Detects MongoDB ObjectId vs Stripe customer ID
* - Creates Stripe customer if user doesn't have one
* - Updates user record with new customer ID
* - Uses Mongoose markModified() for nested object updates
*
* IDEMPOTENCY SUPPORT
* - Supports idempotency keys to prevent duplicate requests
* - Ensures setup intent creation is idempotent
*
* ENVIRONMENT SUPPORT
* - Uses Stripe test keys in development
* - Uses Stripe live keys in production
* - Automatic key selection based on NODE_ENV
*
* BUSINESS LOGIC
*
* ID DETECTION
* - MongoDB ObjectId: 24 hex characters (e.g., '507f1f77bcf86cd799439011')
* - Stripe Customer ID: Starts with 'cus_' (e.g., 'cus_1a2b3c4d5e6f')
* - If ObjectId: Looks up user, creates customer if needed
* - If Stripe ID: Uses directly
*
* CUSTOMER CREATION
* - Creates Stripe customer with user email and name
* - Stores MongoDB user ID in customer metadata
* - Updates user.payment.customer_id in database
* - Uses markModified() to ensure Mongoose saves nested object
*
* SETUP INTENT CREATION
* - Creates setup intent for payment method collection
* - Sets usage to 'on_session' for future payments
* - Associates with Stripe customer
* - Returns client secret for frontend integration
*
* DEPENDENCIES
* - stripe: Stripe SDK for payment processing
* - models/User: User model for database operations
*
* @module controller/payments/setupIntent
* @requires stripe
* @requires ../../models/User
* @requires ../../services/logger
*/
const STRIPE_SK =
process.env.NODE_ENV === 'production'
? process.env.STRIPE_SK_LIVE
: process.env.STRIPE_SK_TEST,
stripe = require('stripe')(STRIPE_SK);
const { User } = require('../../models');
const { requireLogger } = require('../../services/logger');
const logger = requireLogger(__filename);
module.exports = {
/**
* Creates a Stripe setup intent for payment method collection
*
* This endpoint handles the creation of setup intents for collecting payment methods.
* It can accept either a MongoDB ObjectId (user ID) or a Stripe customer ID.
* If a user ID is provided and the user doesn't have a Stripe customer ID,
* it will create one automatically and update the user record.
*
* @async
* @function setupIntent
* @param {Object} req - Express request object
* @param {string} req.params.id - Either MongoDB ObjectId (24 hex chars) or Stripe customer ID
* @param {Object} req.body - Request body
* @param {string} req.body.idempotencyKey - Stripe idempotency key for request deduplication
* @param {Object} res - Express response object
*
* @returns {Promise<void>} Sends JSON response with customer ID and client secret
*
* @throws {404} When user ID is provided but user not found in database
* @throws {500} When Stripe API call fails or database operation fails
*
* @example
* // Request with user ID (MongoDB ObjectId)
* POST /api/payments/setupIntent/507f1f77bcf86cd799439011
* Body: { "idempotencyKey": "unique-key-123" }
*
* // Request with existing Stripe customer ID
* POST /api/payments/setupIntent/cus_1a2b3c4d5e6f
* Body: { "idempotencyKey": "unique-key-456" }
*
* @example
* // Response format
* {
* "customer": "cus_1a2b3c4d5e6f",
* "clientSecret": "seti_1a2b3c4d5e6f_secret_xyz"
* }
*/
setupIntent: async (req, res) => {
try {
let customerId = req.params.id;
// Detect if the provided ID is a MongoDB ObjectId (24 hex characters)
// This allows the endpoint to accept both user IDs and existing Stripe customer IDs
if (/^[0-9a-fA-F]{24}$/.test(customerId)) {
// Look up the user in the database using the MongoDB ObjectId
const user = await User.findById(customerId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Check if the user already has a Stripe customer ID
if (!user.payment?.customer_id) {
// Create a new Stripe customer for the user
const customer = await stripe.customers.create({
email: user.email,
name: `${user.firstName} ${user.lastName}`,
metadata: {
userId: customerId, // Store the MongoDB user ID for reference
},
});
// Update the user record with the new Stripe customer ID
user.payment = user.payment || {}; // Ensure payment object exists
user.payment.customer_id = customer.id;
user.markModified('payment'); // Tell Mongoose that the payment object has changed
await user.save();
// Use the newly created customer ID for the setup intent
customerId = customer.id;
} else {
// User already has a customer ID, use the existing one
customerId = user.payment.customer_id;
}
}
// If the ID is not a MongoDB ObjectId, assume it's already a Stripe customer ID
// and use it directly for the setup intent
// Create the Stripe setup intent for payment method collection
const intent = await stripe.setupIntents.create(
{
usage: 'on_session', // Payment method can be used for future payments
customer: customerId, // Associate with the Stripe customer
payment_method_types: ['card'], // Only accept card payments
},
{ idempotencyKey: req.body.idempotencyKey } // Prevent duplicate requests
);
// Return the customer ID and client secret for frontend integration
res.send({
customer: customerId,
clientSecret: intent.client_secret,
});
} catch (error) {
// Log the error for debugging and return a generic error message
logger.error('Setup intent error:', error.message);
res.status(500).json({ error: 'Failed to create setup intent' });
}
},
};