# 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" | | 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**. ### 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: 0. **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. 1. **`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. 2. **`tbrs.royalties_percentage` nunca é lido.** Os 8% sobre faturamento são a constante `ROYALTIES_REVENUE_RATE` no serviço. 3. **`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`. 4. **Duplicidade é checada só pelo mês de contrato**, sem olhar ano/mês de competência (`existingReceivable(unit_id, contract_month)`). 5. **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". 6. **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: ```bash 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): ```bash createdb -h localhost -U postgres db_test_ginastica_cerebro ```