regras-financeiras-contrato-unidade.md 11 KB

Regras financeiras do contrato da unidade (TBR / Royalties / FNM / Manutenção)

Documento simples e direto sobre como o sistema cobra a unidade franqueada. Base do código: app/Services/TbrCalculationService.php.


1. O que é criado junto com a unidade

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_contracts não tem regra de cobrança — é só upload de PDF. Quem carrega as regras é franchisee_contracts.


2. O contrato (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.
  • Ao alterar campos de imposto (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".

3. Os três valores cobrados todo mês

Toda competência (mês/ano) a franqueadora cobra da unidade:

  1. Royalties (ROI)
  2. FNM — Fundo Nacional de Marketing
  3. Taxa de Manutenção

Total do título = soma dos três (final_value). Não há desconto/juros embutidos no cálculo.


4. Passo a passo do cálculo

4.1. Achar o valor da TBR

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.

4.2. Achar o "mês de contrato"

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.

  • competência jan/2026 → mês 1
  • competência mar/2026 → mês 3
  • competência abr/2026 → mês 4

Se o contrato não tiver start_date, o mês vira 1.

4.3. Isenção dos meses 1 a 3

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.

4.4. Do mês 4 em diante — a regra do "maior valor"

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%)

4.5. Faixas de royalties por porte do município

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."

4.6. Chaves liga/desliga da unidade (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.


5. Como o faturamento do mês é apurado

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:

  • Automático: soma acima, do próprio mês de referência (a constante REVENUE_FROM_PREVIOUS_MONTH está false).
  • Manual: quem calcula manda revenue_value > 0 e esse valor substitui a soma automática.

6. Da conta ao título

6.1. Cálculo (preview / calculate)

  • preview só simula; calculate grava um registro em tbr_calculations.
  • O contrato usado é o mais recente com start_date preenchido.
  • Sem contrato → "Unidade não possui contrato cadastrado."
  • Contrato sem municipality_size_id → "O contrato da unidade não tem a faixa de habitantes definida."

6.2. Geração do título (generateReceivable)

Bloqueios:

  • cálculo já usado (receivable_generated = true) → erro;
  • já existe outro título gerado para a mesma unidade no mesmo mês de contrato → 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"
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.

6.3. Geração em lote (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.


7. Exemplo numérico

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

8. Pontos de atenção encontrados na varredura

Itens que divergem entre si no código atual — vale decidir qual é o certo:

  1. 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.

  2. 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.

  3. tbrs.royalties_percentage nunca é lido. Os 8% sobre faturamento são a constante ROYALTIES_REVENUE_RATE no serviço.

  4. 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.

  5. Duplicidade é checada só pelo mês de contrato, sem olhar ano/mês de competência (existingReceivable(unit_id, contract_month)).

  6. 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".

  7. 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.


9. Onde estão os testes

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