From 3436ae1e802800187eb9b98816b2e826fe58199f Mon Sep 17 00:00:00 2001 From: Armando Soares Date: Wed, 16 Sep 2026 05:21:20 +0000 Subject: [PATCH] tenants Cliente 1 e Cliente 2 --- README.md | 9 +- api/prisma/seed.ts | 177 ++++++++++++------- docs/ARCHITECTURE.md | 2 +- web/next.config.ts | 6 +- web/src/app/admin/(panel)/page.tsx | 2 +- web/src/components/landing/sinka-landing.tsx | 4 +- web/src/components/landing/site-header.tsx | 2 +- 7 files changed, 128 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index ae9a74f..590ec18 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ SaaS de inteligência logística. Cada operação simula frete, compara transpor - Chat interno (admin da plataforma ↔ empresa) - Arquivos de operação (CSV/XLSX) por empresa -Dois produtos no mesmo sistema: o **admin da Sinka** (`/admin`) cria e suspende empresas; cada empresa entra pelo slug na URL (`/demo`, `/outra-empresa`). +Dois produtos no mesmo sistema: o **admin da Sinka** (`/admin`) cria e suspende empresas; cada empresa entra pelo slug na URL (`/cliente-1`, `/cliente-2`). ## Isolamento @@ -68,9 +68,10 @@ docker compose --profile full up -d --build | Área | URL | E-mail | Senha | | --- | --- | --- | --- | | Plataforma | `/admin/login` | tina.r@example.net | SinkaPlatform!1 | -| Empresa (admin) | `/demo/login` | xena.w@example.org | SinkaAdmin!1 | -| Empresa (manager) | `/demo/login` | james.b@example.com | SinkaManager!1 | -| Empresa (operador) | `/demo/login` | ursula.b@example.com | SinkaOperador!1 | +| Cliente 1 (admin) | `/cliente-1/login` | xena.w@example.org | SinkaAdmin!1 | +| Cliente 1 (manager) | `/cliente-1/login` | james.b@example.com | SinkaManager!1 | +| Cliente 1 (operador) | `/cliente-1/login` | ursula.b@example.com | SinkaOperador!1 | +| Cliente 2 (admin) | `/cliente-2/login` | ivan.p@example.net | Cliente2Admin!1 | ## Testes diff --git a/api/prisma/seed.ts b/api/prisma/seed.ts index 410e648..98e4bd7 100644 --- a/api/prisma/seed.ts +++ b/api/prisma/seed.ts @@ -24,10 +24,70 @@ async function upsertUser( }); } +async function ensureTenant( + platform: PlatformPrisma, + name: string, + slug: string, + aliases: string[] = [], +) { + let tenant = await platform.tenant.findUnique({ where: { slug } }); + if (!tenant) { + for (const alias of aliases) { + const old = await platform.tenant.findUnique({ where: { slug: alias } }); + if (old) { + tenant = await platform.tenant.update({ + where: { id: old.id }, + data: { name, slug }, + }); + break; + } + } + } + if (!tenant) { + const database = databaseForSlug(slug); + await createMysqlDatabase(database); + await pushTenantSchema(database); + tenant = await platform.tenant.create({ data: { name, slug, database } }); + } else { + await pushTenantSchema(tenant.database); + if (tenant.name !== name) { + tenant = await platform.tenant.update({ where: { id: tenant.id }, data: { name } }); + } + } + return tenant; +} + +async function seedCompany( + tenant: { database: string }, + users: { email: string; name: string; password: string; role: 'ADMIN' | 'MANAGER' | 'OPERATOR' }[], + customers: { name: string; document: string; city: string; state: string }[], + carriers: { name: string; baseFee: number; pricePerKg: number; pricePerKm: number; adValoremRate: number }[], +) { + const db = new TenantPrisma({ + datasources: { db: { url: urlForDatabase(tenant.database) } }, + }); + for (const user of users) { + await upsertUser(db, user.email, user.name, user.password, user.role); + } + for (const customer of customers) { + const existing = await db.customer.findFirst({ where: { name: customer.name } }); + if (!existing) { + await db.customer.create({ data: customer }); + } + } + for (const carrier of carriers) { + const existing = await db.carrier.findFirst({ where: { name: carrier.name } }); + if (existing) { + await db.carrier.update({ where: { id: existing.id }, data: carrier }); + } else { + await db.carrier.create({ data: carrier }); + } + } + await db.$disconnect(); +} + async function main() { const platform = new PlatformPrisma(); - const slug = 'demo'; - const database = databaseForSlug(slug); const platformEmail = process.env.PLATFORM_ADMIN_EMAIL ?? 'tina.r@example.net'; const platformPassword = process.env.PLATFORM_ADMIN_PASSWORD ?? 'SinkaPlatform!1'; @@ -42,75 +102,68 @@ async function main() { }, }); - let tenant = await platform.tenant.findUnique({ where: { slug } }); - if (!tenant) { - await createMysqlDatabase(database); - await pushTenantSchema(database); - tenant = await platform.tenant.create({ - data: { name: 'Sinka Demo', slug, database }, - }); - } else { - await pushTenantSchema(tenant.database); - } + const cliente1 = await ensureTenant(platform, 'Cliente 1', 'cliente-1', ['demo']); + const cliente2 = await ensureTenant(platform, 'Cliente 2', 'cliente-2'); - const others = await platform.tenant.findMany({ where: { slug: { not: slug } } }); + const others = await platform.tenant.findMany({ + where: { slug: { notIn: ['cliente-1', 'cliente-2'] } }, + }); for (const item of others) { await pushTenantSchema(item.database); } - const db = new TenantPrisma({ - datasources: { db: { url: urlForDatabase(tenant.database) } }, - }); - - await upsertUser( - db, - process.env.DEMO_ADMIN_EMAIL ?? 'xena.w@example.org', - 'Ana Admin', - process.env.DEMO_ADMIN_PASSWORD ?? 'SinkaAdmin!1', - 'ADMIN', - ); - await upsertUser( - db, - process.env.DEMO_MANAGER_EMAIL ?? 'james.b@example.com', - 'Marcos Manager', - process.env.DEMO_MANAGER_PASSWORD ?? 'SinkaManager!1', - 'MANAGER', - ); - await upsertUser( - db, - process.env.DEMO_OPERATOR_EMAIL ?? 'ursula.b@example.com', - 'Olga Operador', - process.env.DEMO_OPERATOR_PASSWORD ?? 'SinkaOperador!1', - 'OPERATOR', + await seedCompany( + cliente1, + [ + { + email: process.env.DEMO_ADMIN_EMAIL ?? 'xena.w@example.org', + name: 'Ana Admin', + password: process.env.DEMO_ADMIN_PASSWORD ?? 'SinkaAdmin!1', + role: 'ADMIN', + }, + { + email: process.env.DEMO_MANAGER_EMAIL ?? 'james.b@example.com', + name: 'Marcos Manager', + password: process.env.DEMO_MANAGER_PASSWORD ?? 'SinkaManager!1', + role: 'MANAGER', + }, + { + email: process.env.DEMO_OPERATOR_EMAIL ?? 'ursula.b@example.com', + name: 'Olga Operador', + password: process.env.DEMO_OPERATOR_PASSWORD ?? 'SinkaOperador!1', + role: 'OPERATOR', + }, + ], + [ + { name: 'Mercado Aurora', document: '12.345.678/0001-90', city: 'São Paulo', state: 'SP' }, + { name: 'Café do Litoral', document: '98.765.432/0001-10', city: 'Santos', state: 'SP' }, + ], + [ + { name: 'Litoral Mix', baseFee: 45, pricePerKg: 1.8, pricePerKm: 0.85, adValoremRate: 0.003 }, + { name: 'Rota Sul', baseFee: 32, pricePerKg: 1.4, pricePerKm: 0.72, adValoremRate: 0.004 }, + { name: 'Express RJ', baseFee: 60, pricePerKg: 2.2, pricePerKm: 1.1, adValoremRate: 0.002 }, + ], ); - for (const customer of [ - { name: 'Mercado Aurora', document: '12.345.678/0001-90', city: 'São Paulo', state: 'SP' }, - { name: 'Café do Litoral', document: '98.765.432/0001-10', city: 'Santos', state: 'SP' }, - ]) { - const existing = await db.customer.findFirst({ where: { name: customer.name } }); - if (!existing) { - await db.customer.create({ data: customer }); - } - } + await seedCompany( + cliente2, + [ + { + email: 'ivan.p@example.net', + name: 'Bruno Admin', + password: 'Cliente2Admin!1', + role: 'ADMIN', + }, + ], + [ + { name: 'Padaria Centro', document: '11.222.333/0001-44', city: 'Curitiba', state: 'PR' }, + { name: 'Loja Norte', document: '55.666.777/0001-88', city: 'Joinville', state: 'SC' }, + ], + [{ name: 'Carga Paraná', baseFee: 38, pricePerKg: 1.5, pricePerKm: 0.7, adValoremRate: 0.0035 }], + ); - const carriers = [ - { name: 'Litoral Mix', baseFee: 45, pricePerKg: 1.8, pricePerKm: 0.85, adValoremRate: 0.003 }, - { name: 'Rota Sul', baseFee: 32, pricePerKg: 1.4, pricePerKm: 0.72, adValoremRate: 0.004 }, - { name: 'Express RJ', baseFee: 60, pricePerKg: 2.2, pricePerKm: 1.1, adValoremRate: 0.002 }, - ]; - for (const carrier of carriers) { - const existing = await db.carrier.findFirst({ where: { name: carrier.name } }); - if (existing) { - await db.carrier.update({ where: { id: existing.id }, data: carrier }); - } else { - await db.carrier.create({ data: carrier }); - } - } - - await db.$disconnect(); await platform.$disconnect(); - console.log('Seed ok: platform + tenant demo (admin, manager, operador).'); + console.log('Seed ok: platform + Cliente 1 + Cliente 2.'); } main().catch(async (error) => { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 720ea4e..96e4488 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -14,4 +14,4 @@ Document root de produto: o frontend Next.js. A API não serve HTML. -Login de empresa exige o slug **na URL** (`/demo`, `/filial-sul`). O JWT carrega `tenantId` + papel; as queries usam o PrismaClient daquele database. +Login de empresa exige o slug **na URL** (`/cliente-1`, `/cliente-2`). O JWT carrega `tenantId` + papel; as queries usam o PrismaClient daquele database. diff --git a/web/next.config.ts b/web/next.config.ts index 27ed0fe..09feabd 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -13,9 +13,9 @@ const nextConfig = { }, async redirects() { return [ - { source: "/login", destination: "/demo/login", permanent: false }, - { source: "/app", destination: "/demo", permanent: false }, - { source: "/app/:path*", destination: "/demo/:path*", permanent: false }, + { source: "/login", destination: "/cliente-1/login", permanent: false }, + { source: "/app", destination: "/cliente-1", permanent: false }, + { source: "/app/:path*", destination: "/cliente-1/:path*", permanent: false }, { source: "/platform", destination: "/admin", permanent: false }, { source: "/platform/:path*", destination: "/admin/:path*", permanent: false }, ]; diff --git a/web/src/app/admin/(panel)/page.tsx b/web/src/app/admin/(panel)/page.tsx index 05f1b19..85d2a3a 100644 --- a/web/src/app/admin/(panel)/page.tsx +++ b/web/src/app/admin/(panel)/page.tsx @@ -225,7 +225,7 @@ export default function PlatformPage() { /> setSlug(event.target.value.toLowerCase())} required diff --git a/web/src/components/landing/sinka-landing.tsx b/web/src/components/landing/sinka-landing.tsx index 8713755..5354ac1 100644 --- a/web/src/components/landing/sinka-landing.tsx +++ b/web/src/components/landing/sinka-landing.tsx @@ -49,7 +49,7 @@ export function SinkaLanding() { Conhecer a plataforma - + Começar agora @@ -175,7 +175,7 @@ export function SinkaLanding() { Conhecer a Sinka - + Começar agora diff --git a/web/src/components/landing/site-header.tsx b/web/src/components/landing/site-header.tsx index 9ad4c1c..94b1eeb 100644 --- a/web/src/components/landing/site-header.tsx +++ b/web/src/components/landing/site-header.tsx @@ -43,7 +43,7 @@ export function SiteHeader() { Conhecer a plataforma Entrar