// ============================================================
// Calc logic v2 — modelo simplificado SH Squads
// v1 preservada em logic.jsx
// ============================================================
// ---------- Range → average value mappers ----------
const CLIENTES_MAP = {
"<500": 300,
"500-2k": 1200,
"2k-10k": 6000,
"10k-50k": 30000,
"50k+": 100000,
};
const FATURAMENTO_MAP = {
"<100k": 75000,
"100k-500k": 300000,
"500k-2M": 1250000,
"2M-10M": 6000000,
"10M+": 20000000,
};
// Display labels
const CLIENTES_LABEL = {
"<500": "Menos de 500",
"500-2k": "500 a 2.000",
"2k-10k": "2.000 a 10.000",
"10k-50k": "10.000 a 50.000",
"50k+": "Mais de 50.000",
};
const FATURAMENTO_LABEL = {
"<100k": "Até R$ 100k",
"100k-500k": "R$ 100k a R$ 500k",
"500k-2M": "R$ 500k a R$ 2M",
"2M-10M": "R$ 2M a R$ 10M",
"10M+": "Mais de R$ 10M",
};
const SEGMENT_LABEL = {
marketplace: "Marketplace / Plataforma",
saas: "SaaS / Software",
varejo: "Varejo / E-commerce",
educacao: "Educação / Health / Serviços",
fintech: "Fintech / Banking",
outro: "Outro",
};
// ---------- Product meta ----------
const PRODUCTS = [
{ id: "maquininha", name: "Maquininha de Cartão / Gateway", desc: "Receita por % do volume processado · sub-adquirência parceira", model: "Sub-adquirência · spread MDR", icon: "credit-card" },
{ id: "conta", name: "Conta digital", desc: "Ganhe por tarifas e cesta de serviços ou reduza o custo que você já tem hoje", model: "Float + tarifa", icon: "wallet" },
{ id: "cartao", name: "Cartão com sua marca", desc: "Interchange + anuidade em cartão emitido pela sua fintech", model: "Interchange + anuidade", icon: "card-shield" },
{ id: "credito", name: "Crédito pessoal ou PJ", desc: "Spread sobre originações via securitizadora parceira", model: "Securitizadora · spread sobre originação", icon: "trending-up" },
{ id: "antecipacao", name: "Antecipação de recebíveis", desc: "Taxa sobre antecipação dos pagamentos", model: "Taxa de antecipação", icon: "fast-forward" },
{ id: "cambio", name: "Câmbio / Internacional", desc: "Spread em remessas e operações cambiais", model: "Spread cambial", icon: "globe" },
];
// ---------- v2 Model constants ----------
// Taxa de adoção baseada em experiência dos sócios
const ADOPTION_RATE = 0.70127;
// Base anual por cliente (independente de produto)
const BASE_PER_CLIENT = 700;
// Peso anual por produto — escolhidos para nunca gerar soma terminando em .x0
// Combinação válida: últimos dígitos {3,6,3,3,3,3}
const PRODUCT_WEIGHTS = {
maquininha: 162.23,
conta: 114.56,
antecipacao: 210.13,
cambio: 132.93,
cartao: 143.63,
credito: 189.03,
};
// ---------- Receita calc v2 ----------
function computeReceita(form) {
const clientes = CLIENTES_MAP[form.clientes] || 0;
const faturamentoMensal = FATURAMENTO_MAP[form.faturamento] || 0;
const faturamentoAnual = faturamentoMensal * 12;
const ticketMedio = clientes > 0 ? faturamentoMensal / clientes : 0;
// Clientes que adotam os serviços financeiros
const adoptingClients = clientes * ADOPTION_RATE;
// Soma dos pesos dos produtos selecionados
let weightSum = 0;
for (const pid of form.produtos) {
weightSum += PRODUCT_WEIGHTS[pid] || 0;
}
// Total = clientes adotantes × (base + soma dos pesos selecionados)
const basePerClient = BASE_PER_CLIENT + weightSum;
const total = adoptingClients * basePerClient;
// Receita por produto: peso + fração da base (para somar = total)
// Base R$700 dividida igualmente entre os produtos selecionados
const n = form.produtos.length || 1;
const baseShare = BASE_PER_CLIENT / n;
const byProduct = {};
for (const pid of form.produtos) {
const w = PRODUCT_WEIGHTS[pid] || 0;
byProduct[pid] = adoptingClients * (baseShare + w);
}
return {
total,
byProduct,
clientes,
faturamentoMensal,
faturamentoAnual,
ticketMedio,
adoptingClients,
basePerClient,
};
}
// ---------- Formatters (pt-BR) ----------
function fmtBRL(v, opts = {}) {
const { short = false, fractionDigits } = opts;
if (v == null || isNaN(v)) return "R$ 0";
if (short) {
if (Math.abs(v) >= 1e9) return "R$ " + (v / 1e9).toLocaleString("pt-BR", { maximumFractionDigits: 2 }) + " bi";
if (Math.abs(v) >= 1e6) return "R$ " + (v / 1e6).toLocaleString("pt-BR", { maximumFractionDigits: 2 }) + " M";
if (Math.abs(v) >= 1e3) return "R$ " + (v / 1e3).toLocaleString("pt-BR", { maximumFractionDigits: 0 }) + "k";
return "R$ " + Math.round(v).toLocaleString("pt-BR");
}
const fd = fractionDigits != null ? fractionDigits : 2;
return "R$ " + v.toLocaleString("pt-BR", { minimumFractionDigits: fd, maximumFractionDigits: fd });
}
function fmtNum(v) {
if (v == null || isNaN(v)) return "0";
return Math.round(v).toLocaleString("pt-BR");
}
// ---------- Email validation ----------
const PERSONAL_EMAIL_DOMAINS = new Set([
"gmail.com","hotmail.com","outlook.com","yahoo.com","yahoo.com.br",
"icloud.com","live.com","msn.com","bol.com.br","uol.com.br",
"terra.com.br","ig.com.br","globo.com","r7.com",
]);
function validateEmail(value) {
const v = (value || "").trim().toLowerCase();
if (!v) return { ok: false, kind: "empty" };
const m = /^[a-z0-9._%+\-]+@([a-z0-9.\-]+\.[a-z]{2,})$/i.exec(v);
if (!m) return { ok: false, kind: "format" };
const domain = m[1].toLowerCase();
if (PERSONAL_EMAIL_DOMAINS.has(domain)) return { ok: false, kind: "personal" };
return { ok: true };
}
// ---------- WhatsApp mask ----------
// Formato: (XX) XXXXX-XXXX — sem prefixo +55 no valor do input
function maskPhone(v) {
const digits = (v || "").replace(/\D/g, "").slice(0, 11);
if (digits.length === 0) return "";
if (digits.length <= 2) return `(${digits}`;
if (digits.length <= 7) return `(${digits.slice(0,2)}) ${digits.slice(2)}`;
return `(${digits.slice(0,2)}) ${digits.slice(2,7)}-${digits.slice(7,11)}`;
}
function isPhoneValid(v) {
const d = (v || "").replace(/\D/g, "");
return d.length >= 10 && d.length <= 11;
}
// ---------- Icons ----------
function Icon({ name, size = 20, stroke = 1.7, ...rest }) {
const props = {
width: size, height: size, viewBox: "0 0 24 24",
fill: "none", stroke: "currentColor", strokeWidth: stroke,
strokeLinecap: "round", strokeLinejoin: "round", ...rest,
};
switch (name) {
case "arrow-right": return ;
case "check": return ;
case "x": return ;
case "calculator": return ;
case "trending-up": return ;
case "credit-card": return ;
case "wallet": return ;
case "card-shield": return ;
case "fast-forward": return ;
case "globe": return ;
case "spark": return ;
case "linkedin": return ;
case "whatsapp": return ;
case "link": return ;
case "download": return ;
case "calendar": return ;
case "chevron-left": return ;
case "alert": return ;
default: return null;
}
}
// expose to globals (sobrescreve a v1 se ambas carregadas)
Object.assign(window, {
CLIENTES_MAP, FATURAMENTO_MAP, CLIENTES_LABEL, FATURAMENTO_LABEL, SEGMENT_LABEL,
PRODUCTS, PRODUCT_WEIGHTS, ADOPTION_RATE, BASE_PER_CLIENT,
computeReceita, fmtBRL, fmtNum,
validateEmail, maskPhone, isPhoneValid,
Icon,
});