File

src/transporters/transporters.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: CreateTransporterDto, userId?: string)
Parameters :
Name Type Optional
organizationId string No
dto CreateTransporterDto No
userId string Yes
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: UpdateTransporterDto, userId?: string)
Parameters :
Name Type Optional
organizationId string No
id string No
dto UpdateTransporterDto No
userId string Yes
Returns : unknown
import {
  BadRequestException,
  ConflictException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateTransporterDto } from './dto/create-transporter.dto';
import { UpdateTransporterDto } from './dto/update-transporter.dto';
import {
  buildPaginationQuery,
  buildPaginationMeta,
  PaginationParams,
} from '../common/utils/pagination.util';
import { BulkUploadService, TRANSPORTER_MAPPINGS, BulkUploadResult } from '../common/services/bulk-upload.service';

@Injectable()
export class TransportersService {
  private readonly logger = new Logger(TransportersService.name);
  private readonly bulkUpload = new BulkUploadService();

  constructor(private prisma: PrismaService) {}

  async create(organizationId: string, dto: CreateTransporterDto, userId?: string) {
    const existing = await this.prisma.transporter.findFirst({
      where: { organizationId, code: dto.code },
    });
    if (existing) {
      throw new ConflictException(`A carrier with code "${dto.code}" already exists in this organization`);
    }

    return this.prisma.transporter.create({
      data: {
        organizationId,
        name: dto.name,
        code: dto.code,
        contactName: dto.contactName,
        contactEmail: dto.contactEmail,
        contactPhone: dto.contactPhone,
        createdById: userId,
      },
    });
  }

  async findAll(organizationId: string, params: PaginationParams) {
    const { skip, take, orderBy, page, limit } = buildPaginationQuery(params);

    const where: any = { organizationId };
    if (params.search) {
      where.OR = [
        { name: { contains: params.search, mode: 'insensitive' } },
        { code: { contains: params.search, mode: 'insensitive' } },
      ];
    }

    const [items, total] = await Promise.all([
      this.prisma.transporter.findMany({
        where,
        skip,
        take,
        orderBy,
        include: { _count: { select: { vehicles: true } } },
      }),
      this.prisma.transporter.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 transporter = await this.prisma.transporter.findFirst({
      where: { id, organizationId },
      include: {
        vehicles: {
          select: { id: true, unitNumber: true, type: true, status: true },
        },
        _count: { select: { vehicles: true } },
      },
    });
    if (!transporter) throw new NotFoundException('Carrier not found');
    return transporter;
  }

  async update(
    organizationId: string,
    id: string,
    dto: UpdateTransporterDto,
    userId?: string,
  ) {
    await this.findOne(organizationId, id);

    if (dto.code) {
      const existing = await this.prisma.transporter.findFirst({
        where: { organizationId, code: dto.code, NOT: { id } },
      });
      if (existing) {
        throw new ConflictException(`Another carrier with code "${dto.code}" already exists`);
      }
    }

    return this.prisma.transporter.update({
      where: { id },
      data: { ...dto, updatedById: userId },
    });
  }

  async remove(organizationId: string, id: string) {
    const transporter = await this.findOne(organizationId, id);

    // ABSOLUTELY block deletion if any vehicles are still parented to
    // this carrier. The previous behaviour cascade-deleted vehicles AND
    // their entire trip history — catastrophic.
    const vehicleCount = transporter._count?.vehicles ?? 0;
    if (vehicleCount > 0) {
      throw new BadRequestException(
        `Carrier owns ${vehicleCount} vehicle${
          vehicleCount === 1 ? '' : 's'
        }. Reassign or remove the vehicles first.`,
      );
    }

    await this.prisma.transporter.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, TRANSPORTER_MAPPINGS);
    const created: Record<string, unknown>[] = [];
    let skipped = 0;

    for (const row of rows) {
      const code = String(row.code || '').trim().toUpperCase();
      if (!code || !/^[A-Z0-9_-]{2,20}$/.test(code)) {
        skipped++;
        continue;
      }

      const existing = await this.prisma.transporter.findFirst({
        where: { organizationId, code },
      });
      if (existing) {
        skipped++;
        continue;
      }

      const transporter = await this.prisma.transporter.create({
        data: {
          organizationId,
          name: String(row.name || '').trim(),
          code,
          contactName: (row.contactName as string) || undefined,
          contactEmail: (row.contactEmail as string) || undefined,
          contactPhone: (row.contactPhone as string) || undefined,
          isActive: true,
        },
      });
      created.push(transporter);
    }

    this.logger.log(`Bulk transporter 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 ""