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
text/typescript
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;