src/drivers/drivers.service.ts
Methods |
constructor(prisma: PrismaService)
|
||||||
|
Defined in src/drivers/drivers.service.ts:22
|
||||||
|
Parameters :
|
| Async bulkUploadFromFile | |||||||||
bulkUploadFromFile(organizationId: string, file: Express.Multer.File)
|
|||||||||
|
Defined in src/drivers/drivers.service.ts:114
|
|||||||||
|
Parameters :
Returns :
Promise<BulkUploadResult>
|
| Async create | |||||||||
create(organizationId: string, dto: CreateDriverDto)
|
|||||||||
|
Defined in src/drivers/drivers.service.ts:26
|
|||||||||
|
Parameters :
Returns :
unknown
|
| Async findAll | |||||||||
findAll(organizationId: string, params: PaginationParams)
|
|||||||||
|
Defined in src/drivers/drivers.service.ts:40
|
|||||||||
|
Parameters :
Returns :
unknown
|
| Async findOne |
findOne(organizationId: string, id: string)
|
|
Defined in src/drivers/drivers.service.ts:68
|
|
Org-scoped lookup — cross-tenant ID guessing impossible.
Returns :
unknown
|
| Async remove |
remove(organizationId: string, id: string)
|
|
Defined in src/drivers/drivers.service.ts:89
|
|
Returns :
unknown
|
| Async update | ||||||||||||
update(organizationId: string, id: string, dto: UpdateDriverDto)
|
||||||||||||
|
Defined in src/drivers/drivers.service.ts:80
|
||||||||||||
|
Parameters :
Returns :
unknown
|
import {
BadRequestException,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateDriverDto } from './dto/create-driver.dto';
import { UpdateDriverDto } from './dto/update-driver.dto';
import {
buildPaginationQuery,
buildPaginationMeta,
PaginationParams,
} from '../common/utils/pagination.util';
import { BulkUploadService, DRIVER_MAPPINGS, BulkUploadResult } from '../common/services/bulk-upload.service';
const ACTIVE_TRIP_STATUSES = ['DISPATCHED', 'IN_TRANSIT', 'AT_STOP'] as const;
@Injectable()
export class DriversService {
private readonly logger = new Logger(DriversService.name);
private readonly bulkUpload = new BulkUploadService();
constructor(private prisma: PrismaService) {}
async create(organizationId: string, dto: CreateDriverDto) {
return this.prisma.driver.create({
data: {
organizationId,
firstName: dto.firstName,
lastName: dto.lastName,
email: dto.email,
phone: dto.phone,
licenseNumber: dto.licenseNumber,
licenseExpiry: dto.licenseExpiry ? new Date(dto.licenseExpiry) : undefined,
},
});
}
async findAll(organizationId: string, params: PaginationParams) {
const { skip, take, orderBy, page, limit } = buildPaginationQuery(params);
const where: any = { organizationId };
if (params.search) {
where.OR = [
{ firstName: { contains: params.search, mode: 'insensitive' } },
{ lastName: { contains: params.search, mode: 'insensitive' } },
{ email: { contains: params.search, mode: 'insensitive' } },
{ phone: { contains: params.search, mode: 'insensitive' } },
];
}
const [items, total] = await Promise.all([
this.prisma.driver.findMany({
where,
skip,
take,
orderBy,
include: { _count: { select: { trips: true } } },
}),
this.prisma.driver.count({ where }),
]);
return { data: items, meta: buildPaginationMeta(total, page, limit) };
}
/** Org-scoped lookup — cross-tenant ID guessing impossible. */
async findOne(organizationId: string, id: string) {
const driver = await this.prisma.driver.findFirst({
where: { id, organizationId },
include: {
trips: { take: 5, orderBy: { createdAt: 'desc' } },
_count: { select: { trips: true } },
},
});
if (!driver) throw new NotFoundException('Driver not found');
return driver;
}
async update(organizationId: string, id: string, dto: UpdateDriverDto) {
await this.findOne(organizationId, id);
const data: Record<string, unknown> = { ...dto };
if (dto.licenseExpiry !== undefined) {
data.licenseExpiry = dto.licenseExpiry ? new Date(dto.licenseExpiry) : null;
}
return this.prisma.driver.update({ where: { id }, data });
}
async remove(organizationId: string, id: string) {
await this.findOne(organizationId, id);
// Block delete if any in-flight trip references this driver. Past
// trips are OK — the FK is ON DELETE SET NULL so historical trips
// survive (with the driver shown as "—" instead of name).
const inFlight = await this.prisma.trip.count({
where: {
organizationId,
driverId: id,
status: { in: [...ACTIVE_TRIP_STATUSES] as any },
},
});
if (inFlight > 0) {
throw new BadRequestException(
`Driver has ${inFlight} in-flight trip${
inFlight === 1 ? '' : 's'
}. Complete or reassign before deleting.`,
);
}
await this.prisma.driver.delete({ where: { id } });
return { deleted: true };
}
async bulkUploadFromFile(organizationId: string, file: Express.Multer.File): Promise<BulkUploadResult> {
const { rows, errors } = this.bulkUpload.parseFile(file.buffer, file.originalname, DRIVER_MAPPINGS);
const created: Record<string, unknown>[] = [];
let skipped = 0;
for (const row of rows) {
const firstName = String(row.firstName || '').trim();
const lastName = String(row.lastName || '').trim();
if (!firstName || !lastName) {
skipped++;
continue;
}
const email = row.email ? String(row.email).trim().toLowerCase() : null;
// Duplicate detection — by email if provided, otherwise by full
// name (case-insensitive). Always scoped to the calling org.
if (email) {
const existing = await this.prisma.driver.findFirst({
where: { organizationId, email },
});
if (existing) {
skipped++;
continue;
}
} else {
const existing = await this.prisma.driver.findFirst({
where: {
organizationId,
firstName: { equals: firstName, mode: 'insensitive' },
lastName: { equals: lastName, mode: 'insensitive' },
},
});
if (existing) {
skipped++;
continue;
}
}
const licenseExpiry = row.licenseExpiry ? new Date(String(row.licenseExpiry)) : undefined;
const driver = await this.prisma.driver.create({
data: {
organizationId,
firstName,
lastName,
email: email || undefined,
phone: (row.phone as string) || undefined,
licenseNumber: (row.licenseNumber as string) || undefined,
licenseExpiry: licenseExpiry && !isNaN(licenseExpiry.getTime()) ? licenseExpiry : undefined,
},
});
created.push(driver);
}
this.logger.log(`Bulk driver upload: ${created.length} created, ${skipped} skipped, ${errors.length} errors`);
return { created: created.length, skipped, errors, records: created, totalRows: rows.length };
}
}