Bug de design encontrado em revisão: PATCH /v1/resellers/:id/assign-user aceitava um resellerId diferente no corpo, e a implementação usava esse valor do corpo como alvo real da atribuição — o :id da URL virava só uma checagem de existência, não o alvo de fato. Corrigido: o :id da URL é agora a única fonte de verdade; desatribuição (reseller_id = null) virou um endpoint próprio (POST /v1/resellers/unassign-user), já que 'nenhuma revenda' não pertence à URL de nenhuma revenda específica. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
116 lines
3.9 KiB
TypeScript
116 lines
3.9 KiB
TypeScript
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<Reseller[]> {
|
|
return resellersRepo.listResellers(includeInactive);
|
|
}
|
|
|
|
async get(id: string): Promise<Reseller> {
|
|
const reseller = await resellersRepo.getResellerById(id);
|
|
if (!reseller) throw new NotFoundException("Revenda não encontrada");
|
|
return reseller;
|
|
}
|
|
|
|
async create(dto: CreateResellerDto, actorId: string): Promise<Reseller> {
|
|
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<Reseller> {
|
|
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");
|
|
}
|
|
}
|
|
}
|