UNPKG

self-serve-integration-service

Version:

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

1,120 lines (1,076 loc) 42 kB
import { Router, Request, Response } from 'express'; import { body, param, validationResult } from 'express-validator'; import { asyncHandler } from '../middleware/errorHandler'; import { logger } from '../utils/logger'; import { IntegrationService } from '../services/integrationService'; import { QuoteRequest, ProposalRequest } from '../types/integration'; import { QuoteSource } from '@prisma/client'; const router = Router(); const integrationService = new IntegrationService(); /** * @swagger * tags: * name: Integration API * description: Common API for calculating quotes and generating proposals across multiple funders */ /** * @swagger * /api/integration/quotes/calculate: * post: * summary: Calculate a vehicle finance quote * description: | * Calculates a vehicle finance quote using the specified funder (e.g., LEX, ALPHABET) based on common customer, vehicle, and contract details. * * **📊 Quick Summary:** * - **Required Fields:** Only 9 fields mandatory * - **Customer Info:** firstName, lastName, email (phone & address optional) * - **Vehicle Info:** capId, capCode, year only * - **Response Time:** ~2-5 seconds * - **Authentication:** Bearer token (currently disabled for testing) * * **🔄 Process Flow:** * 1. Creates a QuoteRequest record with PENDING status * 2. Calls the specified funder API (LEX or ALPHABET) * 3. Updates QuoteRequest status to PROCESSING * 4. On success: Creates Quote record and updates QuoteRequest to SUCCESS * 5. On failure: Updates QuoteRequest to FAILED and retries up to 3 times * 6. All interactions are logged in IntegrationLog * * --- * * **✅ Mandatory Fields (9 Total):** * - ✅ `funderName` - Funder identifier (e.g., "LEX", "ALPHABET") * - ✅ `environment` - "SANDBOX" or "LIVE" * - ✅ `customerDetails.firstName` - Customer first name * - ✅ `customerDetails.lastName` - Customer last name * - ✅ `customerDetails.email` - Valid email address * - ✅ `vehicleDetails.capId` - CAP vehicle ID * - ✅ `vehicleDetails.capCode` - CAP vehicle code * - ✅ `vehicleDetails.year` - Vehicle year (1900-2030) * - ✅ `contractDetails.funderSpecificParams` - LEX-specific parameters (financials & adjustments) * * **Optional Fields:** * - ⚪ `source` - Quote source identifier (defaults to SELF_SERVE_APP) * - ⚪ `customerDetails.phone` - Phone number * - ⚪ `customerDetails.dateOfBirth` - Date of birth * - ⚪ `customerDetails.address` - **ENTIRE OBJECT IS OPTIONAL** (line1, line2, city, postcode, country) * - ⚪ `customerDetails.employment` - Employment details (employerName, jobTitle, employmentType, monthlyIncome) * - ⚪ `vehicleDetails` (optional fields beyond capId, capCode, year): * - `make`, `model`, `variant` - Vehicle description * - `price`, `mileage` - Pricing and usage * - `fuelType`, `transmission`, `bodyType` - Specifications * - `doors`, `seats`, `engineSize`, `power` - Physical attributes * - `co2Emissions`, `color` - Environmental and appearance * - `options`, `accessories` - Additional features * - ⚪ `contractDetails` (optional if using funderSpecificParams): * - `quoteType`, `term`, `deposit`, `annualMileage` - Basic contract info * - `balloonPayment`, `maintenanceIncluded`, `insuranceIncluded` - Additional terms * - ⚪ `metadata` - Additional metadata object * * **Funder-Specific Parameters (Optional but Recommended for LEX):** * * Use `contractDetails.funderSpecificParams` to pass LEX-specific values. If not provided, defaults will be used: * * **LEX Funder Parameters:** * - `contract_type_id` (number, default: 3): Contract type (3 = Contract Hire) * - `plan` (number, default: 106): LEX plan number * - `term` (number): Contract term (uses contractDetails.term if not specified) * - `mileage` (number): Annual mileage (uses contractDetails.annualMileage if not specified) * - `relief_vehicle` (number, default: 1): Maintenance included (0 = No, 1 = Yes) ⚠️ **Must be number (0 or 1), NOT boolean** * - `customer_initial_payment` (number): Initial deposit (uses contractDetails.deposit if not specified) * - `off_invoice_support` (number, default: 0): Off-invoice support amount * - `otrp` (number, default: 0): On-the-road price * - `commission` (number, default: 0): Commission amount * - `discount` (number, default: 0): Discount amount * - `customer_terms` (number, default: 1): Customer terms (0 or 1) ⚠️ **Must be number (0 or 1), NOT boolean** * - `co2_emission` (number): CO2 emissions (uses vehicleDetails.co2Emissions if not specified) * * **Critical Notes:** * - ⚠️ For LEX: `relief_vehicle` and `customer_terms` MUST be numeric (0 or 1), NOT boolean * - 💰 All monetary values should be numbers with decimals (e.g., 2000.00) * - 🚗 Both `capId` and `capCode` are mandatory for vehicle identification * tags: [Integration API] * requestBody: * required: true * content: * application/json: * schema: * $ref: '#/components/schemas/CommonQuoteRequest' * examples: * minimal: * summary: Minimal Request (9 Required Fields Only) * description: | * This example shows the absolute minimum required fields to generate a LEX quote. * Only 9 fields are mandatory: funderName, environment, 3 customer fields, 3 vehicle fields, and funderSpecificParams. * value: * funderName: "LEX" * environment: "SANDBOX" * customerDetails: * firstName: "John" * lastName: "Doe" * email: "john.doe@example.com" * vehicleDetails: * capId: "105841" * capCode: "KISP16SHD5EHTA 6" * year: 2025 * contractDetails: * funderSpecificParams: * contract_type_id: 3 * plan: 106 * term: 12 * mileage: 10000 * relief_vehicle: 1 * customer_initial_payment: 2000.00 * off_invoice_support: 500.00 * otrp: 25000.00 * commission: 200.00 * discount: 10.00 * customer_terms: 1 * co2_emission: 145 * complete: * summary: Complete Request (With Optional Fields) * description: | * This example shows a complete request including optional fields like phone, address, and additional vehicle details. * These optional fields will be stored but are not required for quote generation. * value: * funderName: "LEX" * environment: "SANDBOX" * customerDetails: * firstName: "John" * lastName: "Doe" * email: "john.doe@example.com" * phone: "+447700900123" * dateOfBirth: "1990-01-15" * address: * line1: "123 Test Street" * city: "London" * postcode: "SW1A 1AA" * country: "UK" * vehicleDetails: * capId: "105841" * capCode: "KISP16SHD5EHTA 6" * make: "Kia" * model: "Sportage" * variant: "Estate Special Editions 1.6T GDi HEV Shadow 5dr Auto" * year: 2025 * price: 25000.00 * fuelType: "Hybrid" * transmission: "Automatic" * co2Emissions: 145 * contractDetails: * funderSpecificParams: * contract_type_id: 3 * plan: 106 * term: 12 * mileage: 10000 * relief_vehicle: 1 * customer_initial_payment: 2000.00 * off_invoice_support: 500.00 * otrp: 25000.00 * commission: 200.00 * discount: 10.00 * customer_terms: 1 * co2_emission: 145 * metadata: * source: "swagger_ui" * testRun: true * responses: * 200: * description: Quote calculated successfully * content: * application/json: * schema: * allOf: * - $ref: '#/components/schemas/SuccessResponse' * - type: object * properties: * data: * $ref: '#/components/schemas/CommonQuoteResponse' * example: * success: true * message: "Quote calculated successfully" * data: * id: "5df06713-1b53-42c1-ba2d-fd6edbcf844b" * funderName: "LEX" * status: "GENERATED" * monthlyPayment: 170.24 * totalAmount: 2042.88 * apr: 0 * balloonPayment: 0 * externalQuoteId: "1344018037" * externalReference: "-235304123/00001" * validUntil: "2025-11-14T07:02:09.795Z" * createdAt: "2025-10-15T07:02:09.798Z" * updatedAt: "2025-10-15T07:02:09.798Z" * funderResponseData: * quote_id: 1344018037 * quote_reference: "-235304123/00001" * vehicle_description: "SPORTAGE ESTATE SPECIAL EDITIONS 1.6T GDi HEV Shadow 5dr Auto 25" * monthly_rental: * amount: 170.24 * vat_rule_applies: true * initial_payment: * amount: 2000 * vat_rule_applies: true * 400: * description: Bad request - Validation failed * content: * application/json: * example: * success: false * error: "Validation failed" * details: * - msg: "Customer first name is required" * param: "customerDetails.firstName" * location: "body" * 500: * description: Internal server error * content: * application/json: * example: * success: false * error: "Failed to calculate quote" * details: "Error details here" */ router.post( '/quotes/calculate', [ // Mandatory root fields body('funderName').notEmpty().isString().withMessage('Funder name is required'), body('environment') .isIn(['SANDBOX', 'LIVE']) .withMessage('Environment must be SANDBOX or LIVE'), // Customer details - REQUIRED body('customerDetails').isObject().withMessage('Customer details are required'), body('customerDetails.firstName') .notEmpty() .isString() .withMessage('Customer first name is required'), body('customerDetails.lastName') .notEmpty() .isString() .withMessage('Customer last name is required'), body('customerDetails.email').isEmail().withMessage('Valid customer email is required'), // Customer details - OPTIONAL fields body('customerDetails.phone').optional().isString(), body('customerDetails.dateOfBirth').optional().isString(), body('customerDetails.address').optional().isObject(), // Vehicle details - ONLY cap_id, cap_code, and model_year are mandatory for LEX body('vehicleDetails').isObject().withMessage('Vehicle details are required'), body('vehicleDetails.capId').notEmpty().withMessage('CAP ID is required'), body('vehicleDetails.capCode').notEmpty().withMessage('CAP Code is required'), body('vehicleDetails.year') .isInt({ min: 1900, max: 2030 }) .withMessage('Valid vehicle year is required'), // Contract details - now rely on funderSpecificParams for LEX body('contractDetails').isObject().withMessage('Contract details are required'), body('contractDetails.funderSpecificParams').optional().isObject(), body('source') .optional() .isIn(Object.values(QuoteSource)) .withMessage('Valid quote source is required'), ], asyncHandler(async (req: Request, res: Response) => { const errors = validationResult(req); if (!errors.isEmpty()) { logger.warn('Validation errors for calculate quote request', { errors: errors.array() }); return res .status(400) .json({ success: false, error: 'Validation failed', details: errors.array() }); } const quoteRequest: QuoteRequest = req.body; try { const quoteResponse = await integrationService.generateQuote(quoteRequest); return res .status(200) .json({ success: true, data: quoteResponse, message: 'Quote calculated successfully' }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorStack = error instanceof Error ? error.stack : undefined; logger.error('Error calculating quote:', { error: errorMessage, stack: errorStack }); return res .status(500) .json({ success: false, error: 'Failed to calculate quote', details: errorMessage }); } }) ); /** * @swagger * /api/integration/quotes/{quoteId}/status: * get: * summary: Get quote status * description: Retrieves the current status and details of a specific quote * tags: [Integration API] * parameters: * - in: path * name: quoteId * required: true * schema: * type: string * format: uuid * example: "123e4567-e89b-12d3-a456-426614174000" * description: Internal Quote ID * responses: * 200: * description: Quote status retrieved successfully * content: * application/json: * schema: * allOf: * - $ref: '#/components/schemas/SuccessResponse' * - type: object * properties: * data: * $ref: '#/components/schemas/CommonQuoteResponse' * 404: * $ref: '#/components/responses/NotFound' * 500: * $ref: '#/components/responses/InternalServerError' */ router.get( '/quotes/:quoteId/status', [param('quoteId').isUUID().withMessage('Valid quote ID is required')], asyncHandler(async (req: Request, res: Response) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res .status(400) .json({ success: false, error: 'Validation failed', details: errors.array() }); } const { quoteId } = req.params; try { const quote = await integrationService.getQuoteDetails(quoteId); return res.status(200).json({ success: true, data: quote, message: 'Quote details retrieved successfully', }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('Error retrieving quote details:', { error: errorMessage, quoteId }); if (errorMessage === 'Quote not found') { return res .status(404) .json({ success: false, error: 'Quote not found', details: errorMessage }); } return res .status(500) .json({ success: false, error: 'Failed to retrieve quote details', details: errorMessage }); } }) ); /** * @swagger * /api/integration/proposals/generate: * post: * summary: Generate a vehicle finance proposal * description: | * Generates a vehicle finance proposal for an existing quote using the specified funder. * * **Process Flow:** * 1. Validates the quote exists and is in correct status * 2. Calls the funder API to submit the proposal * 3. Creates Proposal record with funder response * 4. Updates quote status if needed * 5. Logs all interactions * tags: [Integration API] * requestBody: * required: true * content: * application/json: * schema: * $ref: '#/components/schemas/CommonProposalRequest' * example: * quoteId: "123e4567-e89b-12d3-a456-426614174000" * customerDetails: * firstName: "John" * lastName: "Doe" * email: "john.doe@example.com" * phone: "+447700900000" * dateOfBirth: "1980-01-01" * address: * line1: "123 Main St" * city: "London" * postcode: "SW1A 0AA" * country: "UK" * employment: * employerName: "Acme Corp" * jobTitle: "Engineer" * employmentType: "FULL_TIME" * monthlyIncome: 3000 * correlationId: "prop-123456789" * responses: * 200: * description: Proposal generated successfully * content: * application/json: * schema: * allOf: * - $ref: '#/components/schemas/SuccessResponse' * - type: object * properties: * data: * $ref: '#/components/schemas/CommonProposalResponse' * 400: * $ref: '#/components/responses/BadRequest' * 404: * $ref: '#/components/responses/NotFound' * 500: * $ref: '#/components/responses/InternalServerError' */ router.post( '/proposals/generate', [ body('quoteId').isUUID().withMessage('Valid quote ID is required'), body('customerDetails').isObject().withMessage('Customer details are required'), body('customerDetails.firstName') .notEmpty() .isString() .withMessage('Customer first name is required'), body('customerDetails.lastName') .notEmpty() .isString() .withMessage('Customer last name is required'), body('customerDetails.email').isEmail().withMessage('Valid customer email is required'), body('customerDetails.address.line1').notEmpty().withMessage('Address line 1 is required'), body('customerDetails.address.city').notEmpty().withMessage('City is required'), body('customerDetails.address.postcode').notEmpty().withMessage('Postcode is required'), body('customerDetails.employment.employerName') .notEmpty() .withMessage('Employer name is required'), body('customerDetails.employment.jobTitle').notEmpty().withMessage('Job title is required'), body('customerDetails.employment.employmentType') .isIn(['FULL_TIME', 'PART_TIME', 'CONTRACT', 'SELF_EMPLOYED', 'UNEMPLOYED']) .withMessage('Valid employment type is required'), body('customerDetails.employment.monthlyIncome') .isFloat({ min: 0 }) .withMessage('Monthly income must be a positive number'), ], asyncHandler(async (req: Request, res: Response) => { const errors = validationResult(req); if (!errors.isEmpty()) { logger.warn('Validation errors for generate proposal request', { errors: errors.array() }); return res .status(400) .json({ success: false, error: 'Validation failed', details: errors.array() }); } const proposalRequest: ProposalRequest = req.body; try { const proposalResponse = await integrationService.generateProposal(proposalRequest); return res.status(200).json({ success: true, data: proposalResponse, message: 'Proposal generated successfully', }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorStack = error instanceof Error ? error.stack : undefined; logger.error('Error generating proposal:', { error: errorMessage, stack: errorStack }); return res .status(500) .json({ success: false, error: 'Failed to generate proposal', details: errorMessage }); } }) ); /** * @swagger * /api/integration/proposals/{proposalId}/status: * get: * summary: Get proposal status * description: Retrieves the current status and details of a specific proposal * tags: [Integration API] * parameters: * - in: path * name: proposalId * required: true * schema: * type: string * format: uuid * example: "456e7890-e89b-12d3-a456-426614174001" * description: Internal Proposal ID * responses: * 200: * description: Proposal status retrieved successfully * content: * application/json: * schema: * allOf: * - $ref: '#/components/schemas/SuccessResponse' * - type: object * properties: * data: * $ref: '#/components/schemas/CommonProposalResponse' * 404: * $ref: '#/components/responses/NotFound' * 500: * $ref: '#/components/responses/InternalServerError' */ router.get( '/proposals/:proposalId/status', [param('proposalId').isUUID().withMessage('Valid proposal ID is required')], asyncHandler(async (req: Request, res: Response) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res .status(400) .json({ success: false, error: 'Validation failed', details: errors.array() }); } const { proposalId } = req.params; try { // This would typically fetch from database return res.status(200).json({ success: true, data: { id: proposalId, status: 'SUBMITTED' }, message: 'Proposal status retrieved successfully', }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('Error retrieving proposal status:', { error: errorMessage, proposalId }); return res.status(500).json({ success: false, error: 'Failed to retrieve proposal status', details: errorMessage, }); } }) ); /** * @swagger * /api/integration/funders: * get: * summary: Get available funders * description: Retrieves a list of all available funders and their capabilities * tags: [Integration API] * responses: * 200: * description: Funders retrieved successfully * content: * application/json: * schema: * allOf: * - $ref: '#/components/schemas/SuccessResponse' * - type: object * properties: * data: * type: array * items: * $ref: '#/components/schemas/Funder' * 500: * $ref: '#/components/responses/InternalServerError' */ router.get( '/funders', asyncHandler(async (_req: Request, res: Response) => { try { // This would typically fetch from database const funders = [ { id: '789e0123-e89b-12d3-a456-426614174002', name: 'LEX', code: 'LEX', displayName: 'Lex Autolease', description: 'Lex Autolease vehicle finance and leasing services', website: 'https://www.lexautolease.co.uk', integrationType: 'REST_API', status: 'ACTIVE', priority: 1, supportedQuoteTypes: ['PERSONAL_CONTRACT_HIRE', 'BUSINESS_CONTRACT_HIRE'], supportedProposalTypes: ['PERSONAL_CONTRACT_HIRE', 'BUSINESS_CONTRACT_HIRE'], responseTime: 1200, successRate: 0.95, lastIntegration: '2024-08-26T10:20:54.000Z', createdAt: '2024-08-26T10:20:54.000Z', updatedAt: '2024-08-26T10:20:54.000Z', }, { id: '789e0123-e89b-12d3-a456-426614174003', name: 'ALPHABET', code: 'ALPHABET', displayName: 'Alphabet (BMW Group)', description: 'Alphabet vehicle finance and leasing services via Codeweavers API', website: 'https://www.alphabet.co.uk', integrationType: 'REST_API', status: 'ACTIVE', priority: 2, supportedQuoteTypes: ['PERSONAL_CONTRACT_HIRE', 'BUSINESS_CONTRACT_HIRE'], supportedProposalTypes: ['PERSONAL_CONTRACT_HIRE', 'BUSINESS_CONTRACT_HIRE'], responseTime: 1500, successRate: 0.92, lastIntegration: '2024-08-26T10:20:54.000Z', createdAt: '2024-08-26T10:20:54.000Z', updatedAt: '2024-08-26T10:20:54.000Z', }, ]; res .status(200) .json({ success: true, data: funders, message: 'Funders retrieved successfully' }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('Error retrieving funders:', { error: errorMessage }); res .status(500) .json({ success: false, error: 'Failed to retrieve funders', details: errorMessage }); } }) ); /** * @swagger * /api/integration/funders/{funderName}/test: * post: * summary: Test funder connection * description: Tests the connection to a specific funder in the specified environment * tags: [Integration API] * parameters: * - in: path * name: funderName * required: true * schema: * type: string * enum: [LEX, ALPHABET] * example: "LEX" * description: Name of the funder to test * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - environment * properties: * environment: * type: string * enum: [SANDBOX, LIVE] * example: "SANDBOX" * description: Environment to test (SANDBOX or LIVE) * responses: * 200: * description: Funder connection test completed * content: * application/json: * schema: * allOf: * - $ref: '#/components/schemas/SuccessResponse' * - type: object * properties: * data: * type: object * properties: * funderName: * type: string * example: "LEX" * environment: * type: string * example: "SANDBOX" * status: * type: string * enum: [healthy, unhealthy] * example: "healthy" * responseTime: * type: integer * example: 1200 * message: * type: string * example: "Connection successful" * 400: * $ref: '#/components/responses/BadRequest' * 500: * $ref: '#/components/responses/InternalServerError' */ router.post( '/funders/:funderName/test', [ param('funderName').isIn(['LEX', 'ALPHABET']).withMessage('Valid funder name is required'), body('environment') .isIn(['SANDBOX', 'LIVE']) .withMessage('Environment must be SANDBOX or LIVE'), ], asyncHandler(async (req: Request, res: Response) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res .status(400) .json({ success: false, error: 'Validation failed', details: errors.array() }); } const { funderName } = req.params; const { environment } = req.body; try { // This would typically test the actual funder connection const testResult = { funderName, environment, status: 'healthy', responseTime: 1200, message: 'Connection successful', }; return res .status(200) .json({ success: true, data: testResult, message: 'Funder connection test completed' }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('Error testing funder connection:', { error: errorMessage, funderName, environment, }); return res .status(500) .json({ success: false, error: 'Failed to test funder connection', details: errorMessage }); } }) ); /** * @swagger * /api/integration/customers/{email}/quotes: * get: * summary: Get customer quotes * description: Retrieves all quotes for a specific customer by email address * tags: [Integration API] * parameters: * - in: path * name: email * required: true * schema: * type: string * format: email * example: "john.doe@example.com" * description: Customer email address * - in: query * name: status * schema: * type: string * enum: [DRAFT, PENDING, APPROVED, DECLINED, EXPIRED, CANCELLED, GENERATED, CONVERTED, FAILED] * description: Filter by quote status * - in: query * name: funderName * schema: * type: string * enum: [LEX, ALPHABET] * description: Filter by funder name * - in: query * name: limit * schema: * type: integer * minimum: 1 * maximum: 100 * default: 20 * description: Number of quotes to return * - in: query * name: offset * schema: * type: integer * minimum: 0 * default: 0 * description: Number of quotes to skip * responses: * 200: * description: Customer quotes retrieved successfully * content: * application/json: * schema: * allOf: * - $ref: '#/components/schemas/SuccessResponse' * - type: object * properties: * data: * type: array * items: * $ref: '#/components/schemas/CommonQuoteResponse' * pagination: * type: object * properties: * total: * type: integer * example: 25 * limit: * type: integer * example: 20 * offset: * type: integer * example: 0 * hasMore: * type: boolean * example: true * 400: * $ref: '#/components/responses/BadRequest' * 500: * $ref: '#/components/responses/InternalServerError' */ router.get( '/customers/:email/quotes', [param('email').isEmail().withMessage('Valid email is required')], asyncHandler(async (req: Request, res: Response) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res .status(400) .json({ success: false, error: 'Validation failed', details: errors.array() }); } const { email } = req.params; const { limit = 20, offset = 0 } = req.query; try { // This would typically fetch from database with filters const quotes: unknown[] = []; const pagination = { total: 0, limit: parseInt(limit as string), offset: parseInt(offset as string), hasMore: false, }; return res.status(200).json({ success: true, data: quotes, pagination, message: 'Customer quotes retrieved successfully', }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('Error retrieving customer quotes:', { error: errorMessage, email }); return res.status(500).json({ success: false, error: 'Failed to retrieve customer quotes', details: errorMessage, }); } }) ); /** * @swagger * /api/integration/customers/{email}/proposals: * get: * summary: Get customer proposals * description: Retrieves all proposals for a specific customer by email address * tags: [Integration API] * parameters: * - in: path * name: email * required: true * schema: * type: string * format: email * example: "john.doe@example.com" * description: Customer email address * - in: query * name: status * schema: * type: string * enum: [DRAFT, SUBMITTED, UNDER_REVIEW, APPROVED, DECLINED, REFERRED, CANCELLED, COMPLETED, FAILED] * description: Filter by proposal status * - in: query * name: funderName * schema: * type: string * enum: [LEX, ALPHABET] * description: Filter by funder name * - in: query * name: limit * schema: * type: integer * minimum: 1 * maximum: 100 * default: 20 * description: Number of proposals to return * - in: query * name: offset * schema: * type: integer * minimum: 0 * default: 0 * description: Number of proposals to skip * responses: * 200: * description: Customer proposals retrieved successfully * content: * application/json: * schema: * allOf: * - $ref: '#/components/schemas/SuccessResponse' * - type: object * properties: * data: * type: array * items: * $ref: '#/components/schemas/CommonProposalResponse' * pagination: * type: object * properties: * total: * type: integer * example: 15 * limit: * type: integer * example: 20 * offset: * type: integer * example: 0 * hasMore: * type: boolean * example: false * 400: * $ref: '#/components/responses/BadRequest' * 500: * $ref: '#/components/responses/InternalServerError' */ router.get( '/customers/:email/proposals', [param('email').isEmail().withMessage('Valid email is required')], asyncHandler(async (req: Request, res: Response) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res .status(400) .json({ success: false, error: 'Validation failed', details: errors.array() }); } const { email } = req.params; const { limit = 20, offset = 0 } = req.query; try { // This would typically fetch from database with filters const proposals: unknown[] = []; const pagination = { total: 0, limit: parseInt(limit as string), offset: parseInt(offset as string), hasMore: false, }; return res.status(200).json({ success: true, data: proposals, pagination, message: 'Customer proposals retrieved successfully', }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('Error retrieving customer proposals:', { error: errorMessage, email }); return res.status(500).json({ success: false, error: 'Failed to retrieve customer proposals', details: errorMessage, }); } }) ); /** * @swagger * /api/integration/quotes/order/{orderReferenceId}: * get: * summary: Get quote details by order reference ID * description: | * Retrieves quote details using the order reference ID that was provided during quote creation. * This is useful for order management systems to retrieve quotes associated with specific orders. * * **Use Cases:** * - Order management system integration * - Tracking quotes by order reference * - Retrieving quotes without knowing the internal quote ID * * **Response includes:** * - Complete quote details (ID, status, payments, etc.) * - Customer information * - Vehicle details * - Funder response data * - Metadata including contract type information * tags: [Integration API] * parameters: * - in: path * name: orderReferenceId * required: true * schema: * type: string * example: "ORD-1760620296758-GATEWAY" * description: Order reference ID that was provided during quote creation * responses: * 200: * description: Quote details retrieved successfully * content: * application/json: * schema: * allOf: * - $ref: '#/components/schemas/SuccessResponse' * - type: object * properties: * data: * $ref: '#/components/schemas/CommonQuoteResponse' * example: * success: true * message: "Quote details retrieved successfully" * data: * id: "488a8a29-d687-46d5-964b-367f948d6e68" * funderName: "LEX" * status: "GENERATED" * monthlyPayment: 214.64 * totalAmount: 2575.68 * apr: 0 * balloonPayment: 0 * externalQuoteId: "1344018037" * externalReference: "-235304123/00001" * orderReferenceId: "ORD-1760620296758-GATEWAY" * validUntil: "2025-11-14T07:02:09.795Z" * createdAt: "2025-10-15T07:02:09.798Z" * updatedAt: "2025-10-15T07:02:09.798Z" * funderResponseData: * quote_id: 1344018037 * quote_reference: "-235304123/00001" * vehicle_description: "SPORTAGE ESTATE SPECIAL EDITIONS 1.6T GDi HEV Shadow 5dr Auto 25" * monthly_rental: * amount: 214.64 * vat_rule_applies: true * initial_payment: * amount: 2000 * vat_rule_applies: true * metadata: * isMaintained: false * calculatedContractTypeId: 88 * 404: * description: Quote not found for the given order reference ID * content: * application/json: * example: * success: false * error: "Quote not found" * details: "No quote found with order reference ID: ORD-1760620296758-GATEWAY" * 400: * $ref: '#/components/responses/BadRequest' * 500: * $ref: '#/components/responses/InternalServerError' */ router.get( '/quotes/order/:orderReferenceId', [param('orderReferenceId').notEmpty().isString().withMessage('Order reference ID is required')], asyncHandler(async (req: Request, res: Response) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res .status(400) .json({ success: false, error: 'Validation failed', details: errors.array() }); } const { orderReferenceId } = req.params; try { logger.info('Retrieving quote by order reference ID', { orderReferenceId }); const quote = await integrationService.getQuoteByOrderReference(orderReferenceId); return res.status(200).json({ success: true, data: quote, message: 'Quote details retrieved successfully', }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('Error retrieving quote by order reference:', { error: errorMessage, orderReferenceId, }); if (errorMessage === 'Quote not found') { return res.status(404).json({ success: false, error: 'Quote not found', details: `No quote found with order reference ID: ${orderReferenceId}`, }); } return res.status(500).json({ success: false, error: 'Failed to retrieve quote details', details: errorMessage, }); } }) ); export default router;