{
  "id": "migration-11-internship-types",
  "title": "Migration 11 — internship_types",
  "description": "Contrato dos tipos de estágio e suas regras configuráveis.",
  "type": "migration-reference",
  "status": "implemented",
  "visibility": "public",
  "tags": [
    "sge/migrations",
    "sge/estagio",
    "sge/banco-de-dados"
  ],
  "related": [
    "migration-09-courses",
    "service-internshipenddatecalculator"
  ],
  "sourceRefs": [],
  "authors": [],
  "updated": null,
  "diagram": null,
  "body": "> [!success] Estado\n> Migration, Model, factory, regras Laravel em PHP e testes PostgreSQL implementados após [`courses`](doc:migration-09-courses). O snapshot do tipo pertence à futura Migration 15.\n\n## Contrato\n\n| Campo | Regra |\n| --- | --- |\n| `id` | `bigint` autoincremental, chave primária. |\n| `course_id` | `bigint` obrigatório, FK para `courses.id` com `ON DELETE RESTRICT`. |\n| `name` | Nome obrigatório, `varchar(255)`. |\n| `required_hours` | Carga horária obrigatória, inteiro positivo. |\n| `supervisor_evaluation_weight` | Peso da avaliação do supervisor, inteiro positivo. |\n| `report_weight` | Peso do relatório, inteiro positivo. |\n| `presentation_weight` | Peso da apresentação, inteiro positivo. |\n| `very_good_value`, `good_value`, `satisfactory_value`, `unsatisfactory_value` | Valores configuráveis dos conceitos, `numeric(5,1)`. |\n| `max_daily_hours` | Máximo de horas por dia, inteiro com padrão `6`; não pode ser menor que `6`. |\n| `max_weekly_hours` | Máximo de horas por semana, inteiro com padrão `30`; não pode ser menor que `30`. |\n| `safety_margin_days` | Margem de segurança para previsão do término, inteiro não negativo com padrão `7`. |\n| `deactivated_at` | Timestamp nullable; nulo significa tipo ativo. |\n| `created_at`, `updated_at` | Timestamps convencionais do Laravel. |\n\nOs campos escalares substituem a coluna JSONB `rules`. Eles têm casts declarados no Model, incluindo cast `integer` para os pesos e cast `decimal:1` para os valores configuráveis dos conceitos. `EvaluationConcept` define os nomes persistidos e seus rótulos em português: a chave histórica `excellent` aparece como “Ótimo” na interface. O valor de `excellent` é o próprio `supervisor_evaluation_weight` e não tem coluna. `unsatisfactory` tem valor inicial `0`, mas permanece configurável por tipo de estágio na coluna `unsatisfactory_value`. Os limites de jornada e a margem são inteiros.\n\n## Regras de negócio em PHP\n\nAs regras são aplicadas pela validação Laravel no Model ao salvar. A migration não adiciona constraints `CHECK` para validar regras de negócio; a validação acontece na aplicação.\n\n- `required_hours` deve ser inteiro positivo.\n- Os três pesos devem ser inteiros positivos e sua soma deve ser exatamente `10`. O peso da avaliação do supervisor também define o valor de `excellent` e o limite superior dos demais conceitos.\n- Os quatro conceitos configuráveis devem ser números não negativos com até uma casa decimal. A sequência completa — `excellent`, `very_good`, `good`, `satisfactory` e `unsatisfactory` — deve ser estritamente decrescente; valores iguais não são permitidos. Os conceitos configuráveis não podem ultrapassar `supervisor_evaluation_weight`. `unsatisfactory` costuma começar em zero, mas cada tipo pode definir outro valor, como `1.0`.\n- `excellent` usa diretamente `supervisor_evaluation_weight`, sem duplicar esse valor em uma coluna.\n- `max_daily_hours` e `max_weekly_hours` são inteiros. O formulário começa preenchido com `6` e `30`; esses valores são limites mínimos, não podem ser reduzidos e podem ser aumentados. A jornada informada deve respeitar os limites configurados para cada dia e para a soma semanal.\n- `safety_margin_days` é inteiro não negativo, inicialmente `7`, e será congelado no estágio. O cálculo automático usa essa margem e grava somente a previsão em `internships.projected_end_date`; não persiste fórmula, versão de algoritmo ou entradas. A regra completa está definida por este contrato e pelo [serviço de cálculo de término](doc:service-internshipenddatecalculator).\n\nA validação também exige curso ativo na criação, troca de curso ou reativação. Uma desativação posterior do curso não bloqueia a edição de um tipo já existente enquanto sua associação não mudar. `active()` filtra tipos não desativados; esta migration não cria índices secundários explícitos.\n\nOs pesos e os valores dos conceitos são configurados no tipo de estágio, nunca na resposta da avaliação nem livremente em cada estágio. Na criação do estágio, o snapshot pode agrupar esses valores no contrato histórico `internship_type_snapshot.rules`, montado a partir das colunas escalares, usando o peso do supervisor para `excellent`; alterar o tipo depois não pode recalcular históricos já formalizados.\n\n## Checklist\n\n- [x] Definir que pesos e valores dos conceitos pertencem ao tipo de estágio.\n- [x] Definir campos escalares, precisão dos pesos e conceitos, limites de jornada e margem do cálculo de término.\n- [x] Criar migration sem constraints `CHECK` de regras de negócio e sem índice secundário explícito.\n- [x] Criar Model com casts e validação Laravel em PHP.\n- [x] Criar factory com regras completas.\n- [x] Implementar validação da carga horária obrigatória, dos pesos, conceitos e limites diário e semanal da jornada.\n- [x] Testar consulta de tipos por curso/campus e ciclo de desativação.\n- [ ] Testar snapshot e alteração do tipo após uso no estágio, quando a Migration 15 existir.\n- [x] Testar conceitos fora do peso, `excellent` derivado do peso do supervisor, valores repetidos, ordem inválida, limites menores que 6/30 e margem negativa.\n- [x] Testar migrate/rollback das Migrations 15, 11, 10 e 09 na ordem das FKs.",
  "sections": [
    {
      "id": "contrato",
      "level": 2,
      "title": "Contrato",
      "text": "| Campo | Regra | | --- | --- | | `id` | `bigint` autoincremental, chave primária. | | `course_id` | `bigint` obrigatório, FK para `courses.id` com `ON DELETE RESTRICT`. | | `name` | Nome obrigatório, `varchar(255)`. | | `required_hours` | Carga horária obrigatória, inteiro positivo. | | `supervisor_evaluation_weight` | Peso da avaliação do supervisor, inteiro positivo. | | `report_weight` | Peso do relatório, inteiro positivo. | | `presentation_weight` | Peso da apresentação, inteiro positivo. | | `very_good_value`, `good_value`, `satisfactory_value`, `unsatisfactory_value` | Valores configuráveis dos conceitos, `numeric(5,1)`. | | `max_daily_hours` | Máximo de horas por dia, inteiro com padrão `6`; não pode ser menor que `6`. | | `max_weekly_hours` | Máximo de horas por semana, inteiro com padrão `30`; não pode ser menor que `30`. | | `safety_margin_days` | Margem de segurança para previsão do término, inteiro não negativo com padrão `7`. | | `deactivated_at` | Timestamp nullable; nulo significa tipo ativo. | | `created_at`, `updated_at` | Timestamps convencionais do Laravel. |  Os campos escalares substituem a coluna JSONB `rules`. Eles têm casts declarados no Model, incluindo cast `integer` para os pesos e cast `decimal:1` para os valores configuráveis dos conceitos. `EvaluationConcept` define os nomes persistidos e seus rótulos em português: a chave histórica `excellent` aparece como “Ótimo” na interface. O valor de `excellent` é o próprio `supervisor_evaluation_weight` e não tem coluna. `unsatisfactory` tem valor inicial `0`, mas permanece configurável por tipo de estágio na coluna `unsatisfactory_value`. Os limites de jornada e a margem são inteiros.",
      "line": 4
    },
    {
      "id": "regras-de-negocio-em-php",
      "level": 2,
      "title": "Regras de negócio em PHP",
      "text": "As regras são aplicadas pela validação Laravel no Model ao salvar. A migration não adiciona constraints `CHECK` para validar regras de negócio; a validação acontece na aplicação.  - `required_hours` deve ser inteiro positivo. - Os três pesos devem ser inteiros positivos e sua soma deve ser exatamente `10`. O peso da avaliação do supervisor também define o valor de `excellent` e o limite superior dos demais conceitos. - Os quatro conceitos configuráveis devem ser números não negativos com até uma casa decimal. A sequência completa — `excellent`, `very_good`, `good`, `satisfactory` e `unsatisfactory` — deve ser estritamente decrescente; valores iguais não são permitidos. Os conceitos configuráveis não podem ultrapassar `supervisor_evaluation_weight`. `unsatisfactory` costuma começar em zero, mas cada tipo pode definir outro valor, como `1.0`. - `excellent` usa diretamente `supervisor_evaluation_weight`, sem duplicar esse valor em uma coluna. - `max_daily_hours` e `max_weekly_hours` são inteiros. O formulário começa preenchido com `6` e `30`; esses valores são limites mínimos, não podem ser reduzidos e podem ser aumentados. A jornada informada deve respeitar os limites configurados para cada dia e para a soma semanal. - `safety_margin_days` é inteiro não negativo, inicialmente `7`, e será congelado no estágio. O cálculo automático usa essa margem e grava somente a previsão em `internships.projected_end_date`; não persiste fórmula, versão de algoritmo ou entradas. A regra completa está definida por este contrato e pelo [serviço de cálculo de término](doc:service-internshipenddatecalculator).  A validação também exige curso ativo na criação, troca de curso ou reativação. Uma desativação posterior do curso não bloqueia a edição de um tipo já existente enquanto sua associação não mudar. `active()` filtra tipos não desativados; esta migration não cria índices secundários explícitos.  Os pesos e os valores dos conceitos são configurados no tipo de estágio, nunca na resposta da avaliação nem livremente em cada estágio. Na criação do estágio, o snapshot pode agrupar esses valores no contrato histórico `internship_type_snapshot.rules`, montado a partir das colunas escalares, usando o peso do supervisor para `excellent`; alterar o tipo depois não pode recalcular históricos já formalizados.",
      "line": 24
    },
    {
      "id": "checklist",
      "level": 2,
      "title": "Checklist",
      "text": "- [x] Definir que pesos e valores dos conceitos pertencem ao tipo de estágio. - [x] Definir campos escalares, precisão dos pesos e conceitos, limites de jornada e margem do cálculo de término. - [x] Criar migration sem constraints `CHECK` de regras de negócio e sem índice secundário explícito. - [x] Criar Model com casts e validação Laravel em PHP. - [x] Criar factory com regras completas. - [x] Implementar validação da carga horária obrigatória, dos pesos, conceitos e limites diário e semanal da jornada. - [x] Testar consulta de tipos por curso/campus e ciclo de desativação. - [ ] Testar snapshot e alteração do tipo após uso no estágio, quando a Migration 15 existir. - [x] Testar conceitos fora do peso, `excellent` derivado do peso do supervisor, valores repetidos, ordem inválida, limites menores que 6/30 e margem negativa. - [x] Testar migrate/rollback das Migrations 15, 11, 10 e 09 na ordem das FKs.",
      "line": 39
    }
  ],
  "sourcePath": "content/migration-11-internship-types.md",
  "visuals": [],
  "apiVersion": 1
}
