Integra módulo de Resellers (agente eden-commercial)
Resolve conflitos de merge (users.ts, audit.ts, br-documents.ts, pnpm-lock/workspace.yaml — o worktree partiu de uma base anterior à existência do módulo de auth/companies, então recriou versões próprias de arquivos que já existiam no main; mantido o conteúdo do main, incorporando só a função nova assignUserToReseller). Adiciona resellersRepo ao barrel @eden/database e ResellersModule ao app.module.ts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import { CompaniesModule } from "./companies/companies.module.js";
|
|||||||
import { UsersModule } from "./users/users.module.js";
|
import { UsersModule } from "./users/users.module.js";
|
||||||
import { RolesModule } from "./roles/roles.module.js";
|
import { RolesModule } from "./roles/roles.module.js";
|
||||||
import { ProductsModule } from "./products/products.module.js";
|
import { ProductsModule } from "./products/products.module.js";
|
||||||
|
import { ResellersModule } from "./resellers/resellers.module.js";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -19,6 +20,7 @@ import { ProductsModule } from "./products/products.module.js";
|
|||||||
UsersModule,
|
UsersModule,
|
||||||
RolesModule,
|
RolesModule,
|
||||||
ProductsModule,
|
ProductsModule,
|
||||||
|
ResellersModule,
|
||||||
],
|
],
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
})
|
})
|
||||||
|
|||||||
15
apps/api/src/resellers/dto/assign-user-to-reseller.dto.ts
Normal file
15
apps/api/src/resellers/dto/assign-user-to-reseller.dto.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { IsUUID, ValidateIf } from "class-validator";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `resellerId` is required-but-nullable (unassign is expressed as an
|
||||||
|
* explicit `null`, not by omitting the field) — @ValidateIf lets a null
|
||||||
|
* value through while still requiring a valid UUID whenever it's present.
|
||||||
|
*/
|
||||||
|
export class AssignUserToResellerDto {
|
||||||
|
@IsUUID()
|
||||||
|
userId!: string;
|
||||||
|
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsUUID()
|
||||||
|
resellerId!: string | null;
|
||||||
|
}
|
||||||
11
apps/api/src/resellers/dto/create-reseller.dto.ts
Normal file
11
apps/api/src/resellers/dto/create-reseller.dto.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { IsBoolean, IsEmail, IsOptional, IsString, MinLength } from "class-validator";
|
||||||
|
|
||||||
|
export class CreateResellerDto {
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1)
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsOptional() @IsString() document?: string;
|
||||||
|
@IsOptional() @IsEmail() contactEmail?: string;
|
||||||
|
@IsOptional() @IsBoolean() active?: boolean;
|
||||||
|
}
|
||||||
4
apps/api/src/resellers/dto/update-reseller.dto.ts
Normal file
4
apps/api/src/resellers/dto/update-reseller.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from "@nestjs/swagger";
|
||||||
|
import { CreateResellerDto } from "./create-reseller.dto.js";
|
||||||
|
|
||||||
|
export class UpdateResellerDto extends PartialType(CreateResellerDto) {}
|
||||||
82
apps/api/src/resellers/resellers.controller.ts
Normal file
82
apps/api/src/resellers/resellers.controller.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common";
|
||||||
|
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { ResellersService } from "./resellers.service.js";
|
||||||
|
import { CreateResellerDto } from "./dto/create-reseller.dto.js";
|
||||||
|
import { UpdateResellerDto } from "./dto/update-reseller.dto.js";
|
||||||
|
import { AssignUserToResellerDto } from "./dto/assign-user-to-reseller.dto.js";
|
||||||
|
import { JwtAuthGuard } from "../auth/jwt-auth.guard.js";
|
||||||
|
import { PermissionGuard } from "../auth/permission.guard.js";
|
||||||
|
import { RequirePermission } from "../auth/decorators/require-permission.decorator.js";
|
||||||
|
import { CurrentUser } from "../auth/decorators/current-user.decorator.js";
|
||||||
|
import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operational reseller CRUD (eden.md §1.7, §7) — MVP scope only. The full
|
||||||
|
* "Programa de Canais" approval workflow with public tokens (eden.md §6)
|
||||||
|
* is a separate, later phase and isn't touched here.
|
||||||
|
*
|
||||||
|
* Read is open to any authenticated user (no @RequirePermission) —
|
||||||
|
* mirrors Companies: needed for reseller-selector dropdowns elsewhere
|
||||||
|
* (e.g. Orçamentos, cadastro de usuário) and isn't sensitive data.
|
||||||
|
* Write requires `resellers:edit`.
|
||||||
|
*
|
||||||
|
* Deliberate deviation from legacy, noted per eden-commercial rules:
|
||||||
|
* the legacy (`resellers.routes.js`, eden.md §7) hardcoded this whole
|
||||||
|
* CRUD to `requireAdmin` (a role check — admin/super_admin only,
|
||||||
|
* stricter than the revenda cadastro/aprovação feature). EDEN
|
||||||
|
* generalizes that role check to the resource+action+scope permission
|
||||||
|
* model (docs/adr/0004-permission-model.md) instead, so a role can be
|
||||||
|
* granted `resellers:edit` without needing the full `admin` role.
|
||||||
|
*/
|
||||||
|
@ApiTags("resellers")
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(JwtAuthGuard, PermissionGuard)
|
||||||
|
@Controller("v1/resellers")
|
||||||
|
export class ResellersController {
|
||||||
|
constructor(private readonly resellers: ResellersService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
list(@Query("includeInactive") includeInactive?: string) {
|
||||||
|
return this.resellers.list(includeInactive === "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(":id")
|
||||||
|
get(@Param("id") id: string) {
|
||||||
|
return this.resellers.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@RequirePermission("resellers", "edit")
|
||||||
|
create(@Body() dto: CreateResellerDto, @CurrentUser() user: EdenAuthenticatedUser) {
|
||||||
|
return this.resellers.create(dto, user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(":id")
|
||||||
|
@RequirePermission("resellers", "edit")
|
||||||
|
update(
|
||||||
|
@Param("id") id: string,
|
||||||
|
@Body() dto: UpdateResellerDto,
|
||||||
|
@CurrentUser() user: EdenAuthenticatedUser,
|
||||||
|
) {
|
||||||
|
return this.resellers.update(id, dto, user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy (eden.md §7) exposed reseller↔user assignment as a
|
||||||
|
* users-domain route: `PATCH /users/:id { reseller_id }`. EDEN
|
||||||
|
* deliberately places the same operation here instead — documented
|
||||||
|
* deviation — so it stays gated by `resellers:edit` (matching the
|
||||||
|
* legacy's `requireAdmin` scope for this screen) rather than needing a
|
||||||
|
* separate `users:edit` grant just to manage a reseller's roster from
|
||||||
|
* the Resellers screen.
|
||||||
|
*/
|
||||||
|
@Patch(":id/assign-user")
|
||||||
|
@RequirePermission("resellers", "edit")
|
||||||
|
assignUser(
|
||||||
|
@Param("id") id: string,
|
||||||
|
@Body() dto: AssignUserToResellerDto,
|
||||||
|
@CurrentUser() user: EdenAuthenticatedUser,
|
||||||
|
) {
|
||||||
|
return this.resellers.assignUser(id, dto, user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
apps/api/src/resellers/resellers.module.ts
Normal file
11
apps/api/src/resellers/resellers.module.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { AuthModule } from "../auth/auth.module.js";
|
||||||
|
import { ResellersController } from "./resellers.controller.js";
|
||||||
|
import { ResellersService } from "./resellers.service.js";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [AuthModule],
|
||||||
|
controllers: [ResellersController],
|
||||||
|
providers: [ResellersService],
|
||||||
|
})
|
||||||
|
export class ResellersModule {}
|
||||||
101
apps/api/src/resellers/resellers.service.ts
Normal file
101
apps/api/src/resellers/resellers.service.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async assignUser(
|
||||||
|
id: string,
|
||||||
|
dto: AssignUserToResellerDto,
|
||||||
|
actorId: string,
|
||||||
|
): Promise<{ userId: string; resellerId: string | null }> {
|
||||||
|
await this.get(id); // 404 if the reseller in the URL doesn't exist
|
||||||
|
if (dto.resellerId !== null && dto.resellerId !== id) {
|
||||||
|
await this.get(dto.resellerId); // 404 if a different target reseller doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
const found = await usersRepo.assignUserToReseller(dto.userId, dto.resellerId);
|
||||||
|
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: dto.resellerId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { userId: dto.userId, resellerId: dto.resellerId };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
packages/database/migrations/1788439142825_resellers.cjs
Normal file
59
packages/database/migrations/1788439142825_resellers.cjs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Commercial bounded context, first slice: `resellers` — the operational
|
||||||
|
* reseller/partner registry (eden.md §1.7). Pre-dates the "Programa de
|
||||||
|
* Canais" approval workflow with public tokens described in eden.md §6 —
|
||||||
|
* that workflow (client_registrations/reseller_registrations, public
|
||||||
|
* approval tokens) is explicitly out of scope for this migration and
|
||||||
|
* lands in its own later phase. This is just the simple operational CRUD
|
||||||
|
* table eden.md §1.7 describes, already referenced by `quotes.reseller_id`
|
||||||
|
* conceptually (quotes themselves aren't modeled yet either — Commercial
|
||||||
|
* phase lands module by module).
|
||||||
|
*
|
||||||
|
* Also adds `users.reseller_id` (nullable) — how a user gets scoped to a
|
||||||
|
* reseller, per eden.md §7 and the `scope=reseller` permission model
|
||||||
|
* (docs/adr/0004-permission-model.md). Lives in this migration rather
|
||||||
|
* than a separate one so the FK to `resellers(id)` can be added in the
|
||||||
|
* same statement the table is created in, without a two-step migration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.shorthands = undefined;
|
||||||
|
|
||||||
|
exports.up = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
CREATE TABLE resellers (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
created_by UUID REFERENCES users(id),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
document TEXT,
|
||||||
|
contact_email TEXT,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT true
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE resellers IS
|
||||||
|
'Cadastro operacional de revenda — eden.md §1.7. document é CPF/CNPJ livre (sem CHECK de dígito no banco, validado na camada de aplicação, igual companies.cnpj). Leitura aberta a qualquer autenticado (usado em seletores, ex. Orçamentos); escrita restrita à permissão resellers:edit.';
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_resellers_updated_at BEFORE UPDATE ON resellers
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
CREATE INDEX idx_resellers_active ON resellers(active);
|
||||||
|
|
||||||
|
-- =====================================================================
|
||||||
|
-- users.reseller_id — scopes a user to a reseller (eden.md §7,
|
||||||
|
-- docs/adr/0004-permission-model.md scope=reseller)
|
||||||
|
-- =====================================================================
|
||||||
|
ALTER TABLE users ADD COLUMN reseller_id UUID REFERENCES resellers(id);
|
||||||
|
COMMENT ON COLUMN users.reseller_id IS
|
||||||
|
'Nullable — vincula o usuário a uma revenda (vendedor de revenda), escopando o que ele vê (eden.md §7). Atribuído via PATCH /v1/resellers/:id/assign-user, nunca aceito de campo vindo do cliente para decisão de autorização (docs/architecture.md, ADR-0004).';
|
||||||
|
|
||||||
|
CREATE INDEX idx_users_reseller ON users(reseller_id);
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
DROP INDEX IF EXISTS idx_users_reseller;
|
||||||
|
ALTER TABLE users DROP COLUMN IF EXISTS reseller_id;
|
||||||
|
DROP TABLE IF EXISTS resellers;
|
||||||
|
`);
|
||||||
|
};
|
||||||
@@ -5,6 +5,8 @@ export * as sessionsRepo from "./repositories/sessions.js";
|
|||||||
export * as auditRepo from "./repositories/audit.js";
|
export * as auditRepo from "./repositories/audit.js";
|
||||||
export * as companiesRepo from "./repositories/companies.js";
|
export * as companiesRepo from "./repositories/companies.js";
|
||||||
export * as productsRepo from "./repositories/products.js";
|
export * as productsRepo from "./repositories/products.js";
|
||||||
|
export * as resellersRepo from "./repositories/resellers.js";
|
||||||
export type * from "./repositories/types.js";
|
export type * from "./repositories/types.js";
|
||||||
export type { Company } from "./repositories/companies.js";
|
export type { Company } from "./repositories/companies.js";
|
||||||
export type { Product, CreateProductInput, UpdateProductInput } from "./repositories/products.js";
|
export type { Product, CreateProductInput, UpdateProductInput } from "./repositories/products.js";
|
||||||
|
export type { Reseller, CreateResellerInput, UpdateResellerInput } from "./repositories/resellers.js";
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ function canonicalJson(value: unknown): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends one event to the audit_log hash chain. Never call this inside a
|
* Appends one event to the audit_log hash chain (see baseline-schema
|
||||||
* larger transaction that might roll back for unrelated reasons without
|
* migration and .claude/skills/eden-security/references/audit.md). Never
|
||||||
* considering that the chain must stay contiguous — a rolled-back event
|
* call this inside a larger transaction that might roll back for
|
||||||
* never happened, which is fine (nothing was recorded), but a *committed*
|
* unrelated reasons without considering that the chain must stay
|
||||||
* event must never be skipped when computing previous_event_hash for the
|
* contiguous — a rolled-back event never happened (fine, nothing was
|
||||||
* next one. See .claude/skills/eden-security/references/audit.md.
|
* recorded), but a *committed* event must never be skipped when
|
||||||
|
* computing previous_event_hash for the next one.
|
||||||
*/
|
*/
|
||||||
export async function appendAuditEvent(input: AuditEventInput): Promise<void> {
|
export async function appendAuditEvent(input: AuditEventInput): Promise<void> {
|
||||||
const { rows } = await query<{ event_hash: string }>(
|
const { rows } = await query<{ event_hash: string }>(
|
||||||
|
|||||||
71
packages/database/src/repositories/resellers.ts
Normal file
71
packages/database/src/repositories/resellers.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { query } from "../pool.js";
|
||||||
|
|
||||||
|
export interface Reseller {
|
||||||
|
id: string;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
created_by: string | null;
|
||||||
|
name: string;
|
||||||
|
document: string | null;
|
||||||
|
contact_email: string | null;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listResellers(includeInactive = false): Promise<Reseller[]> {
|
||||||
|
const { rows } = await query<Reseller>(
|
||||||
|
includeInactive
|
||||||
|
? "SELECT * FROM resellers ORDER BY created_at ASC"
|
||||||
|
: "SELECT * FROM resellers WHERE active = true ORDER BY created_at ASC",
|
||||||
|
);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getResellerById(id: string): Promise<Reseller | null> {
|
||||||
|
const { rows } = await query<Reseller>("SELECT * FROM resellers WHERE id = $1", [id]);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateResellerInput {
|
||||||
|
name: string;
|
||||||
|
document?: string | null;
|
||||||
|
contactEmail?: string | null;
|
||||||
|
active?: boolean;
|
||||||
|
createdBy?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createReseller(input: CreateResellerInput): Promise<Reseller> {
|
||||||
|
const { rows } = await query<Reseller>(
|
||||||
|
`INSERT INTO resellers (name, document, contact_email, active, created_by)
|
||||||
|
VALUES ($1, $2, $3, COALESCE($4, true), $5)
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
input.name,
|
||||||
|
input.document ?? null,
|
||||||
|
input.contactEmail ?? null,
|
||||||
|
input.active ?? null,
|
||||||
|
input.createdBy ?? null,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
const reseller = rows[0];
|
||||||
|
if (!reseller) throw new Error("failed to create reseller");
|
||||||
|
return reseller;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateResellerInput = Partial<Omit<CreateResellerInput, "createdBy">>;
|
||||||
|
|
||||||
|
export async function updateReseller(
|
||||||
|
id: string,
|
||||||
|
input: UpdateResellerInput,
|
||||||
|
): Promise<Reseller | null> {
|
||||||
|
const { rows } = await query<Reseller>(
|
||||||
|
`UPDATE resellers SET
|
||||||
|
name = COALESCE($2, name),
|
||||||
|
document = COALESCE($3, document),
|
||||||
|
contact_email = COALESCE($4, contact_email),
|
||||||
|
active = COALESCE($5, active)
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *`,
|
||||||
|
[id, input.name ?? null, input.document ?? null, input.contactEmail ?? null, input.active ?? null],
|
||||||
|
);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
@@ -124,3 +124,21 @@ export async function updateUserProfile(
|
|||||||
);
|
);
|
||||||
return rows[0] ?? null;
|
return rows[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reseller-scoping: sets or clears which reseller a user belongs to
|
||||||
|
* (`users.reseller_id`, nullable — eden.md §1.7, §7). Resolved and
|
||||||
|
* enforced server-side only; callers must never accept `reseller_id`
|
||||||
|
* from client input for authorization decisions (docs/adr/0004,
|
||||||
|
* docs/architecture.md).
|
||||||
|
*/
|
||||||
|
export async function assignUserToReseller(
|
||||||
|
userId: string,
|
||||||
|
resellerId: string | null,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { rowCount } = await query("UPDATE users SET reseller_id = $2 WHERE id = $1", [
|
||||||
|
userId,
|
||||||
|
resellerId,
|
||||||
|
]);
|
||||||
|
return (rowCount ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
* CPF/CNPJ format validation (digit-check only — never confirms a document
|
* CPF/CNPJ format validation (digit-check only — never confirms a document
|
||||||
* actually exists or belongs to whoever claims it). Ported from the
|
* actually exists or belongs to whoever claims it). Ported from the
|
||||||
* standard Brazilian algorithm, matching the legacy's isValidCPF/
|
* standard Brazilian algorithm, matching the legacy's isValidCPF/
|
||||||
* isValidCNPJ (eden.md §1 companies, §3 client registration).
|
* isValidCNPJ (eden.md §1 companies, §3 client registration, §1.7
|
||||||
|
* resellers).
|
||||||
*/
|
*/
|
||||||
export function onlyDigits(value: string): string {
|
export function onlyDigits(value: string): string {
|
||||||
return value.replace(/\D/g, "");
|
return value.replace(/\D/g, "");
|
||||||
|
|||||||
Reference in New Issue
Block a user