EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
stripeService.js
Go to the documentation of this file.
1/**
2 * @file services/stripeService.js
3 * @module services/stripeService
4 * @description Stripe Connect service for multi-tenant payment processing. Manages Connected
5 * Account onboarding, payment routing, subscription creation, and payment method management.
6 *
7 * **Architecture:**
8 * - Stripe Connect Platform Model (one platform account, multiple connected tenant accounts)
9 * - OAuth-based account onboarding (standard or express accounts)
10 * - Payment routing via stripeAccount parameter
11 * - Application fee collection (2-5% typical platform fee)
12 * - No stored tenant API keys (only stripe_account_id)
13 *
14 * **Core Functionality:**
15 * - Connected Account creation and onboarding
16 * - Account status monitoring (charges_enabled, payouts_enabled)
17 * - Payment Intent creation with automatic routing
18 * - Subscription management (create, update, cancel)
19 * - Customer creation (platform or connected account level)
20 * - Payment method attachment and management
21 * - Webhook signature verification (handled in routes/stripe-webhook.js)
22 *
23 * **Stripe Connect Flow:**
24 * 1. Create Connected Account (createConnectAccount)
25 * 2. Generate onboarding link (createAccountLink)
26 * 3. Tenant completes onboarding via Stripe-hosted flow
27 * 4. Webhook: account.updated (charges_enabled = true)
28 * 5. Store stripe_account_id in stripe_config table
29 * 6. Route payments via stripeAccount parameter
30 * 7. Collect application fees automatically
31 *
32 * **Payment Routing:**
33 * - Direct charges: Use stripeAccount parameter in API calls
34 * - Destination charges: Create on platform, transfer to connected account
35 * - Separate charges: Create directly on connected account (our approach)
36 *
37 * **Fees:**
38 * - Application fee: Set per transaction (percentage or fixed)
39 * - Stripe fee: ~2.9% + $0.30 per transaction (standard rates)
40 * - Net to tenant: Amount - application_fee - stripe_fee
41 *
42 * **Multi-Tenant Isolation:**
43 * - Each tenant has unique stripe_account_id
44 * - All charges route to correct connected account
45 * - Tenants see only their own transactions in Stripe Dashboard
46 * - Platform sees all transactions across all tenants
47 *
48 * **Security:**
49 * - Platform secret key stored in environment variable
50 * - No tenant API keys stored (Stripe Connect handles auth)
51 * - Webhook signatures verified (see routes/stripe-webhook.js)
52 * - PCI compliance handled by Stripe (no card data in database)
53 *
54 * **Related Modules:**
55 * - routes/stripe-webhook.js - Webhook event processing
56 * - routes/invoices.js - Invoice integration
57 * - routes/contracts.js - Recurring billing integration
58 * - services/db - PostgreSQL connection pool
59 *
60 * @requires stripe
61 * @see {@link https://stripe.com/docs/connect|Stripe Connect Documentation}
62 * @see {@link https://stripe.com/docs/connect/charges|Stripe Connect Charges}
63 * @see {@link https://stripe.com/docs/connect/enable-payment-acceptance-guide|Onboarding Guide}
64 */
65
66// Initialize Stripe with correct key based on mode
67const stripeMode = process.env.STRIPE_MODE || 'test';
68const stripeSecretKey = stripeMode === 'live'
69 ? process.env.STRIPE_LIVE_SECRET_KEY
70 : process.env.STRIPE_TEST_SECRET_KEY;
71const stripe = require('stripe')(stripeSecretKey);
72const pool = require('./db');
73
74console.log(`[Stripe] Service initialized in ${stripeMode.toUpperCase()} mode`);
75
76/**
77 * Stripe service class for Connect platform operations.
78 *
79 * Provides methods for Connected Account management, payment processing, subscription
80 * creation, and customer management. All operations support multi-tenant isolation
81 * via Connected Account routing.
82 *
83 * @class StripeService
84 *
85 * @example
86 * const stripeService = require('./services/stripeService');
87 *
88 * // Create Connected Account for tenant
89 * const account = await stripeService.createConnectAccount('tenant@example.com', 'US');
90 *
91 * // Generate onboarding link
92 * const link = await stripeService.createAccountLink(account.id);
93 *
94 * // Create payment intent routed to tenant
95 * const payment = await stripeService.createPaymentIntent(5000, 'usd', account.id);
96 */
97class StripeService {
98
99 /**
100 * Creates a new Stripe Connected Account for a tenant.
101 *
102 * Creates either Standard or Express account type. Standard accounts give full
103 * control to the tenant (own Stripe Dashboard). Express accounts are faster to
104 * onboard but platform-controlled. Returns account ID to store in stripe_config table.
105 *
106 * **Account Types:**
107 * - **Standard**: Full Stripe Dashboard, tenant controls settings, longer onboarding
108 * - **Express**: Simplified onboarding, platform controls settings, recommended
109 * - **Custom**: Full platform control (not recommended for this use case)
110 *
111 * @async
112 * @static
113 * @function createConnectAccount
114 * @param {string} email - Tenant business email address
115 * @param {string} [country='US'] - ISO 3166-1 alpha-2 country code (US, CA, GB, etc.)
116 * @param {string} [accountType='express'] - Account type (standard, express, custom)
117 * @param {Object} [businessProfile] - Optional business profile details
118 * @param {string} [businessProfile.name] - Business name
119 * @param {string} [businessProfile.url] - Business website URL
120 * @returns {Promise<Object>} Stripe Account object with id, charges_enabled, payouts_enabled
121 * @throws {Error} If account creation fails or invalid country code
122 *
123 * @example
124 * // Create Express account (recommended)
125 * const account = await stripeService.createConnectAccount(
126 * 'tenant@example.com',
127 * 'US',
128 * 'express',
129 * {
130 * name: 'Acme Corp',
131 * url: 'https://acme.example.com'
132 * }
133 * );
134 *
135 * console.log(account.id); // acct_1234567890
136 * console.log(account.charges_enabled); // false (not yet onboarded)
137 *
138 * @example
139 * // Create Standard account (full control to tenant)
140 * const account = await stripeService.createConnectAccount(
141 * 'tenant@example.com',
142 * 'GB',
143 * 'standard'
144 * );
145 */
146 static async createConnectAccount(email, country = 'US', accountType = 'express', businessProfile = {}) {
147 try {
148 const accountData = {
149 type: accountType,
150 country: country,
151 email: email,
152 capabilities: {
153 card_payments: { requested: true },
154 transfers: { requested: true },
155 },
156 };
157
158 // Add business profile if provided
159 if (businessProfile.name || businessProfile.url) {
160 accountData.business_profile = {};
161 if (businessProfile.name) accountData.business_profile.name = businessProfile.name;
162 if (businessProfile.url) accountData.business_profile.url = businessProfile.url;
163 }
164
165 const account = await stripe.accounts.create(accountData);
166
167 console.log(`✅ Created ${accountType} Connected Account: ${account.id} (${email})`);
168
169 return account;
170 } catch (error) {
171 console.error('❌ Failed to create Connected Account:', error.message);
172
173 // Provide helpful error for common issues
174 if (error.message && error.message.includes('signed up for Connect')) {
175 const enhancedError = new Error(
176 'Stripe Connect is not enabled on your account. ' +
177 'Please enable it at https://dashboard.stripe.com/connect before creating connected accounts.'
178 );
179 enhancedError.code = 'STRIPE_CONNECT_NOT_ENABLED';
180 enhancedError.setupUrl = 'https://dashboard.stripe.com/connect';
181 throw enhancedError;
182 }
183
184 throw new Error(`Stripe account creation failed: ${error.message}`);
185 }
186 }
187
188 /**
189 * Creates account link for onboarding flow.
190 *
191 * Generates time-limited URL for tenant to complete Stripe Connect onboarding.
192 * Link expires after expiration time (default 5 minutes). Tenant fills out business
193 * details, banking info, and identity verification. After completion, redirects to
194 * refresh_url or return_url.
195 *
196 * **Flow:**
197 * 1. Generate account link
198 * 2. Redirect tenant to link.url
199 * 3. Tenant completes onboarding on Stripe
200 * 4. Stripe redirects to return_url on success
201 * 5. Webhook: account.updated (charges_enabled = true)
202 *
203 * **Link Expiration:**
204 * - Default expiration: 5 minutes (Stripe enforced)
205 * - Must generate new link if expired
206 * - Store link.expires_at to check validity
207 *
208 * @async
209 * @static
210 * @function createAccountLink
211 * @param {string} accountId - Stripe Connected Account ID (acct_...)
212 * @param {string} [returnUrl] - URL to redirect after successful onboarding
213 * @param {string} [refreshUrl] - URL to redirect if link expires (should regenerate link)
214 * @param {string} [type='account_onboarding'] - Link type (account_onboarding or account_update)
215 * @returns {Promise<Object>} AccountLink object with url and expires_at timestamp
216 * @throws {Error} If link creation fails or account not found
217 *
218 * @example
219 * const link = await stripeService.createAccountLink(
220 * 'acct_1234567890',
221 * 'https://app.example.com/stripe/onboarding-complete',
222 * 'https://app.example.com/stripe/onboarding-refresh'
223 * );
224 *
225 * console.log(link.url); // https://connect.stripe.com/setup/e/acct_1234567890/...
226 * console.log(link.expires_at); // 1234567890 (Unix timestamp)
227 *
228 * // Redirect user to link.url
229 * res.redirect(link.url);
230 *
231 * @example
232 * // Generate link for updating existing account info
233 * const updateLink = await stripeService.createAccountLink(
234 * 'acct_1234567890',
235 * 'https://app.example.com/settings',
236 * 'https://app.example.com/settings',
237 * 'account_update'
238 * );
239 */
240 static async createAccountLink(accountId, returnUrl = null, refreshUrl = null, type = 'account_onboarding') {
241 try {
242 const backendUrl = process.env.BACKEND_URL || 'https://rmm-psa-backend-t9f7k.ondigitalocean.app';
243
244 // Use provided URLs or defaults
245 const return_url = returnUrl || `${backendUrl}/api/stripe/onboarding-complete`;
246 const refresh_url = refreshUrl || `${backendUrl}/api/stripe/onboarding-refresh`;
247
248 const accountLink = await stripe.accountLinks.create({
249 account: accountId,
250 refresh_url: refresh_url,
251 return_url: return_url,
252 type: type,
253 });
254
255 console.log(`✅ Created account link for ${accountId} (expires at ${new Date(accountLink.expires_at * 1000).toISOString()})`);
256
257 return accountLink;
258 } catch (error) {
259 console.error('❌ Failed to create account link:', error.message);
260 throw new Error(`Account link creation failed: ${error.message}`);
261 }
262 }
263
264 /**
265 * Retrieves Connected Account details.
266 *
267 * Fetches current account status including charges_enabled, payouts_enabled, and
268 * verification requirements. Use to monitor account status and determine if tenant
269 * can accept payments. Check requirements.currently_due for missing information.
270 *
271 * **Key Status Fields:**
272 * - **charges_enabled**: Can accept payments (true after onboarding)
273 * - **payouts_enabled**: Can receive payouts (true after bank verification)
274 * - **details_submitted**: Onboarding form completed
275 * - **requirements.currently_due**: Array of missing required fields
276 * - **requirements.eventually_due**: Fields needed in future
277 * - **requirements.past_due**: Overdue requirements (may disable account)
278 *
279 * @async
280 * @static
281 * @function retrieveAccount
282 * @param {string} accountId - Stripe Connected Account ID (acct_...)
283 * @returns {Promise<Object>} Stripe Account object with full details
284 * @throws {Error} If account retrieval fails or account not found
285 *
286 * @example
287 * const account = await stripeService.retrieveAccount('acct_1234567890');
288 *
289 * console.log(account.charges_enabled); // true or false
290 * console.log(account.payouts_enabled); // true or false
291 * console.log(account.details_submitted); // true or false
292 * console.log(account.requirements.currently_due); // ['business_profile.mcc', ...]
293 * console.log(account.requirements.errors); // [{ reason: 'verification_failed', ... }]
294 *
295 * if (account.charges_enabled) {
296 * console.log('✅ Account can accept payments');
297 * } else {
298 * console.log('⏳ Account onboarding incomplete');
299 * console.log('Missing:', account.requirements.currently_due.join(', '));
300 * }
301 */
302 static async retrieveAccount(accountId) {
303 try {
304 const account = await stripe.accounts.retrieve(accountId);
305
306 console.log(`📊 Retrieved account ${accountId}: charges=${account.charges_enabled}, payouts=${account.payouts_enabled}`);
307
308 return account;
309 } catch (error) {
310 console.error('❌ Failed to retrieve account:', error.message);
311 throw new Error(`Account retrieval failed: ${error.message}`);
312 }
313 }
314
315 /**
316 * Creates Payment Intent routed to Connected Account.
317 *
318 * Creates payment with automatic routing to tenant's Connected Account. Platform
319 * can collect application fee. Payment is held by Connected Account, not platform.
320 * Use for one-time payments (invoices, purchases). For recurring payments, use
321 * createSubscription instead.
322 *
323 * **Payment Flow:**
324 * 1. Create Payment Intent with stripeAccount (routes to tenant)
325 * 2. Return client_secret to frontend
326 * 3. Frontend confirms payment with Stripe.js
327 * 4. Webhook: payment_intent.succeeded
328 * 5. Update invoice payment_status = 'paid'
329 *
330 * **Application Fees:**
331 * - Fixed amount: application_fee_amount (cents)
332 * - Percentage: Calculate before calling (e.g., amount * 0.025 for 2.5%)
333 * - Fee goes to platform account
334 * - Net to tenant: amount - application_fee_amount
335 *
336 * @async
337 * @static
338 * @function createPaymentIntent
339 * @param {number} amount - Payment amount in cents (e.g., 5000 = $50.00)
340 * @param {string} [currency='usd'] - Currency code (usd, eur, gbp, cad, aud, etc.)
341 * @param {string} stripeAccountId - Connected Account ID to route payment to
342 * @param {Object} [options={}] - Additional PaymentIntent options
343 * @param {number} [options.application_fee_amount] - Platform fee in cents
344 * @param {string} [options.customer] - Stripe Customer ID
345 * @param {string} [options.payment_method] - Payment Method ID (for auto-confirmation)
346 * @param {Object} [options.metadata] - Custom metadata (invoice_id, tenant_id, etc.)
347 * @param {string} [options.description] - Payment description shown to customer
348 * @param {string} [options.receipt_email] - Email to send receipt to
349 * @returns {Promise<Object>} PaymentIntent object with id, client_secret, status
350 * @throws {Error} If Payment Intent creation fails
351 *
352 * @example
353 * // Basic payment intent with 2.5% application fee
354 * const amount = 5000; // $50.00
355 * const appFee = Math.round(amount * 0.025); // $1.25 (2.5%)
356 *
357 * const paymentIntent = await stripeService.createPaymentIntent(
358 * amount,
359 * 'usd',
360 * 'acct_1234567890',
361 * {
362 * application_fee_amount: appFee,
363 * metadata: {
364 * invoice_id: '456',
365 * tenant_id: '789'
366 * },
367 * description: 'Invoice #INV-000456',
368 * receipt_email: 'customer@example.com'
369 * }
370 * );
371 *
372 * // Return client_secret to frontend for confirmation
373 * res.json({ client_secret: paymentIntent.client_secret });
374 *
375 * @example
376 * // Payment intent with stored payment method (auto-confirm)
377 * const paymentIntent = await stripeService.createPaymentIntent(
378 * 10000, // $100.00
379 * 'usd',
380 * 'acct_1234567890',
381 * {
382 * customer: 'cus_1234567890',
383 * payment_method: 'pm_1234567890',
384 * confirm: true, // Auto-confirm with saved payment method
385 * off_session: true // For recurring charges
386 * }
387 * );
388 */
389 static async createPaymentIntent(amount, currency = 'usd', stripeAccountId, options = {}) {
390 try {
391 const paymentIntentData = {
392 amount: amount,
393 currency: currency,
394 ...options, // Merge in customer, payment_method, metadata, etc.
395 };
396
397 // Create PaymentIntent on connected account
398 const paymentIntent = await stripe.paymentIntents.create(
399 paymentIntentData,
400 {
401 stripeAccount: stripeAccountId, // Route to Connected Account
402 }
403 );
404
405 console.log(`💳 Created Payment Intent ${paymentIntent.id} for ${amount} ${currency} (account: ${stripeAccountId})`);
406 if (options.application_fee_amount) {
407 console.log(` Platform fee: ${options.application_fee_amount} cents`);
408 }
409
410 return paymentIntent;
411 } catch (error) {
412 console.error('❌ Failed to create Payment Intent:', error.message);
413 throw new Error(`Payment Intent creation failed: ${error.message}`);
414 }
415 }
416
417 /**
418 * Creates Stripe Customer on Connected Account.
419 *
420 * Creates customer record for storing payment methods and subscription management.
421 * Customer created on Connected Account (tenant's account), not platform. Use for
422 * recurring billing, saved payment methods, and subscription management.
423 *
424 * **Customer vs PaymentIntent:**
425 * - Create Customer for: Subscriptions, saved cards, recurring billing
426 * - Use PaymentIntent alone for: One-time guest checkout
427 *
428 * @async
429 * @static
430 * @function createCustomer
431 * @param {string} email - Customer email address
432 * @param {string} stripeAccountId - Connected Account ID
433 * @param {Object} [options={}] - Additional customer options
434 * @param {string} [options.name] - Customer full name
435 * @param {string} [options.phone] - Customer phone number
436 * @param {Object} [options.address] - Customer address object
437 * @param {Object} [options.metadata] - Custom metadata (customer_id from database, etc.)
438 * @param {string} [options.description] - Customer description
439 * @returns {Promise<Object>} Stripe Customer object with id, email, created
440 * @throws {Error} If customer creation fails
441 *
442 * @example
443 * const customer = await stripeService.createCustomer(
444 * 'customer@example.com',
445 * 'acct_1234567890',
446 * {
447 * name: 'John Doe',
448 * phone: '+1 555-123-4567',
449 * metadata: {
450 * customer_id: '456', // Link to customers table
451 * tenant_id: '789'
452 * },
453 * address: {
454 * line1: '123 Main St',
455 * city: 'San Francisco',
456 * state: 'CA',
457 * postal_code: '94111',
458 * country: 'US'
459 * }
460 * }
461 * );
462 *
463 * console.log(customer.id); // cus_1234567890
464 *
465 * // Store customer.id in database for future charges
466 * await pool.query(
467 * 'UPDATE customers SET stripe_customer_id = $1 WHERE customer_id = $2',
468 * [customer.id, 456]
469 * );
470 */
471 static async createCustomer(email, stripeAccountId, options = {}) {
472 try {
473 const customerData = {
474 email: email,
475 ...options, // Merge in name, phone, address, metadata, etc.
476 };
477
478 const customer = await stripe.customers.create(
479 customerData,
480 {
481 stripeAccount: stripeAccountId, // Create on Connected Account
482 }
483 );
484
485 console.log(`👤 Created Customer ${customer.id} on account ${stripeAccountId}`);
486
487 return customer;
488 } catch (error) {
489 console.error('❌ Failed to create customer:', error.message);
490 throw new Error(`Customer creation failed: ${error.message}`);
491 }
492 }
493
494 /**
495 * Creates subscription for recurring billing.
496 *
497 * Creates subscription with specified price/plan. Charges automatically on billing
498 * cycle. Links to Stripe Price (must be created in Stripe Dashboard or via API first).
499 * Use for contracts with recurring billing (monthly, yearly, etc.).
500 *
501 * **Prerequisites:**
502 * - Customer must exist (createCustomer first)
503 * - Price must exist in Stripe (create via Dashboard or API)
504 * - Payment method attached to customer (or default_payment_method specified)
505 *
506 * **Billing Cycle:**
507 * - Subscription starts immediately (or at trial_end if trial specified)
508 * - Invoices generated automatically based on interval (month, year, etc.)
509 * - Webhooks: customer.subscription.created, invoice.payment_succeeded
510 *
511 * @async
512 * @static
513 * @function createSubscription
514 * @param {string} customerId - Stripe Customer ID (cus_...)
515 * @param {string} priceId - Stripe Price ID (price_...)
516 * @param {string} stripeAccountId - Connected Account ID
517 * @param {Object} [options={}] - Additional subscription options
518 * @param {number} [options.trial_period_days] - Trial period in days (0 = no trial)
519 * @param {string} [options.default_payment_method] - Payment Method ID to use
520 * @param {number} [options.quantity=1] - Subscription quantity (for seat-based pricing)
521 * @param {number} [options.application_fee_percent] - Platform fee percentage (0-100)
522 * @param {Object} [options.metadata] - Custom metadata (contract_id, etc.)
523 * @returns {Promise<Object>} Stripe Subscription object with id, status, current_period_end
524 * @throws {Error} If subscription creation fails
525 *
526 * @example
527 * // Create monthly subscription with 14-day trial and 2.5% platform fee
528 * const subscription = await stripeService.createSubscription(
529 * 'cus_1234567890',
530 * 'price_1234567890', // $29/month price
531 * 'acct_1234567890',
532 * {
533 * trial_period_days: 14,
534 * quantity: 5, // 5 seats
535 * application_fee_percent: 2.5,
536 * metadata: {
537 * contract_id: '101',
538 * tenant_id: '789'
539 * }
540 * }
541 * );
542 *
543 * console.log(subscription.id); // sub_1234567890
544 * console.log(subscription.status); // 'trialing' or 'active'
545 * console.log(subscription.current_period_end); // Unix timestamp
546 *
547 * // Store subscription.id in stripe_subscriptions table
548 * await pool.query(
549 * `INSERT INTO stripe_subscriptions
550 * (stripe_subscription_id, customer_id, contract_id, status, amount)
551 * VALUES ($1, $2, $3, $4, $5)`,
552 * [subscription.id, customerId, 101, subscription.status, 2900]
553 * );
554 */
555 static async createSubscription(customerId, priceId, stripeAccountId, options = {}) {
556 try {
557 const subscriptionData = {
558 customer: customerId,
559 items: [{ price: priceId, quantity: options.quantity || 1 }],
560 ...options, // Merge in trial_period_days, default_payment_method, metadata, etc.
561 };
562
563 // Remove quantity from root options (already in items array)
564 delete subscriptionData.quantity;
565
566 const subscription = await stripe.subscriptions.create(
567 subscriptionData,
568 {
569 stripeAccount: stripeAccountId, // Create on Connected Account
570 }
571 );
572
573 console.log(`📅 Created Subscription ${subscription.id} (${subscription.status}) on account ${stripeAccountId}`);
574
575 return subscription;
576 } catch (error) {
577 console.error('❌ Failed to create subscription:', error.message);
578 throw new Error(`Subscription creation failed: ${error.message}`);
579 }
580 }
581
582 /**
583 * Attaches payment method to customer.
584 *
585 * Links payment method (card, bank account) to customer for future charges. Required
586 * for recurring billing and saved payment methods. Payment method must be created on
587 * frontend (Stripe.js) before attaching.
588 *
589 * **Frontend Flow:**
590 * 1. Frontend creates PaymentMethod with Stripe.js (card details never touch server)
591 * 2. Frontend sends payment_method_id to backend
592 * 3. Backend attaches payment method to customer
593 * 4. Optionally set as default payment method
594 *
595 * @async
596 * @static
597 * @function attachPaymentMethod
598 * @param {string} paymentMethodId - Payment Method ID (pm_...)
599 * @param {string} customerId - Stripe Customer ID (cus_...)
600 * @param {string} stripeAccountId - Connected Account ID
601 * @param {boolean} [setAsDefault=false] - Set as default payment method for customer
602 * @returns {Promise<Object>} PaymentMethod object with id, type, card details
603 * @throws {Error} If attachment fails or payment method already attached
604 *
605 * @example
606 * // Attach card and set as default
607 * const paymentMethod = await stripeService.attachPaymentMethod(
608 * 'pm_1234567890', // From frontend Stripe.js
609 * 'cus_1234567890',
610 * 'acct_1234567890',
611 * true // Set as default
612 * );
613 *
614 * console.log(paymentMethod.type); // 'card'
615 * console.log(paymentMethod.card.brand); // 'visa'
616 * console.log(paymentMethod.card.last4); // '4242'
617 * console.log(paymentMethod.card.exp_month); // 12
618 * console.log(paymentMethod.card.exp_year); // 2025
619 *
620 * // Store in database
621 * await pool.query(
622 * `INSERT INTO payment_methods
623 * (stripe_payment_method_id, customer_id, type, card_brand, card_last4, is_default)
624 * VALUES ($1, $2, $3, $4, $5, $6)`,
625 * [paymentMethod.id, customerId, 'card', paymentMethod.card.brand,
626 * paymentMethod.card.last4, true]
627 * );
628 */
629 static async attachPaymentMethod(paymentMethodId, customerId, stripeAccountId, setAsDefault = false) {
630 try {
631 // Attach payment method to customer
632 const paymentMethod = await stripe.paymentMethods.attach(
633 paymentMethodId,
634 { customer: customerId },
635 { stripeAccount: stripeAccountId }
636 );
637
638 console.log(`💳 Attached Payment Method ${paymentMethodId} to customer ${customerId}`);
639
640 // Optionally set as default payment method
641 if (setAsDefault) {
642 await stripe.customers.update(
643 customerId,
644 {
645 invoice_settings: {
646 default_payment_method: paymentMethodId,
647 },
648 },
649 { stripeAccount: stripeAccountId }
650 );
651 console.log(` Set as default payment method`);
652 }
653
654 return paymentMethod;
655 } catch (error) {
656 console.error('❌ Failed to attach payment method:', error.message);
657 throw new Error(`Payment method attachment failed: ${error.message}`);
658 }
659 }
660
661 /**
662 * Updates Connected Account configuration in database.
663 *
664 * Syncs Stripe account status to stripe_config table. Call after account.updated
665 * webhook or after retrieving account details. Updates charges_enabled, payouts_enabled,
666 * verification_status, and requirements.
667 *
668 * @async
669 * @static
670 * @function updateAccountConfig
671 * @param {number} tenantId - Tenant ID from tenants table
672 * @param {Object} account - Stripe Account object from retrieveAccount()
673 * @returns {Promise<void>} Resolves when database updated
674 * @throws {Error} If database update fails
675 *
676 * @example
677 * // After receiving account.updated webhook
678 * const account = await stripeService.retrieveAccount('acct_1234567890');
679 * await stripeService.updateAccountConfig(789, account);
680 *
681 * console.log('✅ Account config synced to database');
682 */
683 static async updateAccountConfig(tenantId, account) {
684 try {
685 // Determine account status
686 let accountStatus = 'pending';
687 if (!account.details_submitted) {
688 accountStatus = 'pending';
689 } else if (account.charges_enabled && account.payouts_enabled) {
690 accountStatus = 'verified';
691 } else if (account.requirements.disabled_reason) {
692 accountStatus = 'disabled';
693 }
694
695 // Determine verification status
696 let verificationStatus = 'unverified';
697 if (account.charges_enabled && account.payouts_enabled) {
698 verificationStatus = 'verified';
699 } else if (account.details_submitted && account.requirements.currently_due.length > 0) {
700 verificationStatus = 'pending';
701 } else if (account.requirements.disabled_reason) {
702 verificationStatus = 'failed';
703 }
704
705 await pool.query(
706 `UPDATE stripe_config SET
707 account_status = $1,
708 charges_enabled = $2,
709 payouts_enabled = $3,
710 details_submitted = $4,
711 verification_status = $5,
712 requirements_currently_due = $6,
713 requirements_eventually_due = $7,
714 requirements_disabled_reason = $8,
715 updated_at = NOW()
716 WHERE tenant_id = $9 AND stripe_account_id = $10`,
717 [
718 accountStatus,
719 account.charges_enabled,
720 account.payouts_enabled,
721 account.details_submitted,
722 verificationStatus,
723 account.requirements.currently_due || [],
724 account.requirements.eventually_due || [],
725 account.requirements.disabled_reason || null,
726 tenantId,
727 account.id,
728 ]
729 );
730
731 console.log(`✅ Updated account config for tenant ${tenantId} (status: ${accountStatus})`);
732 } catch (error) {
733 console.error('❌ Failed to update account config:', error.message);
734 throw error;
735 }
736 }
737}
738
739module.exports = StripeService;