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.
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)
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)
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
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)
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
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
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)
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
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}
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');
74console.log(`[Stripe] Service initialized in ${stripeMode.toUpperCase()} mode`);
77 * Stripe service class for Connect platform operations.
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.
83 * @class StripeService
86 * const stripeService = require('./services/stripeService');
88 * // Create Connected Account for tenant
89 * const account = await stripeService.createConnectAccount('tenant@example.com', 'US');
91 * // Generate onboarding link
92 * const link = await stripeService.createAccountLink(account.id);
94 * // Create payment intent routed to tenant
95 * const payment = await stripeService.createPaymentIntent(5000, 'usd', account.id);
100 * Creates a new Stripe Connected Account for a tenant.
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.
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)
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
124 * // Create Express account (recommended)
125 * const account = await stripeService.createConnectAccount(
126 * 'tenant@example.com',
131 * url: 'https://acme.example.com'
135 * console.log(account.id); // acct_1234567890
136 * console.log(account.charges_enabled); // false (not yet onboarded)
139 * // Create Standard account (full control to tenant)
140 * const account = await stripeService.createConnectAccount(
141 * 'tenant@example.com',
146 static async createConnectAccount(email, country = 'US', accountType = 'express', businessProfile = {}) {
148 const accountData = {
153 card_payments: { requested: true },
154 transfers: { requested: true },
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;
165 const account = await stripe.accounts.create(accountData);
167 console.log(`✅ Created ${accountType} Connected Account: ${account.id} (${email})`);
171 console.error('❌ Failed to create Connected Account:', error.message);
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.'
179 enhancedError.code = 'STRIPE_CONNECT_NOT_ENABLED';
180 enhancedError.setupUrl = 'https://dashboard.stripe.com/connect';
184 throw new Error(`Stripe account creation failed: ${error.message}`);
189 * Creates account link for onboarding flow.
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.
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)
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
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
219 * const link = await stripeService.createAccountLink(
221 * 'https://app.example.com/stripe/onboarding-complete',
222 * 'https://app.example.com/stripe/onboarding-refresh'
225 * console.log(link.url); // https://connect.stripe.com/setup/e/acct_1234567890/...
226 * console.log(link.expires_at); // 1234567890 (Unix timestamp)
228 * // Redirect user to link.url
229 * res.redirect(link.url);
232 * // Generate link for updating existing account info
233 * const updateLink = await stripeService.createAccountLink(
235 * 'https://app.example.com/settings',
236 * 'https://app.example.com/settings',
240 static async createAccountLink(accountId, returnUrl = null, refreshUrl = null, type = 'account_onboarding') {
242 const backendUrl = process.env.BACKEND_URL || 'https://rmm-psa-backend-t9f7k.ondigitalocean.app';
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`;
248 const accountLink = await stripe.accountLinks.create({
250 refresh_url: refresh_url,
251 return_url: return_url,
255 console.log(`✅ Created account link for ${accountId} (expires at ${new Date(accountLink.expires_at * 1000).toISOString()})`);
259 console.error('❌ Failed to create account link:', error.message);
260 throw new Error(`Account link creation failed: ${error.message}`);
265 * Retrieves Connected Account details.
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.
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)
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
287 * const account = await stripeService.retrieveAccount('acct_1234567890');
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', ... }]
295 * if (account.charges_enabled) {
296 * console.log('✅ Account can accept payments');
298 * console.log('⏳ Account onboarding incomplete');
299 * console.log('Missing:', account.requirements.currently_due.join(', '));
302 static async retrieveAccount(accountId) {
304 const account = await stripe.accounts.retrieve(accountId);
306 console.log(`📊 Retrieved account ${accountId}: charges=${account.charges_enabled}, payouts=${account.payouts_enabled}`);
310 console.error('❌ Failed to retrieve account:', error.message);
311 throw new Error(`Account retrieval failed: ${error.message}`);
316 * Creates Payment Intent routed to Connected Account.
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.
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'
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
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
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%)
357 * const paymentIntent = await stripeService.createPaymentIntent(
362 * application_fee_amount: appFee,
367 * description: 'Invoice #INV-000456',
368 * receipt_email: 'customer@example.com'
372 * // Return client_secret to frontend for confirmation
373 * res.json({ client_secret: paymentIntent.client_secret });
376 * // Payment intent with stored payment method (auto-confirm)
377 * const paymentIntent = await stripeService.createPaymentIntent(
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
389 static async createPaymentIntent(amount, currency = 'usd', stripeAccountId, options = {}) {
391 const paymentIntentData = {
394 ...options, // Merge in customer, payment_method, metadata, etc.
397 // Create PaymentIntent on connected account
398 const paymentIntent = await stripe.paymentIntents.create(
401 stripeAccount: stripeAccountId, // Route to Connected Account
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`);
410 return paymentIntent;
412 console.error('❌ Failed to create Payment Intent:', error.message);
413 throw new Error(`Payment Intent creation failed: ${error.message}`);
418 * Creates Stripe Customer on Connected Account.
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.
424 * **Customer vs PaymentIntent:**
425 * - Create Customer for: Subscriptions, saved cards, recurring billing
426 * - Use PaymentIntent alone for: One-time guest checkout
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
443 * const customer = await stripeService.createCustomer(
444 * 'customer@example.com',
448 * phone: '+1 555-123-4567',
450 * customer_id: '456', // Link to customers table
454 * line1: '123 Main St',
455 * city: 'San Francisco',
457 * postal_code: '94111',
463 * console.log(customer.id); // cus_1234567890
465 * // Store customer.id in database for future charges
467 * 'UPDATE customers SET stripe_customer_id = $1 WHERE customer_id = $2',
471 static async createCustomer(email, stripeAccountId, options = {}) {
473 const customerData = {
475 ...options, // Merge in name, phone, address, metadata, etc.
478 const customer = await stripe.customers.create(
481 stripeAccount: stripeAccountId, // Create on Connected Account
485 console.log(`👤 Created Customer ${customer.id} on account ${stripeAccountId}`);
489 console.error('❌ Failed to create customer:', error.message);
490 throw new Error(`Customer creation failed: ${error.message}`);
495 * Creates subscription for recurring billing.
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.).
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)
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
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
527 * // Create monthly subscription with 14-day trial and 2.5% platform fee
528 * const subscription = await stripeService.createSubscription(
530 * 'price_1234567890', // $29/month price
533 * trial_period_days: 14,
534 * quantity: 5, // 5 seats
535 * application_fee_percent: 2.5,
537 * contract_id: '101',
543 * console.log(subscription.id); // sub_1234567890
544 * console.log(subscription.status); // 'trialing' or 'active'
545 * console.log(subscription.current_period_end); // Unix timestamp
547 * // Store subscription.id in stripe_subscriptions table
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]
555 static async createSubscription(customerId, priceId, stripeAccountId, options = {}) {
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.
563 // Remove quantity from root options (already in items array)
564 delete subscriptionData.quantity;
566 const subscription = await stripe.subscriptions.create(
569 stripeAccount: stripeAccountId, // Create on Connected Account
573 console.log(`📅 Created Subscription ${subscription.id} (${subscription.status}) on account ${stripeAccountId}`);
577 console.error('❌ Failed to create subscription:', error.message);
578 throw new Error(`Subscription creation failed: ${error.message}`);
583 * Attaches payment method to customer.
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.
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
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
606 * // Attach card and set as default
607 * const paymentMethod = await stripeService.attachPaymentMethod(
608 * 'pm_1234567890', // From frontend Stripe.js
611 * true // Set as default
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
620 * // Store in database
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]
629 static async attachPaymentMethod(paymentMethodId, customerId, stripeAccountId, setAsDefault = false) {
631 // Attach payment method to customer
632 const paymentMethod = await stripe.paymentMethods.attach(
634 { customer: customerId },
635 { stripeAccount: stripeAccountId }
638 console.log(`💳 Attached Payment Method ${paymentMethodId} to customer ${customerId}`);
640 // Optionally set as default payment method
642 await stripe.customers.update(
646 default_payment_method: paymentMethodId,
649 { stripeAccount: stripeAccountId }
651 console.log(` Set as default payment method`);
654 return paymentMethod;
656 console.error('❌ Failed to attach payment method:', error.message);
657 throw new Error(`Payment method attachment failed: ${error.message}`);
662 * Updates Connected Account configuration in database.
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.
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
677 * // After receiving account.updated webhook
678 * const account = await stripeService.retrieveAccount('acct_1234567890');
679 * await stripeService.updateAccountConfig(789, account);
681 * console.log('✅ Account config synced to database');
683 static async updateAccountConfig(tenantId, account) {
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';
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';
706 `UPDATE stripe_config SET
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,
716 WHERE tenant_id = $9 AND stripe_account_id = $10`,
719 account.charges_enabled,
720 account.payouts_enabled,
721 account.details_submitted,
723 account.requirements.currently_due || [],
724 account.requirements.eventually_due || [],
725 account.requirements.disabled_reason || null,
731 console.log(`✅ Updated account config for tenant ${tenantId} (status: ${accountStatus})`);
733 console.error('❌ Failed to update account config:', error.message);
739module.exports = StripeService;