diff --git a/backend/src/features/documents/data/documents.repository.ts b/backend/src/features/documents/data/documents.repository.ts index 0f9ca1e..e0d101e 100644 --- a/backend/src/features/documents/data/documents.repository.ts +++ b/backend/src/features/documents/data/documents.repository.ts @@ -90,6 +90,64 @@ export class DocumentsRepository { return res.rows.map(row => this.mapDocumentRecord(row)); } + async batchInsert( + documents: Array<{ + id: string; + userId: string; + vehicleId: string; + documentType: DocumentType; + title: string; + notes?: string | null; + details?: any; + issuedDate?: string | null; + expirationDate?: string | null; + emailNotifications?: boolean; + scanForMaintenance?: boolean; + }>, + client?: any + ): Promise { + if (documents.length === 0) { + return []; + } + + // Multi-value INSERT for performance (avoids N round-trips) + const queryClient = client || this.db; + const placeholders: string[] = []; + const values: any[] = []; + let paramCount = 1; + + documents.forEach((doc) => { + const docParams = [ + doc.id, + doc.userId, + doc.vehicleId, + doc.documentType, + doc.title, + doc.notes ?? null, + doc.details ?? null, + doc.issuedDate ?? null, + doc.expirationDate ?? null, + doc.emailNotifications ?? false, + doc.scanForMaintenance ?? false + ]; + + const placeholder = `($${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++})`; + placeholders.push(placeholder); + values.push(...docParams); + }); + + const query = ` + INSERT INTO documents ( + id, user_id, vehicle_id, document_type, title, notes, details, issued_date, expiration_date, email_notifications, scan_for_maintenance + ) + VALUES ${placeholders.join(', ')} + RETURNING * + `; + + const result = await queryClient.query(query, values); + return result.rows.map((row: any) => this.mapDocumentRecord(row)); + } + async softDelete(id: string, userId: string): Promise { await this.db.query(`UPDATE documents SET deleted_at = NOW() WHERE id = $1 AND user_id = $2`, [id, userId]); } diff --git a/backend/src/features/fuel-logs/data/fuel-logs.repository.ts b/backend/src/features/fuel-logs/data/fuel-logs.repository.ts index 1e03cb5..934936e 100644 --- a/backend/src/features/fuel-logs/data/fuel-logs.repository.ts +++ b/backend/src/features/fuel-logs/data/fuel-logs.repository.ts @@ -148,6 +148,52 @@ export class FuelLogsRepository { return this.mapRow(result.rows[0]); } + async batchInsert( + logs: Array, + client?: any + ): Promise { + if (logs.length === 0) { + return []; + } + + // Multi-value INSERT for performance (avoids N round-trips) + const queryClient = client || this.pool; + const placeholders: string[] = []; + const values: any[] = []; + let paramCount = 1; + + logs.forEach((log) => { + const logParams = [ + log.userId, + log.vehicleId, + log.date, + log.odometer, + log.gallons, + log.pricePerGallon, + log.totalCost, + log.station, + log.location, + log.notes + ]; + + const placeholder = `($${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++})`; + placeholders.push(placeholder); + values.push(...logParams); + }); + + const query = ` + INSERT INTO fuel_logs ( + user_id, vehicle_id, date, odometer, gallons, + price_per_gallon, total_cost, station, location, notes + ) + VALUES ${placeholders.join(', ')} + RETURNING * + `; + + const result = await queryClient.query(query, values); + return result.rows.map((row: any) => this.mapRow(row)); + } + async delete(id: string): Promise { const query = 'DELETE FROM fuel_logs WHERE id = $1'; const result = await this.pool.query(query, [id]); diff --git a/backend/src/features/maintenance/data/maintenance.repository.ts b/backend/src/features/maintenance/data/maintenance.repository.ts index dffaec9..24758e6 100644 --- a/backend/src/features/maintenance/data/maintenance.repository.ts +++ b/backend/src/features/maintenance/data/maintenance.repository.ts @@ -172,6 +172,62 @@ export class MaintenanceRepository { return res.rows[0] ? this.mapMaintenanceRecord(res.rows[0]) : null; } + async batchInsertRecords( + records: Array<{ + id: string; + userId: string; + vehicleId: string; + category: MaintenanceCategory; + subtypes: string[]; + date: string; + odometerReading?: number | null; + cost?: number | null; + shopName?: string | null; + notes?: string | null; + }>, + client?: any + ): Promise { + if (records.length === 0) { + return []; + } + + // Multi-value INSERT for performance (avoids N round-trips) + const queryClient = client || this.db; + const placeholders: string[] = []; + const values: any[] = []; + let paramCount = 1; + + records.forEach((record) => { + const recordParams = [ + record.id, + record.userId, + record.vehicleId, + record.category, + record.subtypes, + record.date, + record.odometerReading ?? null, + record.cost ?? null, + record.shopName ?? null, + record.notes ?? null + ]; + + const placeholder = `($${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}::text[], $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++})`; + placeholders.push(placeholder); + values.push(...recordParams); + }); + + const query = ` + INSERT INTO maintenance_records ( + id, user_id, vehicle_id, category, subtypes, date, odometer_reading, cost, shop_name, notes + ) + VALUES ${placeholders.join(', ')} + RETURNING * + `; + + const result = await queryClient.query(query, values); + return result.rows.map((row: any) => this.mapMaintenanceRecord(row)); + } + async deleteRecord(id: string, userId: string): Promise { await this.db.query( `DELETE FROM maintenance_records WHERE id = $1 AND user_id = $2`, @@ -336,6 +392,80 @@ export class MaintenanceRepository { return res.rows[0] ? this.mapMaintenanceSchedule(res.rows[0]) : null; } + async batchInsertSchedules( + schedules: Array<{ + id: string; + userId: string; + vehicleId: string; + category: MaintenanceCategory; + subtypes: string[]; + intervalMonths?: number | null; + intervalMiles?: number | null; + lastServiceDate?: string | null; + lastServiceMileage?: number | null; + nextDueDate?: string | null; + nextDueMileage?: number | null; + isActive: boolean; + emailNotifications?: boolean; + scheduleType?: string; + fixedDueDate?: string | null; + reminderDays1?: number | null; + reminderDays2?: number | null; + reminderDays3?: number | null; + }>, + client?: any + ): Promise { + if (schedules.length === 0) { + return []; + } + + // Multi-value INSERT for performance (avoids N round-trips) + const queryClient = client || this.db; + const placeholders: string[] = []; + const values: any[] = []; + let paramCount = 1; + + schedules.forEach((schedule) => { + const scheduleParams = [ + schedule.id, + schedule.userId, + schedule.vehicleId, + schedule.category, + schedule.subtypes, + schedule.intervalMonths ?? null, + schedule.intervalMiles ?? null, + schedule.lastServiceDate ?? null, + schedule.lastServiceMileage ?? null, + schedule.nextDueDate ?? null, + schedule.nextDueMileage ?? null, + schedule.isActive, + schedule.emailNotifications ?? false, + schedule.scheduleType ?? 'interval', + schedule.fixedDueDate ?? null, + schedule.reminderDays1 ?? null, + schedule.reminderDays2 ?? null, + schedule.reminderDays3 ?? null + ]; + + const placeholder = `($${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}::text[], $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++})`; + placeholders.push(placeholder); + values.push(...scheduleParams); + }); + + const query = ` + INSERT INTO maintenance_schedules ( + id, user_id, vehicle_id, category, subtypes, interval_months, interval_miles, + last_service_date, last_service_mileage, next_due_date, next_due_mileage, is_active, email_notifications, + schedule_type, fixed_due_date, reminder_days_1, reminder_days_2, reminder_days_3 + ) + VALUES ${placeholders.join(', ')} + RETURNING * + `; + + const result = await queryClient.query(query, values); + return result.rows.map((row: any) => this.mapMaintenanceSchedule(row)); + } + async deleteSchedule(id: string, userId: string): Promise { await this.db.query( `DELETE FROM maintenance_schedules WHERE id = $1 AND user_id = $2`, diff --git a/backend/src/features/vehicles/data/vehicles.repository.ts b/backend/src/features/vehicles/data/vehicles.repository.ts index cd9311b..3f2df8a 100644 --- a/backend/src/features/vehicles/data/vehicles.repository.ts +++ b/backend/src/features/vehicles/data/vehicles.repository.ts @@ -164,6 +164,57 @@ export class VehiclesRepository { return this.mapRow(result.rows[0]); } + async batchInsert( + vehicles: Array, + client?: any + ): Promise { + if (vehicles.length === 0) { + return []; + } + + // Multi-value INSERT for performance (avoids N round-trips) + const queryClient = client || this.pool; + const placeholders: string[] = []; + const values: any[] = []; + let paramCount = 1; + + vehicles.forEach((vehicle) => { + const vehicleParams = [ + vehicle.userId, + (vehicle.vin && vehicle.vin.trim().length > 0) ? vehicle.vin.trim() : null, + vehicle.make, + vehicle.model, + vehicle.year, + vehicle.engine, + vehicle.transmission, + vehicle.trimLevel, + vehicle.driveType, + vehicle.fuelType, + vehicle.nickname, + vehicle.color, + vehicle.licensePlate, + vehicle.odometerReading || 0 + ]; + + const placeholder = `($${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++})`; + placeholders.push(placeholder); + values.push(...vehicleParams); + }); + + const query = ` + INSERT INTO vehicles ( + user_id, vin, make, model, year, + engine, transmission, trim_level, drive_type, fuel_type, + nickname, color, license_plate, odometer_reading + ) + VALUES ${placeholders.join(', ')} + RETURNING * + `; + + const result = await queryClient.query(query, values); + return result.rows.map((row: any) => this.mapRow(row)); + } + async softDelete(id: string): Promise { const query = ` UPDATE vehicles diff --git a/backend/src/features/vehicles/tests/integration/vehicles.integration.test.ts b/backend/src/features/vehicles/tests/integration/vehicles.integration.test.ts index 5ef2912..ea9062b 100644 --- a/backend/src/features/vehicles/tests/integration/vehicles.integration.test.ts +++ b/backend/src/features/vehicles/tests/integration/vehicles.integration.test.ts @@ -12,9 +12,8 @@ import fastifyPlugin from 'fastify-plugin'; // Mock auth plugin to bypass JWT validation in tests jest.mock('../../../../core/plugins/auth.plugin', () => { - const fp = require('fastify-plugin'); return { - default: fp(async function(fastify: any) { + default: fastifyPlugin(async function(fastify: any) { fastify.decorate('authenticate', async function(request: any, _reply: any) { request.user = { sub: 'test-user-123' }; });