Documento simples e direto sobre como o sistema cobra a unidade franqueada.
Base do código: app/Services/TbrCalculationService.php.
Ao criar uma unidade (UnitService::create), o sistema já cria:
| Registro | Tabela | Para quê |
|---|---|---|
| Unidade | units |
cadastro |
| Franqueado + vínculo | franchisees, franchisee_units |
dono da unidade |
| Financeiro da unidade | unit_financials |
nasce com charge_roi = true e charge_fnm = true |
| Perfis de acesso | user_types |
ADMIN_FRANCHISEE + templates |
| Pacotes de aula | class_package_units |
réplica dos pacotes base |
O contrato é um cadastro à parte (franchisee_contracts), criado depois pela franqueadora.
Uma unidade pode ter vários contratos; o sistema sempre usa o de start_date mais recente.
Atenção ao nome: a tabela
unit_contractsnão tem regra de cobrança — é só upload de PDF. Quem carrega as regras éfranchisee_contracts.
franchisee_contracts)Campos que importam para a cobrança:
| Campo | Papel |
|---|---|
unit_id |
unidade dona do contrato |
start_date |
marco zero: define o "mês de contrato" (mês 1, 2, 3...) |
end_date |
fim da vigência (nulo = sem fim) |
municipality_size_id |
porte do município (PP / MP / GP / MGP) → define o % de royalties |
tbr_fixed_value |
TBR de reserva: só é usada se o ano de referência não estiver cadastrado em tbrs |
invoice_due_date |
dia do vencimento da fatura (1 a 28; padrão 10) |
Regras de cadastro (FranchiseeContractService):
protocol = maior protocolo daquela unidade + 1 (sequência por unidade).signature_date = start_date.validity_months = meses entre start_date e end_date.municipality_size_id, tbr_fixed_value, tbr_fixed_value_percentage,
marketing_fund_percentage, maintance_tax_percentage) o sistema grava
franchisee_contract_tax_histories: na primeira alteração grava o "antes" e o "depois";
nas seguintes, só o "depois".Toda competência (mês/ano) a franqueadora cobra da unidade:
Total do título = soma dos três (final_value). Não há desconto/juros embutidos no cálculo.
TBR = tbrs.tbr_value do ano de referência (se > 0)
senão contrato.tbr_fixed_value
senão → erro "TBR não definida para o ano de referência nem para o contrato."
A tabela anual tbrs manda. É a configuração que o admin cadastra por ano e vale para
a rede inteira; se o ano de referência estiver cadastrado, o valor do contrato é ignorado.
O tbr_fixed_value do contrato só é usado quando aquele ano não tem configuração
(ou está com valor zerado).
Exemplo: tbrs 2026 = R$ 1.621,00 e contrato com tbr_fixed_value = R$ 1.400,00
→ competência de 2026 calcula sobre R$ 1.621,00.
mês_de_contrato = meses entre (início do mês de start_date) e (início do mês de referência) + 1
mínimo = 1
Exemplo: contrato começa em jan/2026.
Se o contrato não tiver start_date, o mês vira 1.
Meses 1, 2 e 3: Royalties = 0 e FNM = 0.
O critério registrado é tbr_fixo.
A Taxa de Manutenção continua sendo cobrada nos meses 1 a 3. A isenção é só de ROI e FNM.
Cada componente tem dois candidatos: um valor fixo sobre a TBR e um percentual do faturamento. Vence o maior. Em caso de empate, vence o fixo da TBR.
Royalties
fixo = % da faixa do porte × TBR
faturamento = 8% × faturamento do mês
Royalties = max(fixo, faturamento)
critério = 'tbr_fixo' se fixo >= faturamento, senão 'percentual_faturamento'
FNM
fixo = tbrs.fnm_percentage do ano × TBR (fallback do código: 20%)
faturamento = 2% × faturamento do mês
FNM = max(fixo, faturamento)
Taxa de Manutenção — não tem comparação e não tem isenção:
Manutenção = tbrs.maintenance_percentage do ano × TBR (fallback do código: 30%)
Vêm de inhabitant_classifications, filtrando is_renewal = false e
start <= mês_de_contrato <= end. Valores atuais do seeder:
| Porte | Habitantes | Meses 1–3 | Meses 4–12 | Meses 13–60 |
|---|---|---|---|---|
| PP | até 50 mil | 0% | 40% da TBR | 60% da TBR |
| MP | 50 mil–100 mil | 0% | 50% | 75% |
| GP | 100 mil–200 mil | 0% | 75% | 100% |
| MGP | acima de 200 mil | 0% | 100% | 150% |
Se não achar faixa para o mês → erro "Não foi encontrada faixa de royalties para o porte e mês de contrato informados."
unit_financials)| Flag | Efeito quando false |
|---|---|
charge_roi |
Royalties = 0, critério vira nao_cobrado e o sistema nem procura a faixa |
charge_fnm |
FNM = 0 |
Se a unidade não tiver registro em unit_financials, o sistema assume cobra os dois.
A Taxa de Manutenção não tem flag: é sempre cobrada.
Só entra o que vence dentro do mês de referência e não está cancelled (nem soft-deleted):
| Fonte | Tabela | Filtro |
|---|---|---|
| Parcelas de aluno | student_contract_installments |
unit_id, due_date no mês |
| Recebíveis da unidade | unit_account_receivables |
unit_id, due_date no mês |
| Contas que a matriz lançou na unidade | franchisee_account_receives |
origin = 'manual_unit' e unit_id preenchido |
Fora da base: recebíveis de TBR (origin = 'tbr') — senão a cobrança realimentaria a própria base.
Duas formas de informar o faturamento:
REVENUE_FROM_PREVIOUS_MONTH está false).revenue_value > 0 e esse valor substitui a soma automática.preview / calculate)preview só simula; calculate grava um registro em tbr_calculations.start_date preenchido.municipality_size_id → "O contrato da unidade não tem a faixa de habitantes definida."generateReceivable)Bloqueios:
receivable_generated = true) → erro;O que é criado:
| Registro | Conteúdo |
|---|---|
franchisee_account_receives |
valor total, status = 'pending', order = mês de contrato, histórico "Royalties / FNM / Manutenção — MM/AAAA" |
3× franchisee_account_receive_details |
Royalties, FNM e Taxa Manutenção separados |
unit_account_payables |
espelho da dívida no financeiro da unidade, origin = 'tbr' |
SyncFranchiseeChargeJob |
tenta emitir a cobrança no Asaas (sem chave configurada, fica para baixa manual) |
Vencimento:
vencimento = (mês de referência + 1 mês) no dia invoice_due_date
dia limitado entre 1 e 28; padrão 10 quando o contrato não informa
Ex.: competência 03/2026, invoice_due_date = 15 → vence 15/04/2026.
generateBatch)Pega os contratos ativos na competência: start_date <= último dia do mês,
end_date nulo ou >= primeiro dia do mês, municipality_size_id preenchido,
um contrato por unidade (o de start_date mais recente).
Retorna generated / skipped (já gerado) / errors.
Contrato: porte GP, start_date = 01/01/2026, TBR = R$ 1.621,00, invoice_due_date = 10.
Tabela tbrs 2026: FNM 20%, Manutenção 30%.
Competência 02/2026 (mês 2) — faturamento R$ 40.000
| Componente | Conta | Valor |
|---|---|---|
| Royalties | isento (mês ≤ 3) | R$ 0,00 |
| FNM | isento (mês ≤ 3) | R$ 0,00 |
| Manutenção | 30% × 1.621,00 | R$ 486,30 |
| Total | R$ 486,30 |
Competência 05/2026 (mês 5) — faturamento R$ 40.000
| Componente | Fixo TBR | % faturamento | Vence | Valor |
|---|---|---|---|---|
| Royalties | 75% × 1.621 = 1.215,75 | 8% × 40.000 = 3.200,00 | faturamento | R$ 3.200,00 |
| FNM | 20% × 1.621 = 324,20 | 2% × 40.000 = 800,00 | faturamento | R$ 800,00 |
| Manutenção | 30% × 1.621 = 486,30 | — | — | R$ 486,30 |
| Total | R$ 4.486,30 |
Vencimento: 10/06/2026.
Competência 05/2026 (mês 5) — faturamento R$ 5.000
| Componente | Fixo TBR | % faturamento | Vence | Valor |
|---|---|---|---|---|
| Royalties | 1.215,75 | 400,00 | fixo TBR | R$ 1.215,75 |
| FNM | 324,20 | 100,00 | fixo TBR | R$ 324,20 |
| Manutenção | — | — | — | R$ 486,30 |
| Total | R$ 2.026,25 |
Itens que divergem entre si no código atual — vale decidir qual é o certo:
Contratos com tbr_fixed_value diferente da tabela anual passam a ser ignorados.
Com a tabela do ano no comando, qualquer TBR negociada e gravada no contrato deixa de
valer enquanto aquele ano estiver cadastrado em tbrs. Se existirem contratos com TBR
negociada, eles precisam de outro campo/regra.
tbrs.fnm_percentage semeado como 0,02.
O TbrSeeder grava fnm_percentage = 0.0200, mas o cálculo usa esse campo como
percentual sobre a TBR (o fallback do código é 0.20). Com o dado semeado, o FNM fixo
de uma TBR de R$ 1.621 vira R$ 32,42 em vez de R$ 324,20. O 2% parece ter sido
pensado para o percentual sobre faturamento, que já é constante no código.
tbrs.royalties_percentage nunca é lido. Os 8% sobre faturamento são a constante
ROYALTIES_REVENUE_RATE no serviço.
TbrBillingPreviewService tem uma segunda versão das regras. Ele usa
contrato.tbr_fixed_value_percentage / marketing_fund_percentage, ignora o faturamento
e ignora charge_roi / charge_fnm. Resultado diferente do TbrCalculationService.
Duplicidade é checada só pelo mês de contrato, sem olhar ano/mês de competência
(existingReceivable(unit_id, contract_month)).
Faixas de renovação não são usadas. A busca fixa is_renewal = false, e as faixas
terminam no mês 60 — a partir do mês 61 o cálculo estoura com "faixa não encontrada".
Campos do contrato sem uso no cálculo: marketing_fund_fixed_value,
maintance_tax_fixed_value, maintance_tax_percentage, cancellation_fine,
discount_until_due_date.
tests/Unit/Financeiro/TbrCalculationRulesTest.php cobre as regras acima.
Rodar com:
php artisan test --filter=TbrCalculationRulesTest
Os testes usam o banco db_test_ginastica_cerebro, apontado em phpunit.xml.
Na primeira vez, crie o banco (o RefreshDatabase cuida das migrations):
createdb -h localhost -U postgres db_test_ginastica_cerebro