|
|
@@ -1,305 +0,0 @@
|
|
|
-# 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
|
|
|
-```
|