EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
portalCustomer.js
Go to the documentation of this file.
1/**
2 * @file portalCustomer.js
3 * @description Customer Portal Data Routes
4 *
5 * Provides data access endpoints for authenticated customer portal users.
6 * All routes require valid JWT authentication token.
7 *
8 * **Features:**
9 * - Profile management (view/update)
10 * - Invoice viewing (list/details)
11 * - Ticket management (list/create/comment)
12 * - Contract viewing
13 * - Asset listing
14 * - Document access
15 * - Dashboard statistics
16 *
17 * **Security:**
18 * - All routes protected by authenticateCustomer middleware
19 * - Tenant isolation via setCustomerContext middleware
20 * - Customer data filtered by customer_id from JWT
21 * - Audit logging for data modifications
22 *
23 * @module routes/portalCustomer
24 * @requires express
25 * @requires services/db
26 * @requires middleware/customerAuth
27 * @author Independent Business Group
28 * @since 1.0.0
29 * @see module:routes/portalAuth
30 * @see module:middleware/customerAuth
31 */
32
33const express = require('express');
34const router = express.Router();
35const pool = require('../services/db');
36const { authenticateCustomer, setCustomerContext, logCustomerAudit } = require('../middleware/customerAuth');
37
38// Apply authentication to all routes
39router.use(authenticateCustomer);
40router.use(setCustomerContext);
41
42/**
43 * @api {get} /portal/customer/profile Get Customer Profile
44 * @apiName GetCustomerProfile
45 * @apiGroup CustomerPortalData
46 * @apiVersion 1.0.0
47 *
48 * @apiDescription
49 * Returns customer profile information including portal user details,
50 * billing address, and account status.
51 *
52 * @apiPermission customer
53 *
54 * @apiHeader {String} Authorization Bearer JWT token
55 *
56 * @apiSuccess {Number} customer_id Customer ID
57 * @apiSuccess {String} customer_name Customer business name
58 * @apiSuccess {String} contact_email Customer contact email
59 * @apiSuccess {String} contact_phone Customer contact phone
60 * @apiSuccess {String} billing_address Billing street address
61 * @apiSuccess {String} billing_city Billing city
62 * @apiSuccess {String} billing_state Billing state/province
63 * @apiSuccess {String} billing_postcode Billing postal code
64 * @apiSuccess {String} billing_country Billing country
65 * @apiSuccess {String} status Account status
66 * @apiSuccess {Date} created_at Account creation date
67 * @apiSuccess {String} portal_email Portal login email
68 * @apiSuccess {String} first_name User first name
69 * @apiSuccess {String} last_name User last name
70 * @apiSuccess {String} portal_phone User phone number
71 *
72 * @apiError (401) {String} error Unauthorized
73 * @apiError (404) {String} error Customer not found
74 * @apiError (500) {String} error Failed to get profile
75 */
76router.get('/profile', async (req, res) => {
77 try {
78 const result = await pool.query(
79 `SELECT
80 c.customer_id, c.name as customer_name, c.email as contact_email, c.phone as contact_phone,
81 c.billing_address, c.billing_city, c.billing_state, c.billing_postcode,
82 c.billing_country, c.status, c.created_at,
83 cu.email as portal_email, cu.first_name, cu.last_name, cu.phone as portal_phone
84 FROM customers c
85 JOIN customer_users cu ON c.customer_id = cu.customer_id
86 WHERE c.customer_id = $1 AND cu.customer_user_id = $2`,
87 [req.customerId, req.customerUserId]
88 );
89
90 if (result.rows.length === 0) {
91 return res.status(404).json({ error: 'Customer not found.' });
92 }
93
94 res.json(result.rows[0]);
95 } catch (error) {
96 console.error('Get customer profile error:', error);
97 res.status(500).json({ error: 'Failed to get profile.', details: error.message });
98 }
99});
100
101/**
102 * @api {put} /portal/customer/profile Update Customer Profile
103 * @apiName UpdateCustomerProfile
104 * @apiGroup CustomerPortalData
105 * @apiVersion 1.0.0
106 *
107 * @apiDescription
108 * Updates customer contact information and billing address.
109 * Logs audit event for profile modifications.
110 *
111 * @apiPermission customer
112 *
113 * @apiHeader {String} Authorization Bearer JWT token
114 * @apiHeader {String} Content-Type application/json
115 *
116 * @apiParam {String} [firstName] User first name
117 * @apiParam {String} [lastName] User last name
118 * @apiParam {String} [phone] User phone number
119 * @apiParam {String} [contactEmail] Customer contact email
120 * @apiParam {String} [contactPhone] Customer contact phone
121 * @apiParam {String} [billingAddress] Billing street address
122 * @apiParam {String} [billingCity] Billing city
123 * @apiParam {String} [billingState] Billing state/province
124 * @apiParam {String} [billingPostcode] Billing postal code
125 * @apiParam {String} [billingCountry] Billing country
126 *
127 * @apiSuccess {String} message Success message
128 *
129 * @apiError (401) {String} error Unauthorized
130 * @apiError (500) {String} error Failed to update profile
131 */
132router.put('/profile', async (req, res) => {
133 try {
134 const {
135 firstName,
136 lastName,
137 phone,
138 contactEmail,
139 contactPhone,
140 billingAddress,
141 billingCity,
142 billingState,
143 billingPostcode,
144 billingCountry
145 } = req.body;
146
147 // Update customer_users
148 await pool.query(
149 `UPDATE customer_users
150 SET first_name = $1, last_name = $2, phone = $3, updated_at = NOW()
151 WHERE customer_user_id = $4`,
152 [firstName, lastName, phone, req.customerUserId]
153 );
154
155 // Update customers
156 await pool.query(
157 `UPDATE customers
158 SET email = $1, phone = $2,
159 billing_address = $3, billing_city = $4, billing_state = $5,
160 billing_postcode = $6, billing_country = $7
161 WHERE customer_id = $8`,
162 [
163 contactEmail,
164 contactPhone,
165 billingAddress,
166 billingCity,
167 billingState,
168 billingPostcode,
169 billingCountry,
170 req.customerId
171 ]
172 );
173
174 // Log audit event
175 await logCustomerAudit(
176 req.customerUserId,
177 req.tenantId,
178 'profile_update',
179 'customer',
180 req.customerId,
181 { fields: Object.keys(req.body) },
182 req.ip || req.connection.remoteAddress
183 );
184
185 res.json({ message: 'Profile updated successfully.' });
186 } catch (error) {
187 console.error('Update customer profile error:', error);
188 res.status(500).json({ error: 'Failed to update profile.', details: error.message });
189 }
190});
191
192/**
193 * @api {get} /portal/customer/invoices List Customer Invoices
194 * @apiName ListCustomerInvoices
195 * @apiGroup CustomerPortalData
196 * @apiVersion 1.0.0
197 *
198 * @apiDescription
199 * Returns paginated list of customer invoices with optional status filtering.
200 *
201 * @apiPermission customer
202 *
203 * @apiHeader {String} Authorization Bearer JWT token
204 *
205 * @apiParam {String} [status] Filter by status (Paid, Unpaid, Overdue, etc.)
206 * @apiParam {Number} [limit=50] Results per page
207 * @apiParam {Number} [offset=0] Pagination offset
208 *
209 * @apiSuccess {Object[]} invoices Array of invoices
210 * @apiSuccess {Number} invoices.invoice_id Invoice ID
211 * @apiSuccess {String} invoices.invoice_number Invoice number
212 * @apiSuccess {Date} invoices.invoice_date Invoice date
213 * @apiSuccess {Date} invoices.due_date Due date
214 * @apiSuccess {Number} invoices.subtotal Subtotal amount
215 * @apiSuccess {Number} invoices.tax Tax amount
216 * @apiSuccess {Number} invoices.total Total amount
217 * @apiSuccess {String} invoices.status Status
218 * @apiSuccess {String} invoices.payment_method Payment method
219 * @apiSuccess {Date} invoices.payment_date Payment date
220 * @apiSuccess {Date} invoices.created_at Creation date
221 * @apiSuccess {Number} total Total count of invoices
222 * @apiSuccess {Number} limit Page size
223 * @apiSuccess {Number} offset Current offset
224 *
225 * @apiError (401) {String} error Unauthorized
226 * @apiError (500) {String} error Failed to get invoices
227 */
228router.get('/invoices', async (req, res) => {
229 try {
230 const { status, limit = 50, offset = 0 } = req.query;
231
232 let query = `
233 SELECT
234 invoice_id, invoice_number, invoice_date, due_date,
235 subtotal, tax, total, status, payment_method, payment_date,
236 created_at
237 FROM invoices
238 WHERE customer_id = $1 AND tenant_id = $2
239 `;
240
241 const params = [req.customerId, req.tenantId];
242
243 if (status) {
244 query += ` AND status = $${params.length + 1}`;
245 params.push(status);
246 }
247
248 query += ` ORDER BY invoice_date DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
249 params.push(parseInt(limit), parseInt(offset));
250
251 const result = await pool.query(query, params);
252
253 // Get total count
254 const countResult = await pool.query(
255 'SELECT COUNT(*) FROM invoices WHERE customer_id = $1 AND tenant_id = $2',
256 [req.customerId, req.tenantId]
257 );
258
259 res.json({
260 invoices: result.rows,
261 total: parseInt(countResult.rows[0].count),
262 limit: parseInt(limit),
263 offset: parseInt(offset)
264 });
265 } catch (error) {
266 console.error('Get customer invoices error:', error);
267 res.status(500).json({ error: 'Failed to get invoices.', details: error.message });
268 }
269});
270
271/**
272 * @api {get} /portal/customer/invoices/:id Get Invoice Details
273 * @apiName GetInvoiceDetails
274 * @apiGroup CustomerPortalData
275 * @apiVersion 1.0.0
276 *
277 * @apiDescription
278 * Returns invoice details including line items.
279 *
280 * @apiPermission customer
281 *
282 * @apiHeader {String} Authorization Bearer JWT token
283 *
284 * @apiParam {Number} id Invoice ID (URL parameter)
285 *
286 * @apiSuccess {Object} invoice Invoice object with line_items array
287 * @apiSuccess {Number} invoice.invoice_id Invoice ID
288 * @apiSuccess {String} invoice.invoice_number Invoice number
289 * @apiSuccess {Object[]} invoice.line_items Array of line items
290 *
291 * @apiError (401) {String} error Unauthorized
292 * @apiError (404) {String} error Invoice not found
293 * @apiError (500) {String} error Failed to get invoice details
294 */
295router.get('/invoices/:id', async (req, res) => {
296 try {
297 const invoiceId = parseInt(req.params.id);
298
299 // Get invoice
300 const invoiceResult = await pool.query(
301 `SELECT * FROM invoices
302 WHERE invoice_id = $1 AND customer_id = $2 AND tenant_id = $3`,
303 [invoiceId, req.customerId, req.tenantId]
304 );
305
306 if (invoiceResult.rows.length === 0) {
307 return res.status(404).json({ error: 'Invoice not found.' });
308 }
309
310 // Get line items
311 const lineItemsResult = await pool.query(
312 `SELECT * FROM invoice_line_items
313 WHERE invoice_id = $1
314 ORDER BY line_item_id`,
315 [invoiceId]
316 );
317
318 res.json({
319 ...invoiceResult.rows[0],
320 line_items: lineItemsResult.rows
321 });
322 } catch (error) {
323 console.error('Get invoice details error:', error);
324 res.status(500).json({ error: 'Failed to get invoice details.', details: error.message });
325 }
326});
327
328/**
329 * @api {get} /portal/customer/tickets List Customer Tickets
330 * @apiName ListCustomerTickets
331 * @apiGroup CustomerPortalData
332 * @apiVersion 1.0.0
333 *
334 * @apiDescription
335 * Returns paginated list of customer support tickets with optional status filtering.
336 *
337 * @apiPermission customer
338 *
339 * @apiHeader {String} Authorization Bearer JWT token
340 *
341 * @apiParam {String} [status] Filter by status (Open, In Progress, Resolved, Closed)
342 * @apiParam {Number} [limit=50] Results per page
343 * @apiParam {Number} [offset=0] Pagination offset
344 *
345 * @apiSuccess {Object[]} tickets Array of tickets
346 * @apiSuccess {Number} tickets.ticket_id Ticket ID
347 * @apiSuccess {String} tickets.ticket_number Ticket number
348 * @apiSuccess {String} tickets.subject Ticket subject
349 * @apiSuccess {String} tickets.description Ticket description
350 * @apiSuccess {String} tickets.status Status
351 * @apiSuccess {String} tickets.priority Priority (Low, Normal, High, Critical)
352 * @apiSuccess {Number} tickets.assigned_to Assigned user ID
353 * @apiSuccess {Date} tickets.created_at Creation date
354 * @apiSuccess {Date} tickets.updated_at Last update date
355 * @apiSuccess {Date} tickets.resolved_at Resolution date
356 * @apiSuccess {Number} total Total count of tickets
357 * @apiSuccess {Number} limit Page size
358 * @apiSuccess {Number} offset Current offset
359 *
360 * @apiError (401) {String} error Unauthorized
361 * @apiError (500) {String} error Failed to get tickets
362 */
363router.get('/tickets', async (req, res) => {
364 try {
365 const { status, limit = 50, offset = 0 } = req.query;
366
367 let query = `
368 SELECT
369 ticket_id, ticket_number, subject, description, status, priority,
370 assigned_to, created_at, updated_at, resolved_at
371 FROM tickets
372 WHERE customer_id = $1 AND tenant_id = $2
373 `;
374
375 const params = [req.customerId, req.tenantId];
376
377 if (status) {
378 query += ` AND status = $${params.length + 1}`;
379 params.push(status);
380 }
381
382 query += ` ORDER BY created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
383 params.push(parseInt(limit), parseInt(offset));
384
385 const result = await pool.query(query, params);
386
387 // Get total count
388 const countResult = await pool.query(
389 'SELECT COUNT(*) FROM tickets WHERE customer_id = $1 AND tenant_id = $2',
390 [req.customerId, req.tenantId]
391 );
392
393 res.json({
394 tickets: result.rows,
395 total: parseInt(countResult.rows[0].count),
396 limit: parseInt(limit),
397 offset: parseInt(offset)
398 });
399 } catch (error) {
400 console.error('Get customer tickets error:', error);
401 res.status(500).json({ error: 'Failed to get tickets.', details: error.message });
402 }
403});
404
405/**
406 * @api {get} /portal/customer/tickets/:id Get Ticket Details
407 * @apiName GetTicketDetails
408 * @apiGroup CustomerPortalData
409 * @apiVersion 1.0.0
410 *
411 * @apiDescription
412 * Returns ticket details including all comments.
413 *
414 * @apiPermission customer
415 *
416 * @apiHeader {String} Authorization Bearer JWT token
417 *
418 * @apiParam {Number} id Ticket ID (URL parameter)
419 *
420 * @apiSuccess {Object} ticket Ticket object with comments array
421 * @apiSuccess {Number} ticket.ticket_id Ticket ID
422 * @apiSuccess {String} ticket.ticket_number Ticket number
423 * @apiSuccess {String} ticket.subject Subject
424 * @apiSuccess {String} ticket.description Description
425 * @apiSuccess {String} ticket.status Status
426 * @apiSuccess {String} ticket.priority Priority
427 * @apiSuccess {String} ticket.assigned_to_name Assigned technician name
428 * @apiSuccess {Object[]} ticket.comments Array of comments
429 *
430 * @apiError (401) {String} error Unauthorized
431 * @apiError (404) {String} error Ticket not found
432 * @apiError (500) {String} error Failed to get ticket details
433 */
434router.get('/tickets/:id', async (req, res) => {
435 try {
436 const ticketId = parseInt(req.params.id);
437
438 // Get ticket
439 const ticketResult = await pool.query(
440 `SELECT t.*, u.user_name as assigned_to_name
441 FROM tickets t
442 LEFT JOIN users u ON t.assigned_to = u.user_id
443 WHERE t.ticket_id = $1 AND t.customer_id = $2 AND t.tenant_id = $3`,
444 [ticketId, req.customerId, req.tenantId]
445 );
446
447 if (ticketResult.rows.length === 0) {
448 return res.status(404).json({ error: 'Ticket not found.' });
449 }
450
451 // Get comments
452 const commentsResult = await pool.query(
453 `SELECT tc.*, u.user_name as author_name
454 FROM ticket_comments tc
455 LEFT JOIN users u ON tc.user_id = u.user_id
456 WHERE tc.ticket_id = $1
457 ORDER BY tc.created_at ASC`,
458 [ticketId]
459 );
460
461 res.json({
462 ...ticketResult.rows[0],
463 comments: commentsResult.rows
464 });
465 } catch (error) {
466 console.error('Get ticket details error:', error);
467 res.status(500).json({ error: 'Failed to get ticket details.', details: error.message });
468 }
469});
470
471/**
472 * @api {post} /portal/customer/tickets Create Support Ticket
473 * @apiName CreateSupportTicket
474 * @apiGroup CustomerPortalData
475 * @apiVersion 1.0.0
476 *
477 * @apiDescription
478 * Creates new support ticket. Auto-generates ticket number.
479 * Logs audit event for ticket creation.
480 *
481 * @apiPermission customer
482 *
483 * @apiHeader {String} Authorization Bearer JWT token
484 * @apiHeader {String} Content-Type application/json
485 *
486 * @apiParam {String} subject Ticket subject (required)
487 * @apiParam {String} description Ticket description (required)
488 * @apiParam {String} [priority=Normal] Priority (Low, Normal, High, Critical)
489 *
490 * @apiSuccess (201) {Object} ticket Created ticket object
491 * @apiSuccess (201) {Number} ticket.ticket_id Ticket ID
492 * @apiSuccess (201) {String} ticket.ticket_number Generated ticket number
493 * @apiSuccess (201) {String} ticket.subject Subject
494 * @apiSuccess (201) {String} ticket.status Status (always Open initially)
495 *
496 * @apiError (400) {String} error Subject and description are required
497 * @apiError (401) {String} error Unauthorized
498 * @apiError (500) {String} error Failed to create ticket
499 */
500router.post('/tickets', async (req, res) => {
501 try {
502 const { subject, description, priority = 'Normal' } = req.body;
503
504 if (!subject || !description) {
505 return res.status(400).json({ error: 'Subject and description are required.' });
506 }
507
508 // Generate ticket number
509 const ticketNumberResult = await pool.query(
510 `SELECT COALESCE(MAX(CAST(SUBSTRING(ticket_number FROM '[0-9]+') AS INTEGER)), 0) + 1 as next_num
511 FROM tickets WHERE tenant_id = $1`,
512 [req.tenantId]
513 );
514 const ticketNumber = `TICKET-${ticketNumberResult.rows[0].next_num}`;
515
516 // Create ticket
517 const result = await pool.query(
518 `INSERT INTO tickets
519 (tenant_id, customer_id, ticket_number, subject, description, priority, status, created_at, updated_at)
520 VALUES ($1, $2, $3, $4, $5, $6, 'Open', NOW(), NOW())
521 RETURNING *`,
522 [req.tenantId, req.customerId, ticketNumber, subject, description, priority]
523 );
524
525 // Log audit event
526 await logCustomerAudit(
527 req.customerUserId,
528 req.tenantId,
529 'ticket_create',
530 'ticket',
531 result.rows[0].ticket_id,
532 { subject, priority },
533 req.ip || req.connection.remoteAddress
534 );
535
536 res.status(201).json(result.rows[0]);
537 } catch (error) {
538 console.error('Create ticket error:', error);
539 res.status(500).json({ error: 'Failed to create ticket.', details: error.message });
540 }
541});
542
543/**
544 * @api {post} /portal/customer/tickets/:id/comments Add Ticket Comment
545 * @apiName AddTicketComment
546 * @apiGroup CustomerPortalData
547 * @apiVersion 1.0.0
548 *
549 * @apiDescription
550 * Adds comment to existing ticket. Updates ticket's updated_at timestamp.
551 * Logs audit event.
552 *
553 * @apiPermission customer
554 *
555 * @apiHeader {String} Authorization Bearer JWT token
556 * @apiHeader {String} Content-Type application/json
557 *
558 * @apiParam {Number} id Ticket ID (URL parameter)
559 * @apiParam {String} comment Comment text (required)
560 *
561 * @apiSuccess (201) {Object} comment Created comment object
562 * @apiSuccess (201) {Number} comment.comment_id Comment ID
563 * @apiSuccess (201) {Number} comment.ticket_id Ticket ID
564 * @apiSuccess (201) {String} comment.comment Comment text
565 * @apiSuccess (201) {Boolean} comment.is_internal Internal flag (always false)
566 * @apiSuccess (201) {Date} comment.created_at Creation date
567 *
568 * @apiError (400) {String} error Comment is required
569 * @apiError (401) {String} error Unauthorized
570 * @apiError (404) {String} error Ticket not found
571 * @apiError (500) {String} error Failed to add comment
572 */
573router.post('/tickets/:id/comments', async (req, res) => {
574 try {
575 const ticketId = parseInt(req.params.id);
576 const { comment } = req.body;
577
578 if (!comment) {
579 return res.status(400).json({ error: 'Comment is required.' });
580 }
581
582 // Verify ticket belongs to customer
583 const ticketResult = await pool.query(
584 'SELECT ticket_id FROM tickets WHERE ticket_id = $1 AND customer_id = $2 AND tenant_id = $3',
585 [ticketId, req.customerId, req.tenantId]
586 );
587
588 if (ticketResult.rows.length === 0) {
589 return res.status(404).json({ error: 'Ticket not found.' });
590 }
591
592 // Add comment
593 const result = await pool.query(
594 `INSERT INTO ticket_comments
595 (ticket_id, user_id, comment, is_internal, created_at)
596 VALUES ($1, NULL, $2, false, NOW())
597 RETURNING *`,
598 [ticketId, comment]
599 );
600
601 // Update ticket updated_at
602 await pool.query(
603 'UPDATE tickets SET updated_at = NOW() WHERE ticket_id = $1',
604 [ticketId]
605 );
606
607 // Log audit event
608 await logCustomerAudit(
609 req.customerUserId,
610 req.tenantId,
611 'ticket_comment',
612 'ticket',
613 ticketId,
614 {},
615 req.ip || req.connection.remoteAddress
616 );
617
618 res.status(201).json(result.rows[0]);
619 } catch (error) {
620 console.error('Add ticket comment error:', error);
621 res.status(500).json({ error: 'Failed to add comment.', details: error.message });
622 }
623});
624
625/**
626 * @api {get} /portal/customer/contracts List Customer Contracts
627 * @apiName ListCustomerContracts
628 * @apiGroup CustomerPortalData
629 * @apiVersion 1.0.0
630 *
631 * @apiDescription
632 * Returns list of all customer contracts with billing information.
633 *
634 * @apiPermission customer
635 *
636 * @apiHeader {String} Authorization Bearer JWT token
637 *
638 * @apiSuccess {Object[]} contracts Array of contracts
639 * @apiSuccess {Number} contracts.contract_id Contract ID
640 * @apiSuccess {String} contracts.contract_name Contract name
641 * @apiSuccess {String} contracts.description Description
642 * @apiSuccess {String} contracts.billing_interval Billing interval (Monthly, Quarterly, Annually)
643 * @apiSuccess {Number} contracts.amount Billing amount
644 * @apiSuccess {Date} contracts.start_date Start date
645 * @apiSuccess {Date} contracts.end_date End date
646 * @apiSuccess {Date} contracts.next_billing_date Next billing date
647 * @apiSuccess {String} contracts.status Status (Active, Expired, Cancelled)
648 * @apiSuccess {Date} contracts.created_at Creation date
649 *
650 * @apiError (401) {String} error Unauthorized
651 * @apiError (500) {String} error Failed to get contracts
652 */
653router.get('/contracts', async (req, res) => {
654 try {
655 const result = await pool.query(
656 `SELECT
657 contract_id, contract_name, description, billing_interval, amount,
658 start_date, end_date, next_billing_date, status, created_at
659 FROM contracts
660 WHERE customer_id = $1 AND tenant_id = $2
661 ORDER BY created_at DESC`,
662 [req.customerId, req.tenantId]
663 );
664
665 res.json({ contracts: result.rows });
666 } catch (error) {
667 console.error('Get customer contracts error:', error);
668 res.status(500).json({ error: 'Failed to get contracts.', details: error.message });
669 }
670});
671
672/**
673 * @api {get} /portal/customer/contracts/:id Get Contract Details
674 * @apiName GetContractDetails
675 * @apiGroup CustomerPortalData
676 * @apiVersion 1.0.0
677 *
678 * @apiDescription
679 * Returns detailed information for specific contract.
680 *
681 * @apiPermission customer
682 *
683 * @apiHeader {String} Authorization Bearer JWT token
684 *
685 * @apiParam {Number} id Contract ID (URL parameter)
686 *
687 * @apiSuccess {Object} contract Contract object with all fields
688 *
689 * @apiError (401) {String} error Unauthorized
690 * @apiError (404) {String} error Contract not found
691 * @apiError (500) {String} error Failed to get contract details
692 */
693router.get('/contracts/:id', async (req, res) => {
694 try {
695 const contractId = parseInt(req.params.id);
696
697 const result = await pool.query(
698 `SELECT * FROM contracts
699 WHERE contract_id = $1 AND customer_id = $2 AND tenant_id = $3`,
700 [contractId, req.customerId, req.tenantId]
701 );
702
703 if (result.rows.length === 0) {
704 return res.status(404).json({ error: 'Contract not found.' });
705 }
706
707 res.json(result.rows[0]);
708 } catch (error) {
709 console.error('Get contract details error:', error);
710 res.status(500).json({ error: 'Failed to get contract details.', details: error.message });
711 }
712});
713
714/**
715 * @api {get} /portal/customer/assets List Customer Assets
716 * @apiName ListCustomerAssets
717 * @apiGroup CustomerPortalData
718 * @apiVersion 1.0.0
719 *
720 * @apiDescription
721 * Returns list of all customer assets/equipment.
722 *
723 * @apiPermission customer
724 *
725 * @apiHeader {String} Authorization Bearer JWT token
726 *
727 * @apiSuccess {Object[]} assets Array of assets
728 * @apiSuccess {Number} assets.asset_id Asset ID
729 * @apiSuccess {String} assets.asset_name Asset name
730 * @apiSuccess {String} assets.asset_type Asset type
731 * @apiSuccess {String} assets.serial_number Serial number
732 * @apiSuccess {String} assets.location Physical location
733 * @apiSuccess {Date} assets.purchase_date Purchase date
734 * @apiSuccess {Date} assets.warranty_expiry Warranty expiration date
735 * @apiSuccess {String} assets.status Status (Active, Retired, etc.)
736 * @apiSuccess {Date} assets.created_at Creation date
737 *
738 * @apiError (401) {String} error Unauthorized
739 * @apiError (500) {String} error Failed to get assets
740 */
741router.get('/assets', async (req, res) => {
742 try {
743 const result = await pool.query(
744 `SELECT
745 asset_id, asset_name, asset_type, serial_number, location,
746 purchase_date, warranty_expiry, status, created_at
747 FROM assets
748 WHERE customer_id = $1 AND tenant_id = $2
749 ORDER BY created_at DESC`,
750 [req.customerId, req.tenantId]
751 );
752
753 res.json({ assets: result.rows });
754 } catch (error) {
755 console.error('Get customer assets error:', error);
756 res.status(500).json({ error: 'Failed to get assets.', details: error.message });
757 }
758});
759
760/**
761 * @api {get} /portal/customer/documents List Customer Documents
762 * @apiName ListCustomerDocuments
763 * @apiGroup CustomerPortalData
764 * @apiVersion 1.0.0
765 *
766 * @apiDescription
767 * Returns list of all customer documents.
768 *
769 * @apiPermission customer
770 *
771 * @apiHeader {String} Authorization Bearer JWT token
772 *
773 * @apiSuccess {Object[]} documents Array of documents
774 * @apiSuccess {Number} documents.document_id Document ID
775 * @apiSuccess {String} documents.document_name Document name
776 * @apiSuccess {String} documents.document_type Document type
777 * @apiSuccess {String} documents.file_path File path
778 * @apiSuccess {Number} documents.file_size File size (bytes)
779 * @apiSuccess {String} documents.uploaded_by Uploader name
780 * @apiSuccess {Date} documents.created_at Upload date
781 *
782 * @apiError (401) {String} error Unauthorized
783 * @apiError (500) {String} error Failed to get documents
784 */
785router.get('/documents', async (req, res) => {
786 try {
787 const result = await pool.query(
788 `SELECT
789 document_id, document_name, document_type, file_path, file_size,
790 uploaded_by, created_at
791 FROM documents
792 WHERE customer_id = $1 AND tenant_id = $2
793 ORDER BY created_at DESC`,
794 [req.customerId, req.tenantId]
795 );
796
797 res.json({ documents: result.rows });
798 } catch (error) {
799 console.error('Get customer documents error:', error);
800 res.status(500).json({ error: 'Failed to get documents.', details: error.message });
801 }
802});
803
804/**
805 * @api {get} /portal/customer/dashboard Get Dashboard Overview
806 * @apiName GetCustomerDashboard
807 * @apiGroup CustomerPortalData
808 * @apiVersion 1.0.0
809 *
810 * @apiDescription
811 * Returns dashboard overview with statistics and recent activity.
812 * Aggregates data from invoices, contracts, tickets, and assets.
813 *
814 * @apiPermission customer
815 *
816 * @apiHeader {String} Authorization Bearer JWT token
817 *
818 * @apiSuccess {Object} stats Statistics object
819 * @apiSuccess {Number} stats.outstandingBalance Total unpaid invoices
820 * @apiSuccess {Number} stats.activeContracts Count of active contracts
821 * @apiSuccess {Number} stats.openTickets Count of open/in-progress tickets
822 * @apiSuccess {Number} stats.totalAssets Count of total assets
823 * @apiSuccess {Object[]} recentInvoices 5 most recent invoices
824 * @apiSuccess {Object[]} recentTickets 5 most recent tickets
825 *
826 * @apiError (401) {String} error Unauthorized
827 * @apiError (500) {String} error Failed to get dashboard data
828 */
829router.get('/dashboard', async (req, res) => {
830 try {
831 // Get outstanding balance
832 const balanceResult = await pool.query(
833 `SELECT COALESCE(SUM(total), 0) as outstanding_balance
834 FROM invoices
835 WHERE customer_id = $1 AND tenant_id = $2 AND status = 'Unpaid'`,
836 [req.customerId, req.tenantId]
837 );
838
839 // Get active contracts count
840 const contractsResult = await pool.query(
841 `SELECT COUNT(*) as active_contracts
842 FROM contracts
843 WHERE customer_id = $1 AND tenant_id = $2 AND status = 'Active'`,
844 [req.customerId, req.tenantId]
845 );
846
847 // Get open tickets count
848 const ticketsResult = await pool.query(
849 `SELECT COUNT(*) as open_tickets
850 FROM tickets
851 WHERE customer_id = $1 AND tenant_id = $2 AND status IN ('Open', 'In Progress')`,
852 [req.customerId, req.tenantId]
853 );
854
855 // Get total assets count
856 const assetsResult = await pool.query(
857 `SELECT COUNT(*) as total_assets
858 FROM assets
859 WHERE customer_id = $1 AND tenant_id = $2`,
860 [req.customerId, req.tenantId]
861 );
862
863 // Get recent invoices
864 const recentInvoicesResult = await pool.query(
865 `SELECT invoice_id, invoice_number, invoice_date, total, status
866 FROM invoices
867 WHERE customer_id = $1 AND tenant_id = $2
868 ORDER BY invoice_date DESC
869 LIMIT 5`,
870 [req.customerId, req.tenantId]
871 );
872
873 // Get recent tickets
874 const recentTicketsResult = await pool.query(
875 `SELECT ticket_id, ticket_number, subject, status, created_at
876 FROM tickets
877 WHERE customer_id = $1 AND tenant_id = $2
878 ORDER BY created_at DESC
879 LIMIT 5`,
880 [req.customerId, req.tenantId]
881 );
882
883 res.json({
884 stats: {
885 outstandingBalance: parseFloat(balanceResult.rows[0].outstanding_balance),
886 activeContracts: parseInt(contractsResult.rows[0].active_contracts),
887 openTickets: parseInt(ticketsResult.rows[0].open_tickets),
888 totalAssets: parseInt(assetsResult.rows[0].total_assets)
889 },
890 recentInvoices: recentInvoicesResult.rows,
891 recentTickets: recentTicketsResult.rows
892 });
893 } catch (error) {
894 console.error('Get customer dashboard error:', error);
895 res.status(500).json({ error: 'Failed to get dashboard data.', details: error.message });
896 }
897});
898
899module.exports = router;