import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { resellersRepo, usersRepo, auditRepo, type Reseller } from "@eden/database"; import { onlyDigits, isValidCPF, isValidCNPJ } from "@eden/domain-shared"; import type { CreateResellerDto } from "./dto/create-reseller.dto.js"; import type { UpdateResellerDto } from "./dto/update-reseller.dto.js"; import type { AssignUserToResellerDto } from "./dto/assign-user-to-reseller.dto.js"; @Injectable() export class ResellersService { list(includeInactive: boolean): Promise { return resellersRepo.listResellers(includeInactive); } async get(id: string): Promise { const reseller = await resellersRepo.getResellerById(id); if (!reseller) throw new NotFoundException("Revenda não encontrada"); return reseller; } async create(dto: CreateResellerDto, actorId: string): Promise { this.assertValidDocumentIfPresent(dto.document); const reseller = await resellersRepo.createReseller({ name: dto.name, document: dto.document ? onlyDigits(dto.document) : undefined, contactEmail: dto.contactEmail, active: dto.active, createdBy: actorId, }); await auditRepo.appendAuditEvent({ actorType: "user", actorId, action: "reseller.created", resourceType: "reseller", resourceId: reseller.id, }); return reseller; } async update(id: string, dto: UpdateResellerDto, actorId: string): Promise { await this.get(id); // 404 if missing this.assertValidDocumentIfPresent(dto.document); const reseller = await resellersRepo.updateReseller(id, { name: dto.name, document: dto.document ? onlyDigits(dto.document) : undefined, contactEmail: dto.contactEmail, active: dto.active, }); if (!reseller) throw new NotFoundException("Revenda não encontrada"); await auditRepo.appendAuditEvent({ actorType: "user", actorId, action: "reseller.updated", resourceType: "reseller", resourceId: id, metadata: { fields: Object.keys(dto) }, }); return reseller; } /** Always assigns to the reseller identified by the URL `:id` — see the DTO's comment for why that's now the only source of truth. */ async assignUser( id: string, dto: AssignUserToResellerDto, actorId: string, ): Promise<{ userId: string; resellerId: string }> { await this.get(id); // 404 if the reseller doesn't exist const found = await usersRepo.assignUserToReseller(dto.userId, id); if (!found) throw new NotFoundException("Usuário não encontrado"); await auditRepo.appendAuditEvent({ actorType: "user", actorId, action: "reseller.user_assigned", resourceType: "user", resourceId: dto.userId, metadata: { resellerId: id }, }); return { userId: dto.userId, resellerId: id }; } /** Clears a user's reseller assignment (sets `reseller_id = null`) — not scoped to any particular reseller's URL, since "no reseller" isn't a reseller. */ async unassignUser(userId: string, actorId: string): Promise<{ userId: string; resellerId: null }> { const found = await usersRepo.assignUserToReseller(userId, null); if (!found) throw new NotFoundException("Usuário não encontrado"); await auditRepo.appendAuditEvent({ actorType: "user", actorId, action: "reseller.user_unassigned", resourceType: "user", resourceId: userId, }); return { userId, resellerId: null }; } /** Mirrors Companies' assertValidCnpjIfPresent — document is CPF or CNPJ here. */ private assertValidDocumentIfPresent(document: string | undefined): void { if (!document) return; const digits = onlyDigits(document); const valid = digits.length === 11 ? isValidCPF(digits) : digits.length === 14 ? isValidCNPJ(digits) : false; if (!valid) { throw new BadRequestException("CPF/CNPJ inválido"); } } }