EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
stripe-connect.js
Go to the documentation of this file.
1/**
2 * @file routes/stripe-connect.js
3 * @module routes/stripe-connect
4 * @description Stripe Connect onboarding and account management routes for tenant dashboard.
5 * Handles OAuth onboarding flow, account status retrieval, and configuration updates.
6 *
7 * **Dashboard Integration:**
8 * - GET /config - Fetch tenant's Stripe account status
9 * - POST /connect/start - Initiate Stripe Connect onboarding
10 * - GET /connect/refresh - Regenerate onboarding link (if expired)
11 * - GET /onboarding-complete - Return URL after successful onboarding
12 * - GET /onboarding-refresh - Return URL when link expires
13 *
14 * **Multi-Tenant Security:**
15 * - All routes require authentication (authenticateToken)
16 * - tenant_id extracted from JWT token
17 * - Row-level security enforced on stripe_config table
18 *
19 * **Onboarding Flow:**
20 * 1. Tenant clicks "Connect with Stripe" in dashboard
21 * 2. POST /connect/start creates Connected Account + account link
22 * 3. Redirect tenant to Stripe-hosted onboarding
23 * 4. Stripe redirects to /onboarding-complete or /onboarding-refresh
24 * 5. Dashboard polls GET /config to check charges_enabled status
25 *
26 * @requires express
27 * @requires ../services/stripeService
28 * @requires ../middleware/auth
29 * @see {@link https://stripe.com/docs/connect/enable-payment-acceptance-guide|Stripe Connect Onboarding}
30 */
31
32const express = require('express');
33const router = express.Router();
34const pool = require('../services/db');
35const StripeService = require('../services/stripeService');
36const authenticateToken = require('../middleware/auth');
37
38/**
39 * GET /api/stripe/config
40 *
41 * Retrieves tenant's Stripe Connect account configuration and status.
42 *
43 * Returns account details if configured, or null if not yet onboarded.
44 * Dashboard uses this to determine whether to show "Connect with Stripe" button
45 * or display account status (verified, pending, disabled).
46 *
47 * **Response Fields:**
48 * - stripe_account_id - Connected Account ID (acct_...)
49 * - account_status - pending, verified, disabled, suspended
50 * - charges_enabled - Can accept payments (true/false)
51 * - payouts_enabled - Can receive payouts (true/false)
52 * - verification_status - unverified, pending, verified, failed
53 * - requirements_currently_due - Array of missing required info
54 * - onboarding_completed_at - Timestamp of onboarding completion
55 *
56 * @route GET /api/stripe/config
57 * @param {express.Request} req - Express request (authenticated)
58 * @param {express.Response} res - Express response
59 * @returns {Promise<Object>} Stripe config or null if not configured
60 *
61 * @example
62 * // Response when not onboarded:
63 * {
64 * "configured": false
65 * }
66 *
67 * @example
68 * // Response when onboarded and verified:
69 * {
70 * "configured": true,
71 * "stripe_account_id": "acct_1234567890",
72 * "account_status": "verified",
73 * "charges_enabled": true,
74 * "payouts_enabled": true,
75 * "verification_status": "verified",
76 * "requirements_currently_due": [],
77 * "onboarding_completed_at": "2026-03-16T10:30:00.000Z",
78 * "country": "US",
79 * "default_currency": "usd"
80 * }
81 */
82router.get('/config', authenticateToken, async (req, res) => {
83 try {
84 const tenantId = req.user.tenant_id;
85
86 const result = await pool.query(
87 `SELECT
88 config_id,
89 stripe_account_id,
90 account_status,
91 charges_enabled,
92 payouts_enabled,
93 details_submitted,
94 verification_status,
95 requirements_currently_due,
96 requirements_eventually_due,
97 requirements_disabled_reason,
98 account_type,
99 country,
100 default_currency,
101 onboarding_completed_at,
102 application_fee_percent,
103 application_fee_fixed,
104 created_at,
105 updated_at
106 FROM stripe_config
107 WHERE tenant_id = $1`,
108 [tenantId]
109 );
110
111 if (result.rows.length === 0) {
112 return res.json({ configured: false });
113 }
114
115 res.json({
116 configured: true,
117 ...result.rows[0]
118 });
119 } catch (error) {
120 console.error('❌ Error fetching Stripe config:', error);
121 res.status(500).json({ error: 'Failed to fetch Stripe configuration' });
122 }
123});
124
125/**
126 * POST /api/stripe/connect/start
127 *
128 * Initiates Stripe Connect onboarding flow for tenant.
129 *
130 * Creates Connected Account (if not exists) and generates onboarding link.
131 * Returns redirect URL for tenant to complete onboarding on Stripe.
132 *
133 * **Request Body:**
134 * - email - Business email address (required)
135 * - country - ISO 3166-1 alpha-2 country code (default: US)
136 * - business_name - Business name (optional)
137 * - business_url - Business website URL (optional)
138 *
139 * **Response:**
140 * - account_id - Stripe Connected Account ID
141 * - onboarding_url - URL to redirect tenant to
142 * - expires_at - Unix timestamp when link expires (5 minutes)
143 *
144 * @route POST /api/stripe/connect/start
145 * @param {express.Request} req - Express request with body
146 * @param {express.Response} res - Express response with onboarding URL
147 * @returns {Promise<Object>} Account ID and onboarding URL
148 *
149 * @example
150 * // Request:
151 * POST /api/stripe/connect/start
152 * {
153 * "email": "billing@acme.com",
154 * "country": "US",
155 * "business_name": "Acme Corp",
156 * "business_url": "https://acme.example.com"
157 * }
158 *
159 * @example
160 * // Response:
161 * {
162 * "account_id": "acct_1234567890",
163 * "onboarding_url": "https://connect.stripe.com/setup/e/acct_1234567890/...",
164 * "expires_at": 1710630000
165 * }
166 */
167router.post('/connect/start', authenticateToken, async (req, res) => {
168 try {
169 const tenantId = req.user.tenant_id;
170 const { email, country = 'US', business_name, business_url } = req.body;
171
172 if (!email) {
173 return res.status(400).json({ error: 'Email is required' });
174 }
175
176 // Check if tenant already has a Stripe account
177 const existingConfig = await pool.query(
178 'SELECT stripe_account_id FROM stripe_config WHERE tenant_id = $1',
179 [tenantId]
180 );
181
182 let accountId;
183
184 if (existingConfig.rows.length > 0) {
185 // Use existing account
186 accountId = existingConfig.rows[0].stripe_account_id;
187 console.log(`🔄 Using existing Stripe account: ${accountId}`);
188 } else {
189 // Create new Connected Account
190 const businessProfile = {};
191 if (business_name) businessProfile.name = business_name;
192 if (business_url) businessProfile.url = business_url;
193
194 const account = await StripeService.createConnectAccount(
195 email,
196 country,
197 'express', // Use Express accounts for faster onboarding
198 businessProfile
199 );
200
201 accountId = account.id;
202
203 // Store in database
204 await pool.query(
205 `INSERT INTO stripe_config
206 (tenant_id, stripe_account_id, account_status, account_type, country, created_by)
207 VALUES ($1, $2, 'pending', 'express', $3, $4)`,
208 [tenantId, accountId, country, req.user.user_id]
209 );
210
211 console.log(`✅ Created new Stripe account: ${accountId} for tenant ${tenantId}`);
212 }
213
214 // Generate onboarding link
215 const frontendUrl = process.env.FRONTEND_URL || 'https://rmm-psa-dashboard-5jyun.ondigitalocean.app';
216 const accountLink = await StripeService.createAccountLink(
217 accountId,
218 `${frontendUrl}/integrations?stripe_onboarding=success`,
219 `${frontendUrl}/integrations?stripe_onboarding=refresh`
220 );
221
222 // Update account_link_created_at and expires_at
223 await pool.query(
224 `UPDATE stripe_config
225 SET account_link_created_at = NOW(),
226 account_link_expires_at = to_timestamp($1),
227 updated_at = NOW()
228 WHERE stripe_account_id = $2`,
229 [accountLink.expires_at, accountId]
230 );
231
232 res.json({
233 account_id: accountId,
234 onboarding_url: accountLink.url,
235 expires_at: accountLink.expires_at
236 });
237 } catch (error) {
238 console.error('❌ Error starting Stripe Connect onboarding:', error);
239
240 // Check if this is the "Connect not enabled" error
241 if (error.message && error.message.includes('signed up for Connect')) {
242 return res.status(400).json({
243 error: 'Stripe Connect not enabled',
244 message: 'Stripe Connect must be enabled on your Stripe account before creating connected accounts.',
245 instructions: [
246 '1. Go to https://dashboard.stripe.com/connect',
247 '2. Click "Get started" to enable Stripe Connect',
248 '3. Complete the platform setup',
249 '4. Return here and try connecting again'
250 ],
251 setupUrl: 'https://dashboard.stripe.com/connect'
252 });
253 }
254
255 res.status(500).json({
256 error: 'Failed to start Stripe onboarding',
257 message: error.message
258 });
259 }
260});
261
262/**
263 * POST /api/stripe/connect/refresh
264 *
265 * Regenerates onboarding link if previous link expired.
266 *
267 * Stripe account links expire after ~5 minutes. If tenant didn't complete
268 * onboarding in time, dashboard calls this to generate a fresh link.
269 *
270 * @route POST /api/stripe/connect/refresh
271 * @param {express.Request} req - Express request
272 * @param {express.Response} res - Express response with new onboarding URL
273 * @returns {Promise<Object>} New onboarding URL and expiration
274 *
275 * @example
276 * // Response:
277 * {
278 * "onboarding_url": "https://connect.stripe.com/setup/e/acct_1234567890/...",
279 * "expires_at": 1710630300
280 * }
281 */
282router.post('/connect/refresh', authenticateToken, async (req, res) => {
283 try {
284 const tenantId = req.user.tenant_id;
285
286 // Get tenant's Stripe account ID
287 const configResult = await pool.query(
288 'SELECT stripe_account_id FROM stripe_config WHERE tenant_id = $1',
289 [tenantId]
290 );
291
292 if (configResult.rows.length === 0) {
293 return res.status(404).json({ error: 'Stripe account not found. Please start onboarding first.' });
294 }
295
296 const accountId = configResult.rows[0].stripe_account_id;
297
298 // Generate new onboarding link
299 const frontendUrl = process.env.FRONTEND_URL || 'https://rmm-psa-dashboard-5jyun.ondigitalocean.app';
300 const accountLink = await StripeService.createAccountLink(
301 accountId,
302 `${frontendUrl}/integrations?stripe_onboarding=success`,
303 `${frontendUrl}/integrations?stripe_onboarding=refresh`
304 );
305
306 // Update timestamps
307 await pool.query(
308 `UPDATE stripe_config
309 SET account_link_created_at = NOW(),
310 account_link_expires_at = to_timestamp($1),
311 updated_at = NOW()
312 WHERE stripe_account_id = $2`,
313 [accountLink.expires_at, accountId]
314 );
315
316 res.json({
317 onboarding_url: accountLink.url,
318 expires_at: accountLink.expires_at
319 });
320 } catch (error) {
321 console.error('❌ Error refreshing Stripe Connect link:', error);
322 res.status(500).json({
323 error: 'Failed to refresh onboarding link',
324 message: error.message
325 });
326 }
327});
328
329/**
330 * POST /api/stripe/connect/sync
331 *
332 * Syncs tenant's Stripe account status from Stripe API to database.
333 *
334 * Dashboard calls this after onboarding redirect to fetch latest account status.
335 * Also useful for manually refreshing verification status if requirements change.
336 *
337 * @route POST /api/stripe/connect/sync
338 * @param {express.Request} req - Express request
339 * @param {express.Response} res - Express response with updated account status
340 * @returns {Promise<Object>} Updated account configuration
341 *
342 * @example
343 * // Response:
344 * {
345 * "synced": true,
346 * "account_status": "verified",
347 * "charges_enabled": true,
348 * "payouts_enabled": true,
349 * "verification_status": "verified"
350 * }
351 */
352router.post('/connect/sync', authenticateToken, async (req, res) => {
353 try {
354 const tenantId = req.user.tenant_id;
355
356 // Get tenant's Stripe account ID
357 const configResult = await pool.query(
358 'SELECT stripe_account_id FROM stripe_config WHERE tenant_id = $1',
359 [tenantId]
360 );
361
362 if (configResult.rows.length === 0) {
363 return res.status(404).json({ error: 'Stripe account not found' });
364 }
365
366 const accountId = configResult.rows[0].stripe_account_id;
367
368 // Fetch latest account details from Stripe
369 const account = await StripeService.retrieveAccount(accountId);
370
371 // Update database with latest status
372 await StripeService.updateAccountConfig(tenantId, account);
373
374 // Check if onboarding was just completed
375 if (account.charges_enabled && !configResult.rows[0].onboarding_completed_at) {
376 await pool.query(
377 `UPDATE stripe_config
378 SET onboarding_completed_at = NOW()
379 WHERE tenant_id = $1`,
380 [tenantId]
381 );
382 }
383
384 // Fetch updated config from database
385 const updatedConfig = await pool.query(
386 `SELECT
387 account_status,
388 charges_enabled,
389 payouts_enabled,
390 verification_status,
391 requirements_currently_due,
392 onboarding_completed_at
393 FROM stripe_config
394 WHERE tenant_id = $1`,
395 [tenantId]
396 );
397
398 res.json({
399 synced: true,
400 ...updatedConfig.rows[0]
401 });
402 } catch (error) {
403 console.error('❌ Error syncing Stripe account status:', error);
404 res.status(500).json({
405 error: 'Failed to sync account status',
406 message: error.message
407 });
408 }
409});
410
411/**
412 * DELETE /api/stripe/connect
413 *
414 * Disconnects tenant from Stripe (removes Connected Account configuration).
415 *
416 * **Warning:** This does NOT delete the Stripe account itself (must be done in Stripe Dashboard).
417 * Only removes the connection from this platform.
418 *
419 * @route DELETE /api/stripe/connect
420 * @param {express.Request} req - Express request
421 * @param {express.Response} res - Express response
422 * @returns {Promise<Object>} Deletion confirmation
423 */
424router.delete('/connect', authenticateToken, async (req, res) => {
425 try {
426 const tenantId = req.user.tenant_id;
427
428 const result = await pool.query(
429 'DELETE FROM stripe_config WHERE tenant_id = $1 RETURNING stripe_account_id',
430 [tenantId]
431 );
432
433 if (result.rows.length === 0) {
434 return res.status(404).json({ error: 'Stripe account not found' });
435 }
436
437 console.log(`🗑️ Disconnected Stripe account ${result.rows[0].stripe_account_id} for tenant ${tenantId}`);
438
439 res.json({
440 disconnected: true,
441 message: 'Stripe account disconnected successfully'
442 });
443 } catch (error) {
444 console.error('❌ Error disconnecting Stripe account:', error);
445 res.status(500).json({ error: 'Failed to disconnect Stripe account' });
446 }
447});
448
449module.exports = router;