UNPKG

self-serve-integration-service

Version:

Self-Serve Integration Service for managing multiple funder integrations including REST APIs, SOAP APIs, and UI automation

722 lines (682 loc) 22 kB
import { Router, Request, Response, NextFunction } from 'express'; import { body, param, validationResult } from 'express-validator'; import { FunderManagementService } from '../services/funderManagementService'; import authMiddleware from '../middleware/authMiddleware'; import { asyncHandler } from '../middleware/errorHandler'; import { logger } from '../utils/logger'; const router = Router(); const funderService = new FunderManagementService(); // Note: Authentication is applied selectively per route // GET routes are public, POST/PUT/DELETE are protected // Validation middleware const validateCreateFunder = [ body('name').trim().isLength({ min: 1 }).withMessage('Name is required'), body('code').trim().isLength({ min: 1, max: 10 }).withMessage('Code must be 1-10 characters'), body('displayName').trim().isLength({ min: 1 }).withMessage('Display name is required'), body('integrationType') .isIn(['REST_API', 'SOAP_API', 'UI_AUTOMATION']) .withMessage('Invalid integration type'), body('uiAutomationType') .optional() .isIn(['UIPATH', 'PLAYWRIGHT', 'CYPRESS']) .withMessage('Invalid UI automation type'), body('baseUrl').optional().isURL().withMessage('Invalid base URL'), body('timeout') .optional() .isInt({ min: 1000, max: 300000 }) .withMessage('Timeout must be between 1-300 seconds'), body('priority') .optional() .isInt({ min: 1, max: 1000 }) .withMessage('Priority must be between 1-1000'), body('supportedQuoteTypes') .isArray({ min: 1 }) .withMessage('At least one quote type is required'), body('supportedProposalTypes') .isArray({ min: 1 }) .withMessage('At least one proposal type is required'), body('credentials').isArray({ min: 1 }).withMessage('At least one credential is required'), body('credentials.*.name').trim().isLength({ min: 1 }).withMessage('Credential name is required'), body('credentials.*.value') .trim() .isLength({ min: 1 }) .withMessage('Credential value is required'), body('credentials.*.environment') .isIn(['SANDBOX', 'LIVE']) .withMessage('Invalid credential environment'), ]; const validateUpdateFunder = [ body('name').optional().trim().isLength({ min: 1 }).withMessage('Name cannot be empty'), body('displayName') .optional() .trim() .isLength({ min: 1 }) .withMessage('Display name cannot be empty'), body('integrationType') .optional() .isIn(['REST_API', 'SOAP_API', 'UI_AUTOMATION']) .withMessage('Invalid integration type'), body('uiAutomationType') .optional() .isIn(['UIPATH', 'PLAYWRIGHT', 'CYPRESS']) .withMessage('Invalid UI automation type'), body('baseUrl').optional().isURL().withMessage('Invalid base URL'), body('timeout') .optional() .isInt({ min: 1000, max: 300000 }) .withMessage('Timeout must be between 1-300 seconds'), body('priority') .optional() .isInt({ min: 1, max: 1000 }) .withMessage('Priority must be between 1-1000'), ]; const validateCredential = [ body('name').trim().isLength({ min: 1 }).withMessage('Credential name is required'), body('value').trim().isLength({ min: 1 }).withMessage('Credential value is required'), body('environment').isIn(['SANDBOX', 'LIVE']).withMessage('Invalid credential environment'), body('isEncrypted').optional().isBoolean().withMessage('isEncrypted must be boolean'), body('expiresAt').optional().isISO8601().withMessage('Invalid expiry date'), ]; // Helper function to handle validation errors const handleValidationErrors = (req: Request, res: Response, next: NextFunction): void => { const errors = validationResult(req); if (!errors.isEmpty()) { res.status(400).json({ success: false, error: 'Validation failed', details: errors.array(), }); return; } next(); }; /** * @swagger * /api/funders: * post: * summary: Create a new funder * description: Creates a new funder with the specified integration type and credentials * tags: [Funders] * security: * - bearerAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - name * - code * - displayName * - integrationType * - supportedQuoteTypes * - supportedProposalTypes * - credentials * properties: * name: * type: string * description: Unique funder name * example: "Alphabet Fleet Services" * code: * type: string * description: Short funder code (1-10 characters) * example: "ALPH" * displayName: * type: string * description: Human-readable display name * example: "Alphabet Fleet Services" * description: * type: string * description: Optional description * example: "Leading vehicle leasing and fleet management company" * website: * type: string * format: uri * description: Funder website URL * example: "https://www.alphabet.co.uk" * integrationType: * type: string * enum: [REST_API, SOAP_API, UI_AUTOMATION] * description: Type of integration * example: "REST_API" * uiAutomationType: * type: string * enum: [UIPATH, PLAYWRIGHT, CYPRESS] * description: UI automation tool (required if integrationType is UI_AUTOMATION) * example: "PLAYWRIGHT" * baseUrl: * type: string * format: uri * description: Base URL for API calls * example: "https://api.alphabet.co.uk" * timeout: * type: integer * minimum: 1000 * maximum: 300000 * description: Request timeout in milliseconds * example: 30000 * priority: * type: integer * minimum: 1 * maximum: 1000 * description: Priority for funder selection (lower = higher priority) * example: 100 * supportedQuoteTypes: * type: array * items: * type: string * description: Supported quote types * example: ["PERSONAL_CONTRACT_PURCHASE", "BUSINESS_CONTRACT_HIRE"] * supportedProposalTypes: * type: array * items: * type: string * description: Supported proposal types * example: ["PERSONAL_CONTRACT_PURCHASE", "BUSINESS_CONTRACT_HIRE"] * credentials: * type: array * items: * type: object * required: * - name * - value * - environment * properties: * name: * type: string * description: Credential name * example: "API Key" * value: * type: string * description: Credential value * example: "abc123xyz789" * environment: * type: string * enum: [SANDBOX, LIVE] * description: Environment for this credential * example: "LIVE" * isEncrypted: * type: boolean * description: Whether the credential is encrypted * default: true * expiresAt: * type: string * format: date-time * description: When the credential expires * example: "2024-12-31T23:59:59Z" * responses: * 201: * description: Funder created successfully * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * example: true * data: * $ref: '#/components/schemas/Funder' * message: * type: string * example: "Funder created successfully" * 400: * description: Validation error or business logic error * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Unauthorized - JWT token required * 403: * description: Forbidden - Admin role required * 500: * description: Internal server error */ router.post( '/', authMiddleware, validateCreateFunder, handleValidationErrors, asyncHandler(async (req, res) => { try { const funder = await funderService.createFunder(req.body); res.status(201).json({ success: true, data: funder, message: 'Funder created successfully', }); } catch (error) { logger.error('Error creating funder:', error); res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Failed to create funder', }); } }) ); /** * @swagger * /api/funders: * get: * summary: Get all funders * description: Retrieves a list of all funders with optional filtering (public access) * tags: [Funders] * parameters: * - in: query * name: status * schema: * type: string * enum: [ACTIVE, INACTIVE, MAINTENANCE, DEPRECATED] * description: Filter by funder status * - in: query * name: integrationType * schema: * type: string * enum: [REST_API, SOAP_API, UI_AUTOMATION] * description: Filter by integration type * - in: query * name: active * schema: * type: boolean * description: Filter by active status (true = ACTIVE, false = not ACTIVE) * responses: * 200: * description: List of funders retrieved successfully * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * example: true * data: * type: array * items: * $ref: '#/components/schemas/Funder' * count: * type: integer * description: Number of funders returned * example: 5 * 401: * description: Unauthorized - JWT token required * 500: * description: Internal server error */ router.get( '/', asyncHandler(async (req, res) => { try { const filters: Record<string, unknown> = {}; if (req.query.status) { filters.status = req.query.status; } if (req.query.integrationType) { filters.integrationType = req.query.integrationType; } if (req.query.active !== undefined) { filters.active = req.query.active === 'true'; } const funders = await funderService.getFunders(filters); res.json({ success: true, data: funders, count: funders.length, }); } catch (error) { logger.error('Error getting funders:', error); res.status(500).json({ success: false, error: 'Failed to retrieve funders', }); } }) ); /** * @swagger * /api/funders/{id}: * get: * summary: Get funder by ID * description: Retrieves a specific funder by their unique ID (public access) * tags: [Funders] * parameters: * - in: path * name: id * required: true * schema: * type: string * format: uuid * description: Funder ID * example: "123e4567-e89b-12d3-a456-426614174000" * responses: * 200: * description: Funder retrieved successfully * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * example: true * data: * $ref: '#/components/schemas/Funder' * 400: * description: Invalid funder ID format * 401: * description: Unauthorized - JWT token required * 404: * description: Funder not found * 500: * description: Internal server error */ router.get( '/:id', param('id').isUUID().withMessage('Invalid funder ID'), handleValidationErrors, asyncHandler(async (req, res) => { try { const funder = await funderService.getFunderById(req.params.id); if (!funder) { return res.status(404).json({ success: false, error: 'Funder not found', }); } return res.json({ success: true, data: funder, }); } catch (error) { logger.error('Error getting funder by ID:', error); return res.status(500).json({ success: false, error: 'Failed to retrieve funder', }); } }) ); /** * @route GET /api/funders/code/:code * @desc Get funder by code * @access Public */ router.get( '/code/:code', param('code').trim().isLength({ min: 1, max: 10 }).withMessage('Invalid funder code'), handleValidationErrors, asyncHandler(async (req, res) => { try { const funder = await funderService.getFunderByCode(req.params.code); if (!funder) { return res.status(404).json({ success: false, error: 'Funder not found', }); } return res.json({ success: true, data: funder, }); } catch (error) { logger.error('Error getting funder by code:', error); return res.status(500).json({ success: false, error: 'Failed to retrieve funder', }); } }) ); /** * @route PUT /api/funders/:id * @desc Update funder * @access Private (Admin only) */ router.put( '/:id', authMiddleware, param('id').isUUID().withMessage('Invalid funder ID'), validateUpdateFunder, handleValidationErrors, asyncHandler(async (req, res) => { try { const funder = await funderService.updateFunder(req.params.id, req.body); res.json({ success: true, data: funder, message: 'Funder updated successfully', }); } catch (error) { logger.error('Error updating funder:', error); res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Failed to update funder', }); } }) ); /** * @route DELETE /api/funders/:id * @desc Delete funder (soft delete) * @access Private (Admin only) */ router.delete( '/:id', authMiddleware, param('id').isUUID().withMessage('Invalid funder ID'), handleValidationErrors, asyncHandler(async (req, res) => { try { await funderService.deleteFunder(req.params.id); res.json({ success: true, message: 'Funder deleted successfully', }); } catch (error) { logger.error('Error deleting funder:', error); res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Failed to delete funder', }); } }) ); /** * @route POST /api/funders/:id/credentials * @desc Add credentials to funder * @access Private (Admin only) */ router.post( '/:id/credentials', authMiddleware, param('id').isUUID().withMessage('Invalid funder ID'), body('credentials').isArray({ min: 1 }).withMessage('At least one credential is required'), body('credentials.*.name').trim().isLength({ min: 1 }).withMessage('Credential name is required'), body('credentials.*.value') .trim() .isLength({ min: 1 }) .withMessage('Credential value is required'), body('credentials.*.environment') .isIn(['SANDBOX', 'LIVE']) .withMessage('Invalid credential environment'), handleValidationErrors, asyncHandler(async (req, res) => { try { const credentials = await funderService.addCredentials(req.params.id, req.body.credentials); res.status(201).json({ success: true, data: credentials, message: 'Credentials added successfully', }); } catch (error) { logger.error('Error adding credentials:', error); res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Failed to add credentials', }); } }) ); /** * @route PUT /api/funders/credentials/:credentialId * @desc Update credential * @access Private (Admin only) */ router.put( '/credentials/:credentialId', authMiddleware, param('credentialId').isUUID().withMessage('Invalid credential ID'), validateCredential, handleValidationErrors, asyncHandler(async (req, res) => { try { const credential = await funderService.updateCredential(req.params.credentialId, req.body); res.json({ success: true, data: credential, message: 'Credential updated successfully', }); } catch (error) { logger.error('Error updating credential:', error); res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Failed to update credential', }); } }) ); /** * @route DELETE /api/funders/credentials/:credentialId * @desc Delete credential * @access Private (Admin only) */ router.delete( '/credentials/:credentialId', authMiddleware, param('credentialId').isUUID().withMessage('Invalid credential ID'), handleValidationErrors, asyncHandler(async (req, res) => { try { await funderService.deleteCredential(req.params.credentialId); res.json({ success: true, message: 'Credential deleted successfully', }); } catch (error) { logger.error('Error deleting credential:', error); res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Failed to delete credential', }); } }) ); /** * @route POST /api/funders/:id/test-connection * @desc Test connection to funder * @access Private (Admin only) */ router.post( '/:id/test-connection', authMiddleware, param('id').isUUID().withMessage('Invalid funder ID'), body('environment').optional().isIn(['SANDBOX', 'LIVE']).withMessage('Invalid environment'), handleValidationErrors, asyncHandler(async (req, res) => { try { const environment = req.body.environment || 'LIVE'; const success = await funderService.testFunderConnection(req.params.id, environment); res.json({ success: true, data: { funderId: req.params.id, environment, connectionSuccessful: success, }, message: success ? 'Connection test successful' : 'Connection test failed', }); } catch (error) { logger.error('Error testing funder connection:', error); res.status(500).json({ success: false, error: 'Failed to test funder connection', }); } }) ); /** * @route POST /api/funders/:id/validate * @desc Validate funder configuration * @access Private (Admin only) */ router.post( '/:id/validate', authMiddleware, param('id').isUUID().withMessage('Invalid funder ID'), handleValidationErrors, asyncHandler(async (req, res) => { try { const errors = await funderService.validateFunderConfiguration(req.params.id); res.json({ success: true, data: { funderId: req.params.id, isValid: errors.length === 0, errors, }, message: errors.length === 0 ? 'Configuration is valid' : 'Configuration has errors', }); } catch (error) { logger.error('Error validating funder configuration:', error); res.status(500).json({ success: false, error: 'Failed to validate funder configuration', }); } }) ); /** * @route GET /api/funders/:id/performance * @desc Get funder performance metrics * @access Public */ router.get( '/:id/performance', param('id').isUUID().withMessage('Invalid funder ID'), handleValidationErrors, asyncHandler(async (req, res) => { try { const funder = await funderService.getFunderById(req.params.id); if (!funder) { return res.status(404).json({ success: false, error: 'Funder not found', }); } // Calculate performance metrics const metrics = { funderId: funder.id, name: funder.name, code: funder.code, responseTime: funder.responseTime, successRate: funder.successRate, lastIntegration: funder.lastIntegration, status: funder.status, priority: funder.priority, }; return res.json({ success: true, data: metrics, }); } catch (error) { logger.error('Error getting funder performance:', error); return res.status(500).json({ success: false, error: 'Failed to retrieve funder performance', }); } }) ); export default router;