EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
invoice-payments.js
Go to the documentation of this file.
1/**
2 * @file routes/invoice-payments.js
3 * @module routes/invoice-payments
4 * @description Invoice payment routes with Stripe integration
5 */
6
7const express = require('express');
8const router = express.Router();
9const pool = require('../services/db');
10const authenticateToken = require('../middleware/auth');
11
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;
20
21const stripe = require('stripe')(stripeSecretKey);
22
23console.log(`[Stripe] Invoice Payments initialized in ${stripeMode.toUpperCase()} mode`);
24
25/**
26 * POST /api/invoices/:id/create-payment-intent
27 *
28 * Create a Stripe payment intent for an invoice
29 */
30router.post('/:id/create-payment-intent', authenticateToken, async (req, res) => {
31 try {
32 const invoiceId = req.params.id;
33 const tenantId = req.user.tenant_id;
34
35 // Get invoice details
36 const invoiceResult = await pool.query(
37 `SELECT i.*, c.name as customer_name, c.email as customer_email
38 FROM invoices i
39 LEFT JOIN customers c ON i.customer_id = c.customer_id
40 WHERE i.invoice_id = $1 AND i.tenant_id = $2`,
41 [invoiceId, tenantId]
42 );
43
44 if (invoiceResult.rows.length === 0) {
45 return res.status(404).json({ error: 'Invoice not found' });
46 }
47
48 const invoice = invoiceResult.rows[0];
49
50 // Check if already paid
51 if (invoice.payment_status === 'paid') {
52 return res.status(400).json({ error: 'Invoice is already paid' });
53 }
54
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',
58 [tenantId]
59 );
60
61 if (stripeAccountResult.rows.length === 0) {
62 return res.status(400).json({ error: 'Stripe account not configured or not enabled for charges' });
63 }
64
65 const stripeAccountId = stripeAccountResult.rows[0].stripe_account_id;
66
67 // Calculate amount in cents
68 const amountInCents = Math.round(parseFloat(invoice.total || invoice.amount || 0) * 100);
69
70 if (amountInCents <= 0) {
71 return res.status(400).json({ error: 'Invalid invoice amount' });
72 }
73
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'}`,
79 metadata: {
80 invoice_id: invoiceId.toString(),
81 customer_id: invoice.customer_id?.toString() || '',
82 tenant_id: tenantId,
83 customer_name: invoice.customer_name || '',
84 },
85 application_fee_amount: Math.round(amountInCents * 0.03), // 3% platform fee
86 }, {
87 stripeAccount: stripeAccountId,
88 });
89
90 res.json({
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
97 });
98
99 } catch (error) {
100 console.error('Error creating payment intent:', error);
101 res.status(500).json({ error: error.message || 'Failed to create payment intent' });
102 }
103});
104
105/**
106 * POST /api/invoices/:id/confirm-payment
107 *
108 * Confirm payment and update invoice status (called after successful payment)
109 */
110router.post('/:id/confirm-payment', authenticateToken, async (req, res) => {
111 const client = await pool.connect(); // Get a client for transaction
112
113 try {
114 const invoiceId = req.params.id;
115 const tenantId = req.user.tenant_id;
116 const { paymentIntentId } = req.body;
117
118 if (!paymentIntentId) {
119 return res.status(400).json({ error: 'Payment intent ID required' });
120 }
121
122 // Get tenant's Stripe account
123 const stripeAccountResult = await client.query(
124 'SELECT stripe_account_id FROM stripe_config WHERE tenant_id = $1',
125 [tenantId]
126 );
127
128 if (stripeAccountResult.rows.length === 0) {
129 return res.status(400).json({ error: 'Stripe account not configured' });
130 }
131
132 const stripeAccountId = stripeAccountResult.rows[0].stripe_account_id;
133
134 // Verify payment intent status with Stripe
135 const paymentIntent = await stripe.paymentIntents.retrieve(
136 paymentIntentId,
137 { stripeAccount: stripeAccountId }
138 );
139
140 if (paymentIntent.status !== 'succeeded') {
141 return res.status(400).json({ error: 'Payment not completed' });
142 }
143
144 // Start transaction
145 await client.query('BEGIN');
146
147 // Update invoice payment status
148 const invoiceResult = await client.query(
149 `UPDATE invoices
150 SET payment_status = 'paid',
151 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]
156 );
157
158 if (invoiceResult.rows.length === 0) {
159 await client.query('ROLLBACK');
160 return res.status(404).json({ error: 'Invoice not found' });
161 }
162
163 // Record payment in payments table
164 const paymentMethodDetails = paymentIntent.charges?.data[0]?.payment_method_details;
165
166 await client.query(
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
174 )
175 SELECT
176 $1, customer_id, $2,
177 $3, $4, $5,
178 $6, $7, $8,
179 $9, $10, $11,
180 $12, $13,
181 $14, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
182 FROM invoices
183 WHERE invoice_id = $2 AND tenant_id = $1`,
184 [
185 tenantId,
186 invoiceId,
187 paymentIntentId,
188 paymentIntent.charges?.data[0]?.id || null,
189 stripeAccountId,
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`
199 ]
200 );
201
202 // Commit transaction
203 await client.query('COMMIT');
204
205 res.json({
206 success: true,
207 invoice: invoiceResult.rows[0],
208 });
209
210 } catch (error) {
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' });
215 } finally {
216 // Release client back to pool
217 client.release();
218 }
219});
220
221module.exports = router;