File

src/drivers/drivers.service.ts

Index

Methods

Constructor

constructor(prisma: PrismaService)
Parameters :
Name Type Optional
prisma PrismaService No

Methods

Async bulkUploadFromFile
bulkUploadFromFile(organizationId: string, file: Express.Multer.File)
Parameters :
Name Type Optional
organizationId string No
file Express.Multer.File No
Async create
create(organizationId: string, dto: CreateDriverDto)
Parameters :
Name Type Optional
organizationId string No
dto CreateDriverDto No
Returns : unknown
Async findAll
findAll(organizationId: string, params: PaginationParams)
Parameters :
Name Type Optional
organizationId string No
params PaginationParams No
Returns : unknown
Async findOne
findOne(organizationId: string, id: string)

Org-scoped lookup — cross-tenant ID guessing impossible.

Parameters :
Name Type Optional
organizationId string No
id string No
Returns : unknown
Async remove
remove(organizationId: string, id: string)
Parameters :
Name Type Optional
organizationId string No
id string No
Returns : unknown
Async update
update(organizationId: string, id: string, dto: UpdateDriverDto)
Parameters :
Name Type Optional
organizationId string No
id string No
dto UpdateDriverDto No
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 };
  }
}

results matching ""

    No results matching ""