/** * 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; deviceModel?: string; deviceSerial?: string; softwareVersion?: string; timestamp?: Date; } /** * DTO for parsed scanner results */ export interface ScannerResultDto { rawData: string | Record; 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; private orderRepository: Repository; private quoteRepository: Repository; 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 { 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 = {}; 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 { 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 { 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 { 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 { 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 { 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 { return this.diagnosticRepository.findOne({ where: { id, tenantId }, }); } /** * Find diagnostics by vehicle (alias for getDiagnosticsByVehicle) */ async findByVehicle(tenantId: string, vehicleId: string): Promise { return this.getDiagnosticsByVehicle(tenantId, vehicleId); } /** * Find diagnostics by order (alias for getDiagnosticsByOrder) */ async findByOrder(tenantId: string, orderId: string): Promise { return this.getDiagnosticsByOrder(tenantId, orderId); } /** * Update diagnostic result (legacy compatibility) */ async updateResult( tenantId: string, id: string, result: DiagnosticResult, summary?: string ): Promise { 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; issuesFound: number; mostCommonCodes: Array<{ code: string; count: number }>; }> { const diagnostics = await this.findByVehicle(tenantId, vehicleId); const diagnosticsByType: Record = { [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 = {}; 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): 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): 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 = { // 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 = { '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 = { '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(); 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): 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): 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 { 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; }): Promise { 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); } }