src/clients/clients.service.ts
Methods |
constructor(prisma: PrismaService)
|
||||||
|
Defined in src/clients/clients.service.ts:21
|
||||||
|
Parameters :
|
| Async bulkUploadFromFile | |||||||||
bulkUploadFromFile(organizationId: string, file: Express.Multer.File)
|
|||||||||
|
Defined in src/clients/clients.service.ts:121
|
|||||||||
|
Parameters :
Returns :
Promise<BulkUploadResult>
|
| Async create | |||||||||
create(organizationId: string, dto: CreateClientDto)
|
|||||||||
|
Defined in src/clients/clients.service.ts:25
|
|||||||||
|
Parameters :
Returns :
unknown
|
| Async findAll | |||||||||
findAll(organizationId: string, params: PaginationParams)
|
|||||||||
|
Defined in src/clients/clients.service.ts:42
|
|||||||||
|
Parameters :
Returns :
unknown
|
| Async findOne |
findOne(organizationId: string, id: string)
|
|
Defined in src/clients/clients.service.ts:70
|
|
Org-scoped lookup — cross-tenant ID guessing impossible.
Returns :
unknown
|
| Async remove |
remove(organizationId: string, id: string)
|
|
Defined in src/clients/clients.service.ts:101
|
|
Returns :
unknown
|
| Async update | ||||||||||||
update(organizationId: string, id: string, dto: UpdateClientDto)
|
||||||||||||
|
Defined in src/clients/clients.service.ts:79
|
||||||||||||
|
Parameters :
Returns :
unknown
|
import {
BadRequestException,
ConflictException,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateClientDto } from './dto/create-client.dto';
import { UpdateClientDto } from './dto/update-client.dto';
import {
buildPaginationQuery,
buildPaginationMeta,
PaginationParams,
} from '../common/utils/pagination.util';
import { BulkUploadService, CLIENT_MAPPINGS, BulkUploadResult } from '../common/services/bulk-upload.service';
@Injectable()
export class ClientsService {
private readonly logger = new Logger(ClientsService.name);
private readonly bulkUpload = new BulkUploadService();
constructor(private prisma: PrismaService) {}
async create(organizationId: string, dto: CreateClientDto) {
// Per-org uniqueness on code (composite unique backs this).
const existing = await this.prisma.client.findFirst({
where: { organizationId, code: dto.code },
});
if (existing) {
throw new ConflictException(`A client with code "${dto.code}" already exists in this organization`);
}
return this.prisma.client.create({
data: {
organizationId,
...dto,
},
});
}
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' } },
{ city: { contains: params.search, mode: 'insensitive' } },
{ region: { contains: params.search, mode: 'insensitive' } },
];
}
const [items, total] = await Promise.all([
this.prisma.client.findMany({
where,
skip,
take,
orderBy,
include: { _count: { select: { orders: true } } },
}),
this.prisma.client.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 client = await this.prisma.client.findFirst({
where: { id, organizationId },
include: { _count: { select: { orders: true, rates: true } } },
});
if (!client) throw new NotFoundException('Client not found');
return client;
}
async update(organizationId: string, id: string, dto: UpdateClientDto) {
await this.findOne(organizationId, id);
if (dto.code) {
const existing = await this.prisma.client.findFirst({
where: {
organizationId,
code: dto.code,
NOT: { id },
},
});
if (existing) {
throw new ConflictException(`Another client with code "${dto.code}" already exists`);
}
}
return this.prisma.client.update({
where: { id },
data: dto,
});
}
async remove(organizationId: string, id: string) {
const client = await this.findOne(organizationId, id);
// Block delete if there are ANY orders. Order has ON DELETE RESTRICT
// on its client FK so the cascade would fail anyway, but a clean
// BadRequest with the count is far friendlier than a 500 leaking the
// constraint name.
const orderCount = client._count?.orders ?? 0;
if (orderCount > 0) {
throw new BadRequestException(
`Client has ${orderCount} order${
orderCount === 1 ? '' : 's'
} in history. Reassign or archive them before deleting.`,
);
}
await this.prisma.client.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, CLIENT_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;
}
// Per-org uniqueness check (was previously global — could collide
// across tenants and silently skip)
const existing = await this.prisma.client.findFirst({
where: { organizationId, code },
});
if (existing) {
skipped++;
continue;
}
const client = await this.prisma.client.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,
address: (row.address as string) || undefined,
city: (row.city as string) || undefined,
country: (row.country as string) || 'Kenya',
},
});
created.push(client);
}
this.logger.log(`Bulk client upload: ${created.length} created, ${skipped} skipped, ${errors.length} errors`);
return { created: created.length, skipped, errors, records: created, totalRows: rows.length };
}
}