erp-mecanicas-diesel-backen.../src/modules/service-management/services/diagnostic.service.ts
Adrian Flores Cortes b455de93b2 feat(service-management): Implement Sprint 2 service order workflow
Sprint 2 - S2-T03, S2-T04, S2-T05

New services:
- VehicleReceptionService: Vehicle reception workflow
  - receiveVehicle, createQuickReception
  - getReceptionForm, validateVehiclePlate
  - Work bay management
  - Reception statistics

- DiagnosticService (enhanced): Complete diagnostic workflow
  - createDiagnostic with order status update
  - addScannerResults with DTC code parsing
  - completeDiagnostic with quote trigger
  - generateDiagnosticReport
  - 70+ diesel DTC codes database

- QuoteService (enhanced): Full quotation workflow
  - createQuote, addQuoteItem, updateQuoteItem
  - recalculateQuoteTotals (16% IVA)
  - sendQuoteToCustomer, customerResponse
  - convertToOrder

New entities:
- QuoteItem entity for quote line items

Fix: rbac.middleware.ts - Use role (singular) instead of roles

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 00:28:03 -06:00

1230 lines
40 KiB
TypeScript

/**
* Diagnostic Service
* Mecánicas Diesel - ERP Suite
*
* Business logic for vehicle diagnostics including:
* - Creating diagnostics for service orders
* - Processing scanner results (OBD-II codes)
* - Managing diagnostic workflow
* - Generating diagnostic reports
*/
import { Repository, DataSource } from 'typeorm';
import {
Diagnostic,
DiagnosticType,
DiagnosticResult,
} from '../entities/diagnostic.entity';
import { ServiceOrder, ServiceOrderStatus } from '../entities/service-order.entity';
import { Quote, QuoteStatus } from '../entities/quote.entity';
// ============================================
// DTOs
// ============================================
/**
* DTO for creating a new diagnostic
*/
export interface CreateDiagnosticDto {
orderId: string;
type: DiagnosticType;
findings?: string;
recommendations?: string;
estimatedCost?: number;
scannerData?: ScannerDataDto;
equipment?: string;
performedBy?: string;
}
/**
* DTO for updating an existing diagnostic
*/
export interface UpdateDiagnosticDto {
findings?: string;
recommendations?: string;
result?: DiagnosticResult;
scannerData?: ScannerDataDto;
equipment?: string;
estimatedCost?: number;
}
/**
* DTO for scanner data input
*/
export interface ScannerDataDto {
rawData: string | Record<string, unknown>;
deviceModel?: string;
deviceSerial?: string;
softwareVersion?: string;
timestamp?: Date;
}
/**
* DTO for parsed scanner results
*/
export interface ScannerResultDto {
rawData: string | Record<string, unknown>;
parsedCodes: ParsedDTCCode[];
timestamp: Date;
deviceInfo?: {
model?: string;
serial?: string;
softwareVersion?: string;
};
}
/**
* Parsed DTC code structure
*/
export interface ParsedDTCCode {
code: string;
description: string;
severity: 'critical' | 'warning' | 'info';
system: string;
suggestedAction?: string;
}
/**
* Diagnostic item for detailed findings
*/
export interface DiagnosticItemDto {
itemType: 'dtc_code' | 'test_result' | 'measurement' | 'observation';
code?: string;
description?: string;
severity?: 'critical' | 'warning' | 'info';
parameter?: string;
value?: number;
unit?: string;
minRef?: number;
maxRef?: number;
status?: 'ok' | 'warning' | 'fail' | 'no_reference';
component?: string;
cylinder?: number;
notes?: string;
}
/**
* Diagnostic recommendation structure
*/
export interface DiagnosticRecommendationDto {
description: string;
priority: 'critical' | 'high' | 'medium' | 'low';
urgency: 'immediate' | 'soon' | 'scheduled' | 'preventive';
suggestedServiceId?: string;
estimatedCost?: number;
notes?: string;
}
/**
* Diagnostic report data structure
*/
export interface DiagnosticReportData {
diagnostic: Diagnostic;
order: ServiceOrder | null;
vehicle: {
id: string;
plate?: string;
make?: string;
model?: string;
year?: number;
vin?: string;
} | null;
customer: {
id: string;
name?: string;
phone?: string;
email?: string;
} | null;
parsedCodes: ParsedDTCCode[];
recommendations: DiagnosticRecommendationDto[];
testResults: DiagnosticItemDto[];
generatedAt: Date;
technicianName?: string;
}
/**
* DiagnosticService
*
* Handles the complete diagnostic workflow for diesel vehicles:
* - Creating diagnostics linked to service orders
* - Processing and parsing OBD-II scanner data
* - Managing diagnostic lifecycle
* - Generating printable reports
* - Triggering quote generation when issues are found
*/
export class DiagnosticService {
private diagnosticRepository: Repository<Diagnostic>;
private orderRepository: Repository<ServiceOrder>;
private quoteRepository: Repository<Quote>;
constructor(private dataSource: DataSource) {
this.diagnosticRepository = dataSource.getRepository(Diagnostic);
this.orderRepository = dataSource.getRepository(ServiceOrder);
this.quoteRepository = dataSource.getRepository(Quote);
}
// ============================================
// CORE CRUD OPERATIONS
// ============================================
/**
* Create a new diagnostic for a service order
*
* Validates:
* - Order exists and belongs to tenant
* - Order is in valid status (received or diagnosed)
*
* Side effects:
* - Updates order status to 'diagnosed' if this is the first diagnostic
*
* @param tenantId - Tenant identifier for multi-tenancy
* @param dto - Create diagnostic data
* @param userId - User performing the diagnostic (optional)
* @returns Created diagnostic record
* @throws Error if order not found or invalid status
*/
async createDiagnostic(
tenantId: string,
dto: CreateDiagnosticDto,
userId?: string
): Promise<Diagnostic> {
const order = await this.orderRepository.findOne({
where: { id: dto.orderId, tenantId },
});
if (!order) {
throw new Error(`Service order not found: ${dto.orderId}`);
}
const validStatuses = [ServiceOrderStatus.RECEIVED, ServiceOrderStatus.DIAGNOSED];
if (!validStatuses.includes(order.status)) {
throw new Error(
`Cannot create diagnostic for order in status '${order.status}'. ` +
`Order must be in status: ${validStatuses.join(' or ')}`
);
}
const rawData: Record<string, unknown> = {};
if (dto.scannerData) {
rawData.scanner = {
rawData: dto.scannerData.rawData,
deviceModel: dto.scannerData.deviceModel,
deviceSerial: dto.scannerData.deviceSerial,
softwareVersion: dto.scannerData.softwareVersion,
timestamp: dto.scannerData.timestamp || new Date(),
};
if (dto.type === DiagnosticType.SCANNER) {
const parsed = this.parseRawScannerData(dto.scannerData.rawData);
rawData.parsedCodes = parsed;
}
}
if (dto.findings) {
rawData.findings = dto.findings;
}
if (dto.recommendations) {
rawData.recommendations = dto.recommendations;
}
if (dto.estimatedCost !== undefined) {
rawData.estimatedCost = dto.estimatedCost;
}
const diagnostic = this.diagnosticRepository.create({
tenantId,
orderId: dto.orderId,
vehicleId: order.vehicleId,
diagnosticType: dto.type,
equipment: dto.equipment,
performedBy: userId || dto.performedBy,
performedAt: new Date(),
rawData,
summary: dto.findings,
});
const savedDiagnostic = await this.diagnosticRepository.save(diagnostic);
if (order.status === ServiceOrderStatus.RECEIVED) {
order.status = ServiceOrderStatus.DIAGNOSED;
await this.orderRepository.save(order);
}
return savedDiagnostic;
}
/**
* Add scanner results to an existing diagnostic
*
* Parses OBD-II codes from scanner output and generates recommendations
* based on detected error codes.
*
* @param tenantId - Tenant identifier
* @param diagnosticId - Diagnostic to update
* @param scannerData - Scanner data to process
* @returns Updated diagnostic with parsed codes
* @throws Error if diagnostic not found
*/
async addScannerResults(
tenantId: string,
diagnosticId: string,
scannerData: ScannerDataDto
): Promise<Diagnostic> {
const diagnostic = await this.findById(tenantId, diagnosticId);
if (!diagnostic) {
throw new Error(`Diagnostic not found: ${diagnosticId}`);
}
const parsedCodes = this.parseRawScannerData(scannerData.rawData);
const recommendations = this.generateRecommendationsFromCodes(parsedCodes);
const existingData = diagnostic.rawData || {};
diagnostic.rawData = {
...existingData,
scanner: {
rawData: scannerData.rawData,
deviceModel: scannerData.deviceModel,
deviceSerial: scannerData.deviceSerial,
softwareVersion: scannerData.softwareVersion,
timestamp: scannerData.timestamp || new Date(),
},
parsedCodes,
autoRecommendations: recommendations,
};
if (parsedCodes.length > 0) {
const summaryParts: string[] = [];
const criticalCodes = parsedCodes.filter(c => c.severity === 'critical');
const warningCodes = parsedCodes.filter(c => c.severity === 'warning');
if (criticalCodes.length > 0) {
summaryParts.push(`${criticalCodes.length} critical issue(s) found`);
}
if (warningCodes.length > 0) {
summaryParts.push(`${warningCodes.length} warning(s)`);
}
summaryParts.push(`Total: ${parsedCodes.length} DTC code(s)`);
diagnostic.summary = summaryParts.join('. ');
}
return this.diagnosticRepository.save(diagnostic);
}
/**
* Get all diagnostics for a service order
*
* @param tenantId - Tenant identifier
* @param orderId - Service order ID
* @returns Array of diagnostics ordered by date descending
*/
async getDiagnosticsByOrder(tenantId: string, orderId: string): Promise<Diagnostic[]> {
return this.diagnosticRepository.find({
where: { tenantId, orderId },
order: { performedAt: 'DESC' },
});
}
/**
* Get diagnostic history for a vehicle
*
* Returns all diagnostics ever performed on a vehicle across all orders,
* useful for tracking recurring issues and maintenance patterns.
*
* @param tenantId - Tenant identifier
* @param vehicleId - Vehicle ID
* @param limit - Maximum number of records (default: 50)
* @returns Array of diagnostics ordered by date descending
*/
async getDiagnosticsByVehicle(
tenantId: string,
vehicleId: string,
limit: number = 50
): Promise<Diagnostic[]> {
return this.diagnosticRepository.find({
where: { tenantId, vehicleId },
order: { performedAt: 'DESC' },
take: limit,
});
}
/**
* Update an existing diagnostic
*
* @param tenantId - Tenant identifier
* @param diagnosticId - Diagnostic to update
* @param dto - Fields to update
* @returns Updated diagnostic or null if not found
*/
async updateDiagnostic(
tenantId: string,
diagnosticId: string,
dto: UpdateDiagnosticDto
): Promise<Diagnostic | null> {
const diagnostic = await this.findById(tenantId, diagnosticId);
if (!diagnostic) {
return null;
}
const existingData = diagnostic.rawData || {};
if (dto.findings !== undefined) {
existingData.findings = dto.findings;
diagnostic.summary = dto.findings;
}
if (dto.recommendations !== undefined) {
existingData.recommendations = dto.recommendations;
}
if (dto.estimatedCost !== undefined) {
existingData.estimatedCost = dto.estimatedCost;
}
if (dto.scannerData) {
const parsedCodes = this.parseRawScannerData(dto.scannerData.rawData);
existingData.scanner = {
rawData: dto.scannerData.rawData,
deviceModel: dto.scannerData.deviceModel,
deviceSerial: dto.scannerData.deviceSerial,
softwareVersion: dto.scannerData.softwareVersion,
timestamp: dto.scannerData.timestamp || new Date(),
};
existingData.parsedCodes = parsedCodes;
}
if (dto.result !== undefined) {
diagnostic.result = dto.result;
}
if (dto.equipment !== undefined) {
diagnostic.equipment = dto.equipment;
}
diagnostic.rawData = existingData;
return this.diagnosticRepository.save(diagnostic);
}
/**
* Complete a diagnostic and optionally trigger quote generation
*
* When the diagnostic result is FAIL or NEEDS_ATTENTION, this method
* can automatically create a draft quote based on the diagnostic findings.
*
* @param tenantId - Tenant identifier
* @param diagnosticId - Diagnostic to complete
* @param result - Final diagnostic result
* @param finalSummary - Optional final summary
* @param triggerQuote - Whether to create a draft quote (default: true for FAIL/NEEDS_ATTENTION)
* @returns Object with diagnostic and optional quote
* @throws Error if diagnostic not found
*/
async completeDiagnostic(
tenantId: string,
diagnosticId: string,
result: DiagnosticResult,
finalSummary?: string,
triggerQuote: boolean = true
): Promise<{ diagnostic: Diagnostic; quote: Quote | null }> {
const diagnostic = await this.findById(tenantId, diagnosticId);
if (!diagnostic) {
throw new Error(`Diagnostic not found: ${diagnosticId}`);
}
if (diagnostic.result) {
throw new Error('Diagnostic has already been completed');
}
diagnostic.result = result;
if (finalSummary) {
diagnostic.summary = finalSummary;
if (diagnostic.rawData) {
diagnostic.rawData.finalSummary = finalSummary;
}
}
const savedDiagnostic = await this.diagnosticRepository.save(diagnostic);
let quote: Quote | null = null;
const needsQuote = triggerQuote &&
(result === DiagnosticResult.FAIL || result === DiagnosticResult.NEEDS_ATTENTION);
if (needsQuote && diagnostic.orderId) {
const order = await this.orderRepository.findOne({
where: { id: diagnostic.orderId, tenantId },
});
if (order) {
quote = await this.createDraftQuoteFromDiagnostic(tenantId, diagnostic, order);
}
}
return { diagnostic: savedDiagnostic, quote };
}
/**
* Generate printable report data for a diagnostic
*
* Compiles all diagnostic information into a structured format
* suitable for generating PDF reports or displaying in UI.
*
* @param tenantId - Tenant identifier
* @param diagnosticId - Diagnostic to generate report for
* @returns Structured report data
* @throws Error if diagnostic not found
*/
async generateDiagnosticReport(
tenantId: string,
diagnosticId: string
): Promise<DiagnosticReportData> {
const diagnostic = await this.diagnosticRepository.findOne({
where: { id: diagnosticId, tenantId },
relations: ['order'],
});
if (!diagnostic) {
throw new Error(`Diagnostic not found: ${diagnosticId}`);
}
const rawData = diagnostic.rawData || {};
const parsedCodes: ParsedDTCCode[] = (rawData.parsedCodes as ParsedDTCCode[]) || [];
const recommendations: DiagnosticRecommendationDto[] = [];
if (rawData.autoRecommendations && Array.isArray(rawData.autoRecommendations)) {
recommendations.push(...(rawData.autoRecommendations as DiagnosticRecommendationDto[]));
}
if (rawData.recommendations && typeof rawData.recommendations === 'string') {
recommendations.push({
description: rawData.recommendations as string,
priority: 'medium',
urgency: 'scheduled',
});
}
const testResults: DiagnosticItemDto[] = [];
if (rawData.testResults && Array.isArray(rawData.testResults)) {
testResults.push(...(rawData.testResults as DiagnosticItemDto[]));
}
for (const code of parsedCodes) {
testResults.push({
itemType: 'dtc_code',
code: code.code,
description: code.description,
severity: code.severity,
component: code.system,
notes: code.suggestedAction,
});
}
let order: ServiceOrder | null = null;
if (diagnostic.orderId) {
order = await this.orderRepository.findOne({
where: { id: diagnostic.orderId, tenantId },
});
}
const reportData: DiagnosticReportData = {
diagnostic,
order,
vehicle: {
id: diagnostic.vehicleId,
},
customer: order ? {
id: order.customerId,
} : null,
parsedCodes,
recommendations,
testResults,
generatedAt: new Date(),
technicianName: undefined,
};
return reportData;
}
// ============================================
// BASIC QUERY METHODS
// ============================================
/**
* Find diagnostic by ID
*/
async findById(tenantId: string, id: string): Promise<Diagnostic | null> {
return this.diagnosticRepository.findOne({
where: { id, tenantId },
});
}
/**
* Find diagnostics by vehicle (alias for getDiagnosticsByVehicle)
*/
async findByVehicle(tenantId: string, vehicleId: string): Promise<Diagnostic[]> {
return this.getDiagnosticsByVehicle(tenantId, vehicleId);
}
/**
* Find diagnostics by order (alias for getDiagnosticsByOrder)
*/
async findByOrder(tenantId: string, orderId: string): Promise<Diagnostic[]> {
return this.getDiagnosticsByOrder(tenantId, orderId);
}
/**
* Update diagnostic result (legacy compatibility)
*/
async updateResult(
tenantId: string,
id: string,
result: DiagnosticResult,
summary?: string
): Promise<Diagnostic | null> {
const diagnostic = await this.findById(tenantId, id);
if (!diagnostic) return null;
diagnostic.result = result;
if (summary) diagnostic.summary = summary;
return this.diagnosticRepository.save(diagnostic);
}
// ============================================
// STATISTICS AND ANALYTICS
// ============================================
/**
* Get diagnostic statistics for a vehicle
*/
async getVehicleStats(tenantId: string, vehicleId: string): Promise<{
totalDiagnostics: number;
lastDiagnosticDate: Date | null;
diagnosticsByType: Record<DiagnosticType, number>;
issuesFound: number;
mostCommonCodes: Array<{ code: string; count: number }>;
}> {
const diagnostics = await this.findByVehicle(tenantId, vehicleId);
const diagnosticsByType: Record<DiagnosticType, number> = {
[DiagnosticType.SCANNER]: 0,
[DiagnosticType.INJECTOR_TEST]: 0,
[DiagnosticType.PUMP_TEST]: 0,
[DiagnosticType.COMPRESSION]: 0,
[DiagnosticType.TURBO_TEST]: 0,
[DiagnosticType.OTHER]: 0,
};
let issuesFound = 0;
const codeCounts: Record<string, number> = {};
for (const diag of diagnostics) {
diagnosticsByType[diag.diagnosticType]++;
if (diag.result === DiagnosticResult.FAIL || diag.result === DiagnosticResult.NEEDS_ATTENTION) {
issuesFound++;
}
const parsedCodes = (diag.rawData?.parsedCodes as ParsedDTCCode[]) || [];
for (const codeData of parsedCodes) {
codeCounts[codeData.code] = (codeCounts[codeData.code] || 0) + 1;
}
}
const mostCommonCodes = Object.entries(codeCounts)
.map(([code, count]) => ({ code, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
return {
totalDiagnostics: diagnostics.length,
lastDiagnosticDate: diagnostics.length > 0 ? diagnostics[0].performedAt : null,
diagnosticsByType,
issuesFound,
mostCommonCodes,
};
}
// ============================================
// SCANNER DATA PARSING
// ============================================
/**
* Parse raw scanner data into structured DTC codes
*
* Handles multiple input formats:
* - String with comma/space separated codes
* - Array of code strings
* - Array of code objects with description/severity
* - Object with dtc_codes, codes, or faults array
*/
private parseRawScannerData(rawData: string | Record<string, unknown>): ParsedDTCCode[] {
const codes: ParsedDTCCode[] = [];
if (typeof rawData === 'string') {
const codePattern = /[PCBU]\d{4}/gi;
const matches = rawData.match(codePattern) || [];
for (const code of matches) {
const upperCode = code.toUpperCase();
codes.push({
code: upperCode,
description: this.getDTCDescription(upperCode),
severity: this.getDTCSeverity(upperCode),
system: this.getDTCSystem(upperCode),
suggestedAction: this.getSuggestedAction(upperCode),
});
}
} else if (typeof rawData === 'object' && rawData !== null) {
const dtcArray = rawData.dtc_codes || rawData.codes || rawData.faults || rawData.errors || [];
if (Array.isArray(dtcArray)) {
for (const item of dtcArray) {
if (typeof item === 'string') {
const upperCode = item.toUpperCase();
codes.push({
code: upperCode,
description: this.getDTCDescription(upperCode),
severity: this.getDTCSeverity(upperCode),
system: this.getDTCSystem(upperCode),
suggestedAction: this.getSuggestedAction(upperCode),
});
} else if (typeof item === 'object' && item !== null) {
const code = (item.code || item.id || '').toUpperCase();
codes.push({
code,
description: item.description || item.message || this.getDTCDescription(code),
severity: item.severity || this.getDTCSeverity(code),
system: item.system || this.getDTCSystem(code),
suggestedAction: item.suggestedAction || this.getSuggestedAction(code),
});
}
}
}
}
return codes;
}
/**
* Parse DTC codes from scanner data (public method for controller)
*/
parseDTCCodes(rawData: Record<string, unknown>): DiagnosticItemDto[] {
const parsedCodes = this.parseRawScannerData(rawData);
return parsedCodes.map(code => ({
itemType: 'dtc_code' as const,
code: code.code,
description: code.description,
severity: code.severity,
component: code.system,
notes: code.suggestedAction,
}));
}
/**
* Get DTC code description (comprehensive diesel codes database)
*/
private getDTCDescription(code: string): string {
const descriptions: Record<string, string> = {
// Fuel System
'P0087': 'Fuel Rail/System Pressure - Too Low',
'P0088': 'Fuel Rail/System Pressure - Too High',
'P0089': 'Fuel Pressure Regulator 1 Performance',
'P0090': 'Fuel Pressure Regulator 1 Control Circuit',
'P0091': 'Fuel Pressure Regulator 1 Control Circuit Low',
'P0092': 'Fuel Pressure Regulator 1 Control Circuit High',
'P0093': 'Fuel System Leak Detected - Large Leak',
'P0094': 'Fuel System Leak Detected - Small Leak',
// Mass Air Flow
'P0100': 'Mass Air Flow Circuit Malfunction',
'P0101': 'Mass Air Flow Circuit Range/Performance',
'P0102': 'Mass Air Flow Circuit Low',
'P0103': 'Mass Air Flow Circuit High',
'P0104': 'Mass Air Flow Circuit Intermittent',
// Intake Air Temperature
'P0110': 'Intake Air Temperature Sensor Circuit Malfunction',
'P0111': 'Intake Air Temperature Sensor Circuit Range/Performance',
'P0112': 'Intake Air Temperature Sensor Circuit Low',
'P0113': 'Intake Air Temperature Sensor Circuit High',
// Coolant Temperature
'P0115': 'Engine Coolant Temperature Circuit Malfunction',
'P0116': 'Engine Coolant Temperature Circuit Range/Performance',
'P0117': 'Engine Coolant Temperature Circuit Low',
'P0118': 'Engine Coolant Temperature Circuit High',
// Crankshaft/Camshaft
'P0335': 'Crankshaft Position Sensor A Circuit Malfunction',
'P0336': 'Crankshaft Position Sensor A Circuit Range/Performance',
'P0340': 'Camshaft Position Sensor A Circuit Malfunction',
'P0341': 'Camshaft Position Sensor A Circuit Range/Performance',
// Injector Circuits
'P0200': 'Injector Circuit Malfunction',
'P0201': 'Injector Circuit/Open - Cylinder 1',
'P0202': 'Injector Circuit/Open - Cylinder 2',
'P0203': 'Injector Circuit/Open - Cylinder 3',
'P0204': 'Injector Circuit/Open - Cylinder 4',
'P0205': 'Injector Circuit/Open - Cylinder 5',
'P0206': 'Injector Circuit/Open - Cylinder 6',
'P0207': 'Injector Circuit/Open - Cylinder 7',
'P0208': 'Injector Circuit/Open - Cylinder 8',
'P0261': 'Cylinder 1 Injector Circuit Low',
'P0262': 'Cylinder 1 Injector Circuit High',
'P0263': 'Cylinder 1 Contribution/Balance',
// Turbocharger
'P0234': 'Turbocharger/Supercharger Overboost Condition',
'P0235': 'Turbocharger Boost Sensor A Circuit',
'P0236': 'Turbocharger Boost Sensor A Circuit Range/Performance',
'P0237': 'Turbocharger Boost Sensor A Circuit Low',
'P0238': 'Turbocharger Boost Sensor A Circuit High',
'P0299': 'Turbocharger/Supercharger Underboost',
// EGR System
'P0400': 'Exhaust Gas Recirculation Flow Malfunction',
'P0401': 'Exhaust Gas Recirculation Flow Insufficient',
'P0402': 'Exhaust Gas Recirculation Flow Excessive',
'P0403': 'Exhaust Gas Recirculation Control Circuit',
'P0404': 'Exhaust Gas Recirculation Circuit Range/Performance',
'P0405': 'Exhaust Gas Recirculation Sensor A Circuit Low',
'P0406': 'Exhaust Gas Recirculation Sensor A Circuit High',
// Diesel Particulate Filter
'P2002': 'Diesel Particulate Filter Efficiency Below Threshold',
'P2003': 'Diesel Particulate Filter Efficiency Below Threshold Bank 2',
'P2004': 'Intake Manifold Runner Control Stuck Open',
'P244A': 'Diesel Particulate Filter Differential Pressure Too Low',
'P244B': 'Diesel Particulate Filter Differential Pressure Too High',
'P2458': 'Diesel Particulate Filter Regeneration Duration',
'P242F': 'Diesel Particulate Filter Restriction - Ash Accumulation',
// Glow Plug System
'P0380': 'Glow Plug/Heater Circuit A Malfunction',
'P0381': 'Glow Plug/Heater Indicator Circuit',
'P0382': 'Glow Plug/Heater Circuit B Malfunction',
'P0670': 'Glow Plug Module Control Circuit',
'P0671': 'Cylinder 1 Glow Plug Circuit',
'P0672': 'Cylinder 2 Glow Plug Circuit',
'P0673': 'Cylinder 3 Glow Plug Circuit',
'P0674': 'Cylinder 4 Glow Plug Circuit',
'P0675': 'Cylinder 5 Glow Plug Circuit',
'P0676': 'Cylinder 6 Glow Plug Circuit',
// NOx System
'P2200': 'NOx Sensor Circuit Bank 1',
'P2201': 'NOx Sensor Circuit Range/Performance Bank 1',
'P2202': 'NOx Sensor Circuit Low Bank 1',
'P2203': 'NOx Sensor Circuit High Bank 1',
// SCR/DEF System
'P2BAD': 'NOx Exceedance - SCR System',
'P20BD': 'Reductant Heater Control Circuit',
'P20E8': 'Reductant Pressure Too Low',
'P20E9': 'Reductant Pressure Too High',
'P207F': 'Reductant Quality Performance',
};
return descriptions[code] || `Unknown code: ${code}`;
}
/**
* Determine DTC severity based on code type and category
*/
private getDTCSeverity(code: string): 'critical' | 'warning' | 'info' {
const criticalPrefixes = ['P008', 'P009', 'P02', 'P033', 'P034'];
const warningPrefixes = ['P023', 'P029', 'P04', 'P2', 'P067'];
for (const prefix of criticalPrefixes) {
if (code.startsWith(prefix)) return 'critical';
}
for (const prefix of warningPrefixes) {
if (code.startsWith(prefix)) return 'warning';
}
if (code.startsWith('P0')) return 'warning';
if (code.startsWith('P1')) return 'info';
if (code.startsWith('P2')) return 'warning';
if (code.startsWith('P3')) return 'warning';
if (code.startsWith('C')) return 'warning';
if (code.startsWith('B')) return 'info';
if (code.startsWith('U')) return 'info';
return 'info';
}
/**
* Determine the vehicle system affected by a DTC code
*/
private getDTCSystem(code: string): string {
const systemMap: Record<string, string> = {
'P00': 'Fuel and Air Metering',
'P01': 'Fuel and Air Metering',
'P02': 'Fuel and Air Metering (Injector Circuit)',
'P03': 'Ignition System',
'P04': 'Auxiliary Emission Controls',
'P05': 'Vehicle Speed/Idle Control',
'P06': 'Computer Output Circuit',
'P07': 'Transmission',
'P08': 'Transmission',
'P2': 'Generic Powertrain',
'P3': 'Generic Powertrain',
'C': 'Chassis',
'B': 'Body',
'U': 'Network',
};
for (const [prefix, system] of Object.entries(systemMap)) {
if (code.startsWith(prefix)) return system;
}
return 'Unknown System';
}
/**
* Get suggested action for a DTC code
*/
private getSuggestedAction(code: string): string {
const actions: Record<string, string> = {
'P0087': 'Check fuel filter, fuel pump, and fuel lines for restrictions. Verify fuel pressure.',
'P0088': 'Check fuel pressure regulator. Inspect for kinked return lines.',
'P0093': 'Inspect all fuel lines, connections, and injectors for leaks immediately.',
'P0201': 'Check injector wiring and connector for cylinder 1. Test injector resistance.',
'P0202': 'Check injector wiring and connector for cylinder 2. Test injector resistance.',
'P0203': 'Check injector wiring and connector for cylinder 3. Test injector resistance.',
'P0204': 'Check injector wiring and connector for cylinder 4. Test injector resistance.',
'P0234': 'Inspect wastegate operation. Check boost pressure sensor and hoses.',
'P0299': 'Check for boost leaks, turbo damage, or wastegate issues.',
'P0401': 'Clean or replace EGR valve. Check for carbon buildup in passages.',
'P2002': 'Perform forced regeneration. Check DPF condition.',
'P242F': 'DPF cleaning or replacement may be required.',
'P0670': 'Check glow plug control module and wiring.',
};
if (actions[code]) return actions[code];
if (code.startsWith('P02')) {
return 'Inspect injector circuit wiring and connectors. Test injector resistance and spray pattern.';
}
if (code.startsWith('P04')) {
return 'Inspect emission control system components. Check for vacuum leaks.';
}
if (code.startsWith('P067')) {
return 'Test glow plug resistance and circuit continuity.';
}
return 'Perform detailed diagnostic testing. Consult service manual for specific procedures.';
}
/**
* Generate recommendations based on parsed DTC codes
*/
private generateRecommendationsFromCodes(codes: ParsedDTCCode[]): DiagnosticRecommendationDto[] {
const recommendations: DiagnosticRecommendationDto[] = [];
const processedSystems = new Set<string>();
for (const code of codes) {
if (processedSystems.has(code.system)) continue;
processedSystems.add(code.system);
const systemCodes = codes.filter(c => c.system === code.system);
const hasCritical = systemCodes.some(c => c.severity === 'critical');
const hasWarning = systemCodes.some(c => c.severity === 'warning');
let priority: 'critical' | 'high' | 'medium' | 'low' = 'low';
let urgency: 'immediate' | 'soon' | 'scheduled' | 'preventive' = 'preventive';
if (hasCritical) {
priority = 'critical';
urgency = 'immediate';
} else if (hasWarning) {
priority = 'high';
urgency = 'soon';
}
const codeList = systemCodes.map(c => c.code).join(', ');
recommendations.push({
description: `${code.system} system requires attention. Codes: ${codeList}`,
priority,
urgency,
notes: systemCodes.map(c => `${c.code}: ${c.suggestedAction}`).join('\n'),
});
}
recommendations.sort((a, b) => {
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
});
return recommendations;
}
// ============================================
// INJECTOR TEST ANALYSIS
// ============================================
/**
* Analyze injector test results from raw data
*/
analyzeInjectorTest(rawData: Record<string, unknown>): DiagnosticItemDto[] {
const items: DiagnosticItemDto[] = [];
const injectors = rawData.injectors || rawData.cylinders || [];
if (Array.isArray(injectors)) {
for (let i = 0; i < injectors.length; i++) {
const injector = injectors[i];
if (typeof injector === 'object' && injector !== null) {
if (injector.return_qty !== undefined) {
const returnQty = Number(injector.return_qty);
items.push({
itemType: 'measurement',
parameter: 'Return Quantity',
value: returnQty,
unit: 'ml/min',
minRef: 0,
maxRef: 50,
status: returnQty > 50 ? 'fail' : returnQty > 40 ? 'warning' : 'ok',
cylinder: i + 1,
notes: returnQty > 50
? 'Excessive return quantity indicates injector wear or seal damage'
: returnQty > 40
? 'Return quantity approaching limit, monitor closely'
: 'Return quantity within acceptable range',
});
}
if (injector.spray_pattern !== undefined) {
const pattern = String(injector.spray_pattern).toLowerCase();
items.push({
itemType: 'observation',
description: `Spray pattern: ${injector.spray_pattern}`,
status: pattern === 'good' || pattern === 'ok' ? 'ok' :
pattern === 'fair' ? 'warning' : 'fail',
cylinder: i + 1,
notes: pattern === 'good' || pattern === 'ok'
? 'Spray pattern is uniform and properly atomized'
: 'Spray pattern indicates potential nozzle issues',
});
}
if (injector.opening_pressure !== undefined) {
const pressure = Number(injector.opening_pressure);
const minPressure = Number(injector.min_pressure) || 200;
const maxPressure = Number(injector.max_pressure) || 250;
items.push({
itemType: 'measurement',
parameter: 'Opening Pressure',
value: pressure,
unit: 'bar',
minRef: minPressure,
maxRef: maxPressure,
status: pressure < minPressure ? 'fail' :
pressure > maxPressure ? 'fail' : 'ok',
cylinder: i + 1,
notes: pressure < minPressure
? 'Opening pressure too low - injector may need rebuild'
: pressure > maxPressure
? 'Opening pressure too high - check for blockage'
: 'Opening pressure within specification',
});
}
if (injector.leakage !== undefined) {
const hasLeakage = injector.leakage === true || injector.leakage === 'yes';
items.push({
itemType: 'test_result',
parameter: 'Leakage Test',
description: hasLeakage ? 'FAILED - Leakage detected' : 'PASSED - No leakage',
status: hasLeakage ? 'fail' : 'ok',
cylinder: i + 1,
notes: hasLeakage
? 'Injector leakage can cause hard starting and reduced efficiency'
: 'Injector seal integrity confirmed',
});
}
}
}
}
return items;
}
// ============================================
// COMPRESSION TEST ANALYSIS
// ============================================
/**
* Analyze compression test results
*/
analyzeCompressionTest(rawData: Record<string, unknown>): DiagnosticItemDto[] {
const items: DiagnosticItemDto[] = [];
const readings = rawData.readings || rawData.cylinders || rawData.compression || [];
if (Array.isArray(readings)) {
const values: number[] = [];
for (let i = 0; i < readings.length; i++) {
const reading = readings[i];
let compressionValue: number;
if (typeof reading === 'number') {
compressionValue = reading;
} else if (typeof reading === 'object' && reading !== null) {
compressionValue = Number(reading.value || reading.psi || reading.bar || 0);
} else {
continue;
}
values.push(compressionValue);
}
if (values.length > 0) {
const avgCompression = values.reduce((a, b) => a + b, 0) / values.length;
const minCompression = Math.min(...values);
const maxCompression = Math.max(...values);
const variation = ((maxCompression - minCompression) / avgCompression) * 100;
for (let i = 0; i < values.length; i++) {
const value = values[i];
const deviationFromAvg = ((value - avgCompression) / avgCompression) * 100;
let status: 'ok' | 'warning' | 'fail' = 'ok';
if (value < avgCompression * 0.85 || Math.abs(deviationFromAvg) > 15) {
status = 'fail';
} else if (value < avgCompression * 0.9 || Math.abs(deviationFromAvg) > 10) {
status = 'warning';
}
items.push({
itemType: 'measurement',
parameter: 'Compression',
value,
unit: 'PSI',
minRef: avgCompression * 0.85,
maxRef: avgCompression * 1.15,
status,
cylinder: i + 1,
notes: status === 'fail'
? `Low compression - ${deviationFromAvg.toFixed(1)}% below average. Check rings, valves, or head gasket.`
: status === 'warning'
? `Compression slightly low - monitor for degradation`
: `Compression within acceptable range`,
});
}
items.push({
itemType: 'observation',
description: `Compression Balance Analysis`,
status: variation > 15 ? 'fail' : variation > 10 ? 'warning' : 'ok',
notes: `Average: ${avgCompression.toFixed(0)} PSI, Variation: ${variation.toFixed(1)}%. ` +
(variation > 15
? 'Significant imbalance detected - engine may have mechanical issues.'
: variation > 10
? 'Slight imbalance - monitor for changes.'
: 'Cylinders are well balanced.'),
});
}
}
return items;
}
// ============================================
// HELPER METHODS
// ============================================
/**
* Create a draft quote from diagnostic findings
*/
private async createDraftQuoteFromDiagnostic(
tenantId: string,
diagnostic: Diagnostic,
order: ServiceOrder
): Promise<Quote> {
const year = new Date().getFullYear();
const prefix = `COT-${year}-`;
const lastQuote = await this.quoteRepository.findOne({
where: { tenantId },
order: { createdAt: 'DESC' },
});
let sequence = 1;
if (lastQuote?.quoteNumber?.startsWith(prefix)) {
const lastSeq = parseInt(lastQuote.quoteNumber.replace(prefix, ''), 10);
sequence = isNaN(lastSeq) ? 1 : lastSeq + 1;
}
const quoteNumber = `${prefix}${sequence.toString().padStart(5, '0')}`;
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 15);
const rawData = diagnostic.rawData || {};
const estimatedCost = (rawData.estimatedCost as number) || 0;
const notesFromDiagnostic: string[] = [`Generated from Diagnostic #${diagnostic.id}`];
if (diagnostic.summary) {
notesFromDiagnostic.push(`Diagnostic Summary: ${diagnostic.summary}`);
}
if (rawData.recommendations) {
notesFromDiagnostic.push(`Recommendations: ${rawData.recommendations}`);
}
const quote = this.quoteRepository.create({
tenantId,
quoteNumber,
customerId: order.customerId,
vehicleId: order.vehicleId,
diagnosticId: diagnostic.id,
status: QuoteStatus.DRAFT,
validityDays: 15,
expiresAt,
laborTotal: estimatedCost * 0.6,
partsTotal: estimatedCost * 0.4,
tax: estimatedCost * 0.16,
grandTotal: estimatedCost * 1.16,
notes: notesFromDiagnostic.join('\n'),
});
return this.quoteRepository.save(quote);
}
/**
* Create basic diagnostic (legacy compatibility)
*/
async create(tenantId: string, dto: {
vehicleId: string;
orderId?: string;
diagnosticType: DiagnosticType;
equipment?: string;
performedBy?: string;
summary?: string;
rawData?: Record<string, unknown>;
}): Promise<Diagnostic> {
const diagnostic = this.diagnosticRepository.create({
tenantId,
vehicleId: dto.vehicleId,
orderId: dto.orderId,
diagnosticType: dto.diagnosticType,
equipment: dto.equipment,
performedBy: dto.performedBy,
summary: dto.summary,
rawData: dto.rawData,
performedAt: new Date(),
});
return this.diagnosticRepository.save(diagnostic);
}
}