fix(admin): criação de tenant violava check constraint de status (#28)

POST /api/v1/admin/tenants inseria status 'onboarding', valor que a check
constraint de organizations.status (active|suspended|redacted|archived)
nunca aceitou — a rota 500ava em qualquer criação. O marcador real de
onboarding no produto é onboarded_at null (mesmo modelo do signup
self-service e do gate de /onboarding), então:

- insert passa a usar status 'active' (onboarded_at null = em onboarding)
- filtro GET status=onboarding vira estado derivado: active + onboarded_at
  is null (o dropdown "Onboarding" do admin, que nunca casava nada, agora
  funciona)
- badge da listagem deriva "Onboarding" de active + onboarded_at null
- tipos de linha (useTenantDetail, TenantActions) deixam de aceitar um
  status que o banco não produz; o tipo de FILTRO mantém 'onboarding'

Sem migration: nenhuma linha com status inválido pode existir (a
constraint sempre bloqueou o INSERT).


Claude-Session: https://claude.ai/code/session_01454SGvQ7wBAvah8ZPnnWXv

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Rafael Melgaço
2026-07-21 20:21:00 +00:00
committed by GitHub
co-authored by Claude Fable 5
parent fd1e291186
commit 73a23750b2
4 changed files with 24 additions and 9 deletions
+7 -2
View File
@@ -100,7 +100,10 @@ export async function GET(req: NextRequest) {
.order("id", { ascending: false })
.limit(limit + 1);
if (status) {
if (status === "onboarding") {
// Estado derivado: ativo no banco, onboarding ainda não concluído.
query = query.eq("status", "active").is("onboarded_at", null);
} else if (status) {
query = query.eq("status", status);
}
@@ -195,7 +198,9 @@ export async function POST(req: NextRequest) {
slug,
legal_name: legal_name ?? null,
cnpj: cnpj ?? null,
status: "onboarding",
// A check constraint de organizations.status não tem 'onboarding' — o
// marcador de onboarding é onboarded_at null (mesmo modelo do signup).
status: "active",
settings: { plan },
created_by: adminCtx.user.id,
})
+2 -2
View File
@@ -11,7 +11,7 @@ import { ImpersonateButton } from "@/components/admin/ImpersonateButton";
interface TenantActionsProps {
organizationId: string;
status: "active" | "suspended" | "onboarding" | "redacted";
status: "active" | "suspended" | "redacted";
displayName: string;
}
@@ -27,7 +27,7 @@ export function TenantActions({
const [suspendOpen, setSuspendOpen] = useState(false);
const [reactivateOpen, setReactivateOpen] = useState(false);
const canSuspend = status === "active" || status === "onboarding";
const canSuspend = status === "active";
const isSuspended = status === "suspended";
const isRedacted = status === "redacted";
+12 -4
View File
@@ -35,10 +35,18 @@ const STATUS_LABELS: Record<string, string> = {
redacted: "Redigido",
};
function StatusBadge({ status }: { status: string }) {
function StatusBadge({
status,
onboardedAt,
}: {
status: string;
onboardedAt: string | null;
}) {
// 'onboarding' não existe no banco — é derivado: ativo sem onboarding concluído.
const effective = status === "active" && !onboardedAt ? "onboarding" : status;
return (
<Badge variant={STATUS_VARIANTS[status] ?? "neutral"}>
{STATUS_LABELS[status] ?? status}
<Badge variant={STATUS_VARIANTS[effective] ?? "neutral"}>
{STATUS_LABELS[effective] ?? effective}
</Badge>
);
}
@@ -158,7 +166,7 @@ export function TenantsTable({
{shortCnpj(row.cnpj)}
</TableCell>
<TableCell>
<StatusBadge status={row.status} />
<StatusBadge status={row.status} onboardedAt={row.onboarded_at} />
</TableCell>
<TableCell className="text-right tabular-nums">
{extractCount(row.user_count)}
+3 -1
View File
@@ -12,7 +12,9 @@ export interface TenantOrganization {
display_name: string;
legal_name: string | null;
cnpj: string | null;
status: "active" | "suspended" | "onboarding" | "redacted";
// 'onboarding' não existe na check constraint do banco — é estado derivado
// (active + onboarded_at null), nunca vem numa linha real.
status: "active" | "suspended" | "redacted";
onboarded_at: string | null;
suspended_at: string | null;
created_at: string;