- Nova tabela sar.regras_desconto (grupo, subgrupo, representante, % e vigencia; campos nulos = todos) com migration idempotente, espelho no sar-erp-schema.sql e GRANT no provision-client - CRUD manager-only em /politicas/regras + aba "Regras de Desconto" na tela de Politicas Comerciais (selects com nomes de rep/grupo/subgrupo via /politicas/grupos-produto) - GET /politicas/regras/vigentes (rep ve so as que o alcancam) e GET /politicas/promocoes/vigentes para o catalogo - Regra vigente pre-aplica o desconto no carrinho do novo pedido - Selos "Promocao"/"Cond. especial" na busca de produto, catalogo do pedido e pagina de catalogo; secao "Condicoes Comerciais" no modal de detalhe do produto (descricao, % e validade) - Empty imageStyle deprecado -> styles.image no carrinho Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
191 lines
7.4 KiB
TypeScript
191 lines
7.4 KiB
TypeScript
#!/usr/bin/env tsx
|
||
/**
|
||
* SAR — Provisionamento de cliente
|
||
*
|
||
* Aplica o schema SAR (views + tabelas + triggers) no banco ERP do cliente.
|
||
* Não requer psql — usa a lib pg que já é dependência do projeto.
|
||
*
|
||
* Uso:
|
||
* pnpm client:provision --host <host> --db <banco> [opções]
|
||
*
|
||
* Opções:
|
||
* --host Host do PostgreSQL (obrigatório)
|
||
* --db Nome do banco ERP (obrigatório)
|
||
* --port Porta (default: 5432)
|
||
* --user Usuário (default: postgres)
|
||
* --password Senha (default: lê PG_PASSWORD do env)
|
||
* --role Role do app SAR — aplica GRANTs se informado (ex: sar_app)
|
||
*
|
||
* O que faz:
|
||
* 1. Executa scripts/sar-erp-schema.sql (schema sar + views + tabelas SAR)
|
||
* 2. Executa scripts/sar-triggers.sql (triggers de sync ERP + staging tables)
|
||
* 3. Aplica GRANTs se --role foi informado
|
||
* 4. Executa `prisma migrate deploy` (aplica migrations Prisma pendentes)
|
||
*/
|
||
|
||
import { readFileSync, readdirSync } from 'fs';
|
||
import { resolve, dirname } from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
import { execSync } from 'child_process';
|
||
import pg from 'pg';
|
||
|
||
// ─── CLI args ─────────────────────────────────────────────────────────────────
|
||
|
||
function arg(flag: string): string | undefined {
|
||
const idx = process.argv.indexOf(flag);
|
||
return idx !== -1 ? process.argv[idx + 1] : undefined;
|
||
}
|
||
|
||
const host = arg('--host');
|
||
const db = arg('--db');
|
||
const port = parseInt(arg('--port') ?? '5432', 10);
|
||
const user = arg('--user') ?? 'postgres';
|
||
const password = arg('--password') ?? process.env['PG_PASSWORD'] ?? '';
|
||
const role = arg('--role');
|
||
|
||
if (!host || !db) {
|
||
console.error(
|
||
'Uso: pnpm client:provision --host <host> --db <banco> [--port 5432] [--user postgres] [--password ...] [--role <pg-role>]',
|
||
);
|
||
console.error(
|
||
'Exemplo: pnpm client:provision --host 192.168.0.43 --db libreplast --role sar_app',
|
||
);
|
||
process.exit(1);
|
||
}
|
||
|
||
// ─── Paths ────────────────────────────────────────────────────────────────────
|
||
|
||
const SCRIPTS_DIR = dirname(fileURLToPath(import.meta.url));
|
||
const MONOREPO_ROOT = resolve(SCRIPTS_DIR, '..');
|
||
const API_DIR = resolve(MONOREPO_ROOT, 'apps/api');
|
||
|
||
// Banco ERP usa LATIN1 — strip chars fora do range 0x00-0xFF (decorações nos comentários SQL)
|
||
function toLatinSafe(sql: string): string {
|
||
// O range LATIN1 começa em \x00 por definição — control chars intencionais.
|
||
// eslint-disable-next-line no-control-regex
|
||
return sql.replace(/[^\x00-\xFF]/g, (c) => {
|
||
if (c === '—' || c === '–') return '--'; // em dash / en dash
|
||
return '-'; // box-drawing chars e outros
|
||
});
|
||
}
|
||
|
||
const SQL_SCHEMA = toLatinSafe(readFileSync(resolve(SCRIPTS_DIR, 'sar-erp-schema.sql'), 'utf8'));
|
||
const SQL_TRIGGERS = toLatinSafe(readFileSync(resolve(SCRIPTS_DIR, 'sar-triggers.sql'), 'utf8'));
|
||
|
||
const DATABASE_URL = `postgresql://${user}:${encodeURIComponent(password)}@${host}:${port}/${db}`;
|
||
|
||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||
|
||
async function runSql(client: pg.Client, label: string, sql: string): Promise<void> {
|
||
console.log(` Executando ${label}...`);
|
||
await client.query(sql);
|
||
console.log(` ✓ ${label} aplicado`);
|
||
}
|
||
|
||
async function applyGrants(client: pg.Client, appRole: string): Promise<void> {
|
||
console.log(` Aplicando GRANTs para role "${appRole}"...`);
|
||
await client.query(`
|
||
GRANT USAGE ON SCHEMA sar TO ${appRole};
|
||
GRANT SELECT ON ALL TABLES IN SCHEMA sar TO ${appRole};
|
||
GRANT INSERT, UPDATE, DELETE ON
|
||
sar.pedidos,
|
||
sar.pedido_itens,
|
||
sar.historico_pedido,
|
||
sar.alcada_desconto,
|
||
sar.meta_representante,
|
||
sar.push_subscription,
|
||
sar.promocoes,
|
||
sar.regras_desconto,
|
||
sar.clientes_novos,
|
||
sar.contatos_novos
|
||
TO ${appRole};
|
||
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA sar TO ${appRole};
|
||
`);
|
||
console.log(` ✓ GRANTs aplicados`);
|
||
}
|
||
|
||
// ─── Main ─────────────────────────────────────────────────────────────────────
|
||
|
||
async function main(): Promise<void> {
|
||
console.log('\nSAR — Provisionamento de Cliente');
|
||
console.log('──────────────────────────────────');
|
||
console.log(`Host: ${host}:${port}`);
|
||
console.log(`Banco: ${db}`);
|
||
console.log(`User: ${user}`);
|
||
if (role) console.log(`Role: ${role} (GRANTs serão aplicados)`);
|
||
console.log('');
|
||
|
||
const client = new pg.Client({ host, port, database: db, user, password });
|
||
await client.connect();
|
||
|
||
let needsBaseline = false;
|
||
|
||
try {
|
||
console.log('[1/3] Schema SAR...');
|
||
await runSql(client, 'sar-erp-schema.sql', SQL_SCHEMA);
|
||
|
||
console.log('[2/3] Triggers e tabelas staging...');
|
||
await runSql(client, 'sar-triggers.sql', SQL_TRIGGERS);
|
||
|
||
if (role) {
|
||
console.log('[+] GRANTs...');
|
||
await applyGrants(client, role);
|
||
}
|
||
|
||
// O schema SQL acima já criou as tabelas, então o schema `sar` nunca está
|
||
// vazio quando o Prisma roda. Sem histórico de migrations o `migrate deploy`
|
||
// aborta com P3005 — é preciso marcar a migration baseline como aplicada.
|
||
const { rows } = await client.query(
|
||
`SELECT to_regclass('sar._prisma_migrations') IS NOT NULL AS existe`,
|
||
);
|
||
needsBaseline = !rows[0]?.existe;
|
||
} finally {
|
||
await client.end();
|
||
}
|
||
|
||
// _prisma_migrations precisa morar no schema sar, não no public do ERP.
|
||
const MIGRATE_URL = `${DATABASE_URL}?schema=sar`;
|
||
const prismaEnv = { ...process.env, DATABASE_URL: MIGRATE_URL };
|
||
|
||
console.log('[3/3] Migrations Prisma...');
|
||
|
||
if (needsBaseline) {
|
||
const baseline = readdirSync(resolve(API_DIR, 'prisma/migrations'), { withFileTypes: true })
|
||
.filter((e) => e.isDirectory())
|
||
.map((e) => e.name)
|
||
.sort()[0];
|
||
if (!baseline) throw new Error('nenhuma migration encontrada para baseline');
|
||
console.log(` Banco pré-existente: marcando "${baseline}" como aplicada (baseline)...`);
|
||
execSync(`pnpm exec prisma migrate resolve --applied ${baseline}`, {
|
||
cwd: API_DIR,
|
||
env: prismaEnv,
|
||
stdio: 'inherit',
|
||
});
|
||
}
|
||
|
||
execSync('pnpm exec prisma migrate deploy', {
|
||
cwd: API_DIR,
|
||
env: prismaEnv,
|
||
stdio: 'inherit',
|
||
});
|
||
console.log(' ✓ Migrations aplicadas');
|
||
|
||
console.log('');
|
||
console.log('════════════════════════════════════');
|
||
console.log('✓ Cliente provisionado com sucesso');
|
||
console.log('════════════════════════════════════');
|
||
console.log('');
|
||
console.log('Próximos passos:');
|
||
console.log(` 1. Configure DATABASE_URL=${DATABASE_URL}`);
|
||
console.log(` no ambiente de produção (Vault ou .env do cliente)`);
|
||
if (!role) {
|
||
console.log(' 2. Execute os GRANTs manualmente ou rode novamente com --role <pg-role>');
|
||
}
|
||
console.log('');
|
||
}
|
||
|
||
main().catch((e: unknown) => {
|
||
console.error('\nErro durante provisionamento:', e);
|
||
process.exit(1);
|
||
});
|