Add operational Resellers module (MVP CRUD, eden.md §1.7/§7)

Migration 1788439142825_resellers.cjs: `resellers` table plus nullable
`users.reseller_id` (scope=reseller per ADR-0004), indexes on
resellers.active and users.reseller_id.

packages/database/src/repositories/resellers.ts: list/get/create/update,
COALESCE-based partial update, mirroring the companies repository
pattern. Also adds repositories/audit.ts (hash-chained appendAuditEvent
over the existing audit_log table) and repositories/users.ts
(assignUserToReseller) - foundational pieces this module depends on that
hadn't landed in this worktree yet; audit.ts mirrors the sibling
worktree's already-built implementation verbatim to minimize merge
friction, and users.ts intentionally contains only the one function the
task asked to append (the fuller users repository belongs to the
auth/identity module being built in parallel).

apps/api/src/resellers/*: controller/service/module/dto mirroring the
Companies module's conventions - read open to any authenticated user,
write gated by @RequirePermission('resellers', 'edit'). Deliberate
deviation from legacy noted in code comments: the legacy hardcoded this
CRUD to requireAdmin and exposed reseller<->user assignment as
`PATCH /users/:id`; EDEN generalizes to the permission model (ADR-0004)
and adds `PATCH /v1/resellers/:id/assign-user` instead. Audits
reseller.created / reseller.updated / reseller.user_assigned.

packages/domain-shared/src/validators/br-documents.ts: CPF/CNPJ
digit-check validators (mirrors the sibling worktree's implementation
verbatim), used for the reseller `document` field the same way Companies
validates cnpj.

Known integration gap (by design, not a bug in this module): this
worktree doesn't yet have apps/api/src/auth/* or the companies-phase
exports in packages/database/src/index.ts - both are being built in
sibling worktrees and are explicitly out of scope here (per the task's
boundaries, app.module.ts and packages/database/src/index.ts are left
untouched for the user to wire up at merge time). @eden/database
typecheck/build pass cleanly; @eden/api typecheck/build currently fail
with exactly ten "cannot find module ../auth/..." /
"no exported member resellersRepo/usersRepo/auditRepo/Reseller" errors,
all attributable to those two integration points and nothing else.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 09:44:45 -03:00
parent 44510bd019
commit dca6eb2766
15 changed files with 653 additions and 14 deletions

View File

@@ -5,3 +5,4 @@
* bounded context needs to share a type with the first.
*/
export const DOMAIN_SHARED_PACKAGE_VERSION = "0.1.0";
export { onlyDigits, isValidCPF, isValidCNPJ } from "./validators/br-documents.js";

View File

@@ -0,0 +1,50 @@
/**
* CPF/CNPJ format validation (digit-check only — never confirms a document
* actually exists or belongs to whoever claims it). Ported from the
* standard Brazilian algorithm, matching the legacy's isValidCPF/
* isValidCNPJ (eden.md §1 companies, §3 client registration, §1.7
* resellers).
*/
export function onlyDigits(value: string): string {
return value.replace(/\D/g, "");
}
export function isValidCPF(value: string): boolean {
const cpf = onlyDigits(value);
if (cpf.length !== 11 || /^(\d)\1{10}$/.test(cpf)) return false;
const digits = cpf.split("").map(Number);
const checkDigit = (length: number): number => {
let sum = 0;
for (let i = 0; i < length; i++) {
sum += digits[i]! * (length + 1 - i);
}
const remainder = (sum * 10) % 11;
return remainder === 10 ? 0 : remainder;
};
return checkDigit(9) === digits[9] && checkDigit(10) === digits[10];
}
export function isValidCNPJ(value: string): boolean {
const cnpj = onlyDigits(value);
if (cnpj.length !== 14 || /^(\d)\1{13}$/.test(cnpj)) return false;
const digits = cnpj.split("").map(Number);
// Standard CNPJ weight sequences — the second is the first with a
// leading 6, not a generic slice (easy to get subtly wrong otherwise).
const WEIGHTS_12 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
const WEIGHTS_13 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
const checkDigit = (length: number, weights: number[]): number => {
let sum = 0;
for (let i = 0; i < length; i++) {
sum += digits[i]! * weights[i]!;
}
const remainder = sum % 11;
return remainder < 2 ? 0 : 11 - remainder;
};
return (
checkDigit(12, WEIGHTS_12) === digits[12] && checkDigit(13, WEIGHTS_13) === digits[13]
);
}