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