2 * @file routes/invoice-payments.js
3 * @module routes/invoice-payments
4 * @description Invoice payment routes with Stripe integration
7const express = require('express');
8const router = express.Router();
9const pool = require('../services/db');
10const authenticateToken = require('../middleware/auth');
12// Initialize Stripe with correct key based on mode
13const stripeMode = process.env.STRIPE_MODE || 'test';
14const stripeSecretKey = stripeMode === 'live'
15 ? process.env.STRIPE_LIVE_SECRET_KEY
16 : process.env.STRIPE_TEST_SECRET_KEY;
17const stripePublishableKey = stripeMode === 'live'
18 ? process.env.STRIPE_LIVE_PUBLISHABLE_KEY
19 : process.env.STRIPE_TEST_PUBLISHABLE_KEY;
21const stripe = require('stripe')(stripeSecretKey);
23console.log(`[Stripe] Invoice Payments initialized in ${stripeMode.toUpperCase()} mode`);
26 * POST /api/invoices/:id/create-payment-intent
28 * Create a Stripe payment intent for an invoice
30router.post('/:id/create-payment-intent', authenticateToken, async (req, res) => {
32 const invoiceId = req.params.id;
33 const tenantId = req.user.tenant_id;
35 // Get invoice details
36 const invoiceResult = await pool.query(
37 `SELECT i.*, c.name as customer_name, c.email as customer_email
39 LEFT JOIN customers c ON i.customer_id = c.customer_id
40 WHERE i.invoice_id = $1 AND i.tenant_id = $2`,
44 if (invoiceResult.rows.length === 0) {
45 return res.status(404).json({ error: 'Invoice not found' });
48 const invoice = invoiceResult.rows[0];
50 // Check if already paid
51 if (invoice.payment_status === 'paid') {
52 return res.status(400).json({ error: 'Invoice is already paid' });
55 // Get tenant's Stripe account
56 const stripeAccountResult = await pool.query(
57 'SELECT stripe_account_id FROM stripe_config WHERE tenant_id = $1 AND charges_enabled = true',
61 if (stripeAccountResult.rows.length === 0) {
62 return res.status(400).json({ error: 'Stripe account not configured or not enabled for charges' });
65 const stripeAccountId = stripeAccountResult.rows[0].stripe_account_id;
67 // Calculate amount in cents
68 const amountInCents = Math.round(parseFloat(invoice.total || invoice.amount || 0) * 100);
70 if (amountInCents <= 0) {
71 return res.status(400).json({ error: 'Invalid invoice amount' });
74 // Create payment intent on connected account
75 const paymentIntent = await stripe.paymentIntents.create({
76 amount: amountInCents,
77 currency: (invoice.currency || 'AUD').toLowerCase(),
78 description: `Invoice #${invoice.invoice_id} - ${invoice.description || 'Payment'}`,
80 invoice_id: invoiceId.toString(),
81 customer_id: invoice.customer_id?.toString() || '',
83 customer_name: invoice.customer_name || '',
85 application_fee_amount: Math.round(amountInCents * 0.03), // 3% platform fee
87 stripeAccount: stripeAccountId,
91 clientSecret: paymentIntent.client_secret,
92 publishableKey: stripePublishableKey,
93 stripeAccount: stripeAccountId, // Connected account ID for Elements
94 amount: amountInCents,
95 currency: (invoice.currency || 'AUD').toLowerCase(),
96 mode: stripeMode, // Include mode in response for client-side logging
100 console.error('Error creating payment intent:', error);
101 res.status(500).json({ error: error.message || 'Failed to create payment intent' });
106 * POST /api/invoices/:id/confirm-payment
108 * Confirm payment and update invoice status (called after successful payment)
110router.post('/:id/confirm-payment', authenticateToken, async (req, res) => {
111 const client = await pool.connect(); // Get a client for transaction
114 const invoiceId = req.params.id;
115 const tenantId = req.user.tenant_id;
116 const { paymentIntentId } = req.body;
118 if (!paymentIntentId) {
119 return res.status(400).json({ error: 'Payment intent ID required' });
122 // Get tenant's Stripe account
123 const stripeAccountResult = await client.query(
124 'SELECT stripe_account_id FROM stripe_config WHERE tenant_id = $1',
128 if (stripeAccountResult.rows.length === 0) {
129 return res.status(400).json({ error: 'Stripe account not configured' });
132 const stripeAccountId = stripeAccountResult.rows[0].stripe_account_id;
134 // Verify payment intent status with Stripe
135 const paymentIntent = await stripe.paymentIntents.retrieve(
137 { stripeAccount: stripeAccountId }
140 if (paymentIntent.status !== 'succeeded') {
141 return res.status(400).json({ error: 'Payment not completed' });
145 await client.query('BEGIN');
147 // Update invoice payment status
148 const invoiceResult = await client.query(
150 SET payment_status = 'paid',
152 updated_at = CURRENT_TIMESTAMP
153 WHERE invoice_id = $1 AND tenant_id = $2
154 RETURNING invoice_id, payment_status, status`,
155 [invoiceId, tenantId]
158 if (invoiceResult.rows.length === 0) {
159 await client.query('ROLLBACK');
160 return res.status(404).json({ error: 'Invoice not found' });
163 // Record payment in payments table
164 const paymentMethodDetails = paymentIntent.charges?.data[0]?.payment_method_details;
167 `INSERT INTO payments (
168 tenant_id, customer_id, invoice_id,
169 stripe_payment_intent_id, stripe_charge_id, stripe_account_id,
170 amount, currency, status,
171 payment_method_type, card_brand, card_last4,
172 application_fee_amount, payment_intent_status,
173 description, succeeded_at, created_at
181 $14, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
183 WHERE invoice_id = $2 AND tenant_id = $1`,
188 paymentIntent.charges?.data[0]?.id || null,
190 paymentIntent.amount,
191 paymentIntent.currency,
192 'succeeded', // Valid status from constraint: pending, processing, succeeded, failed, canceled, refunded
193 paymentMethodDetails?.type || 'card',
194 paymentMethodDetails?.card?.brand || null,
195 paymentMethodDetails?.card?.last4 || null,
196 paymentIntent.application_fee_amount || null,
197 paymentIntent.status,
198 `Invoice #${invoiceId} payment`
202 // Commit transaction
203 await client.query('COMMIT');
207 invoice: invoiceResult.rows[0],
211 // Rollback on any error
212 await client.query('ROLLBACK');
213 console.error('Error confirming payment:', error);
214 res.status(500).json({ error: error.message || 'Failed to confirm payment' });
216 // Release client back to pool
221module.exports = router;