EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
customerMerge.js
Go to the documentation of this file.
1/**
2 * @file customerMerge.js
3 * @module routes/customerMerge
4 * @description Customer merge endpoints for consolidating duplicate customer records.
5 * Provides comparison and merge operations with related data migration (tickets, invoices,
6 * contracts, agents, hosting resources). Admin-only operations.
7 * @requires express
8 * @requires middleware/auth
9 * @requires middleware/adminOnly
10 * @requires middleware/tenant
11 * @requires db
12 * @author RMM-PSA Development Team
13 * @copyright 2026 RMM-PSA Platform
14 * @license Proprietary
15 */
16
17/**
18 * @apiDefine CustomerMerge Customer Merge
19 * Duplicate customer consolidation operations (admin only)
20 */
21
22const express = require('express');
23const router = express.Router();
24const authenticateToken = require('../middleware/auth');
25const requireAdmin = require('../middleware/adminOnly');
26const { setTenantContext } = require('../middleware/tenant');
27const pool = require('../db');
28
29router.use(authenticateToken, setTenantContext);
30
31/**
32 * @api {get} /api/customers/merge/compare/:id1/:id2 Compare customers
33 * @apiName CompareCustomersForMerge
34 * @apiGroup CustomerMerge
35 * @apiDescription Compare two customer records to review data before merge.
36 * Returns both customer details plus counts of related records (tickets, invoices,
37 * contracts, agents, hosting resources). Enforces tenant isolation.
38 * @apiParam {number} id1 First customer ID
39 * @apiParam {number} id2 Second customer ID
40 * @apiSuccess {object} customer1 First customer details and counts
41 * @apiSuccess {number} customer1.customer_id Customer ID
42 * @apiSuccess {string} customer1.name Customer name
43 * @apiSuccess {string} customer1.email Customer email
44 * @apiSuccess {number} customer1.tickets_count Number of tickets
45 * @apiSuccess {number} customer1.invoices_count Number of invoices
46 * @apiSuccess {number} customer1.contracts_count Number of contracts
47 * @apiSuccess {number} customer1.agents_count Number of agents
48 * @apiSuccess {number} customer1.hosting_apps_count Number of hosting apps
49 * @apiSuccess {Object} customer2 Second customer details and counts (same structure)
50 * @apiError (404) {String} error="One or both customers not found" Customer ID(s) not found or tenant mismatch
51 * @apiError (500) {String} error="Failed to compare customers" Database query failed
52 * @apiExample {curl} Example:
53 * curl -X GET http://localhost:3000/api/customers/merge/compare/42/43 \\
54 * -H "Authorization: Bearer YOUR_TOKEN"
55 * @apiSuccessExample {json} Success-Response:
56 * HTTP/1.1 200 OK
57 * {
58 * "customer1": {
59 * "customer_id": 42,
60 * "name": "Acme Corp",
61 * "email": "contact@acme.com",
62 * "tickets_count": 15,
63 * "invoices_count": 8,
64 * "contracts_count": 2,
65 * "agents_count": 5,
66 * "hosting_apps_count": 3
67 * },
68 * "customer2": {
69 * "customer_id": 43,
70 * "name": "Acme Corporation",
71 * "email": "admin@acme.com",
72 * "tickets_count": 3,
73 * "invoices_count": 1,
74 * "contracts_count": 0,
75 * "agents_count": 2,
76 * "hosting_apps_count": 0
77 * }
78 * }
79 */
80router.get('/merge/compare/:id1/:id2', async (req, res) => {
81 const { id1, id2 } = req.params;
82
83 try {
84 // Get customer details
85 const [customer1, customer2] = await Promise.all([
86 pool.query('SELECT * FROM customers WHERE customer_id = $1 AND tenant_id = $2', [id1, req.tenant.id]),
87 pool.query('SELECT * FROM customers WHERE customer_id = $1 AND tenant_id = $2', [id2, req.tenant.id])
88 ]);
89
90 if (customer1.rows.length === 0 || customer2.rows.length === 0) {
91 return res.status(404).json({ error: 'One or both customers not found' });
92 }
93
94 // Get counts of related data for both customers
95 const [
96 tickets1, tickets2,
97 invoices1, invoices2,
98 contracts1, contracts2,
99 agents1, agents2,
100 hostingApps1, hostingApps2
101 ] = await Promise.all([
102 pool.query('SELECT COUNT(*) FROM tickets WHERE customer_id = $1', [id1]),
103 pool.query('SELECT COUNT(*) FROM tickets WHERE customer_id = $1', [id2]),
104 pool.query('SELECT COUNT(*) FROM invoices WHERE customer_id = $1', [id1]),
105 pool.query('SELECT COUNT(*) FROM invoices WHERE customer_id = $1', [id2]),
106 pool.query('SELECT COUNT(*) FROM contracts WHERE customer_id = $1', [id1]),
107 pool.query('SELECT COUNT(*) FROM contracts WHERE customer_id = $1', [id2]),
108 pool.query('SELECT COUNT(*) FROM agents WHERE customer_id = $1', [id1]),
109 pool.query('SELECT COUNT(*) FROM agents WHERE customer_id = $1', [id2]),
110 pool.query('SELECT COUNT(*) FROM hosting_apps WHERE customer_id = $1', [id1]),
111 pool.query('SELECT COUNT(*) FROM hosting_apps WHERE customer_id = $1', [id2])
112 ]);
113
114 res.json({
115 customer1: {
116 ...customer1.rows[0],
117 tickets_count: parseInt(tickets1.rows[0].count),
118 invoices_count: parseInt(invoices1.rows[0].count),
119 contracts_count: parseInt(contracts1.rows[0].count),
120 agents_count: parseInt(agents1.rows[0].count),
121 hosting_apps_count: parseInt(hostingApps1.rows[0].count)
122 },
123 customer2: {
124 ...customer2.rows[0],
125 tickets_count: parseInt(tickets2.rows[0].count),
126 invoices_count: parseInt(invoices2.rows[0].count),
127 contracts_count: parseInt(contracts2.rows[0].count),
128 agents_count: parseInt(agents2.rows[0].count),
129 hosting_apps_count: parseInt(hostingApps2.rows[0].count)
130 }
131 });
132 } catch (error) {
133 console.error('[Customer Merge] Error comparing customers:', error);
134 res.status(500).json({ error: 'Failed to compare customers' });
135 }
136});
137
138/**
139 * @api {post} /api/customers/merge Merge customers
140 * @apiName MergeCustomers
141 * @apiGroup CustomerMerge
142 * @apiDescription Merge two customer records - migrate all data from secondary to primary, then delete secondary.
143 * Admin only. Performs atomic transaction to migrate tickets, invoices, contracts, agents,
144 * and hosting resources (apps, droplets, databases). Requires explicit admin authentication.
145 * @apiParam {number} primary_customer_id Primary customer ID (data preserved)
146 * @apiParam {number} secondary_customer_id Secondary customer ID (will be deleted after merge)
147 * @apiSuccess {string} message="Customers merged successfully"
148 * @apiSuccess {number} primary_customer_id Primary customer ID
149 * @apiSuccess {number} secondary_customer_id Deleted secondary customer ID
150 * @apiError (400) {String} error="Both customer IDs are required" Missing customer IDs
151 * @apiError (400) {String} error="Cannot merge a customer with itself" Same ID provided for both
152 * @apiError (403) {String} error="Forbidden" User not admin
153 * @apiError (500) {String} error Error message from merge operation (transaction rolled back)
154 * @apiExample {curl} Example:
155 * curl -X POST http://localhost:3000/api/customers/merge \\
156 * -H "Authorization: Bearer YOUR_TOKEN" \\
157 * -H "Content-Type: application/json" \\
158 * -d '{
159 * "primary_customer_id": 42,
160 * "secondary_customer_id": 43
161 * }'
162 * @apiSuccessExample {json} Success-Response:
163 * HTTP/1.1 200 OK
164 * {
165 * "message": "Customers merged successfully",
166 * "primary_customer_id": 42,
167 * "secondary_customer_id": 43
168 * }
169 */
170router.post('/merge', requireAdmin, async (req, res) => {
171 const { primary_customer_id, secondary_customer_id } = req.body;
172
173 if (!primary_customer_id || !secondary_customer_id) {
174 return res.status(400).json({ error: 'Both customer IDs are required' });
175 }
176
177 if (primary_customer_id === secondary_customer_id) {
178 return res.status(400).json({ error: 'Cannot merge a customer with itself' });
179 }
180
181 const client = await pool.connect();
182
183 try {
184 await client.query('BEGIN');
185
186 // Verify both customers exist and belong to this tenant
187 const customersCheck = await client.query(
188 'SELECT customer_id FROM customers WHERE customer_id IN ($1, $2) AND tenant_id = $3',
189 [primary_customer_id, secondary_customer_id, req.tenant.id]
190 );
191
192 if (customersCheck.rows.length !== 2) {
193 throw new Error('One or both customers not found or do not belong to this tenant');
194 }
195
196 // Migrate all related data from secondary to primary
197 await client.query('UPDATE tickets SET customer_id = $1 WHERE customer_id = $2', [primary_customer_id, secondary_customer_id]);
198 await client.query('UPDATE invoices SET customer_id = $1 WHERE customer_id = $2', [primary_customer_id, secondary_customer_id]);
199 await client.query('UPDATE contracts SET customer_id = $1 WHERE customer_id = $2', [primary_customer_id, secondary_customer_id]);
200 await client.query('UPDATE agents SET customer_id = $1 WHERE customer_id = $2', [primary_customer_id, secondary_customer_id]);
201 await client.query('UPDATE hosting_apps SET customer_id = $1 WHERE customer_id = $2', [primary_customer_id, secondary_customer_id]);
202 await client.query('UPDATE hosting_droplets SET customer_id = $1 WHERE customer_id = $2', [primary_customer_id, secondary_customer_id]);
203 await client.query('UPDATE hosting_databases SET customer_id = $1 WHERE customer_id = $2', [primary_customer_id, secondary_customer_id]);
204
205 // Delete the secondary customer
206 await client.query('DELETE FROM customers WHERE customer_id = $1', [secondary_customer_id]);
207
208 await client.query('COMMIT');
209
210 res.json({
211 message: 'Customers merged successfully',
212 primary_customer_id,
213 secondary_customer_id
214 });
215 } catch (error) {
216 await client.query('ROLLBACK');
217 console.error('[Customer Merge] Error merging customers:', error);
218 res.status(500).json({ error: error.message || 'Failed to merge customers' });
219 } finally {
220 client.release();
221 }
222});
223
224module.exports = router;