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>
1230 lines
40 KiB
TypeScript
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);
|
|
}
|
|
}
|