Files
termanager2/tests/Feature/RegistroS13Test.php
Francesco Picone d433c9392d S-13 sul modulo ufficiale e correzioni all'integrità dei dati
Modulo S-13
- Il PDF scrive sopra il modulo ufficiale (FPDI, matrice convertita in PDF 1.4)
  invece di ricostruirlo in HTML: 20 righe per foglio come l'originale.
- Una riga per ogni territorio attivo; riporto dell'ultimo rientro prima
  dell'anno; assegnazioni a cavallo d'anno su entrambi i fogli; scelta per
  data e non per anno registrato; nomi come "Cognome I.".
- Logica spostata in App\Services\Registro\ModuloS13.

Integrità dei dati
- Export/import XML (e quindi backup/ripristino): le date non slittano più di
  un giorno; i file vecchi in UTC tornano al giorno giusto.
- Import XML: rifiuta file estranei o incompleti senza toccare i dati, errore
  con riga e motivo, territori senza zona importati con il loro storico,
  caratteri di controllo rimossi in export, chi importa conserva password e
  ruolo di amministratore.
- Assegnazioni: assegnazioneCorrente corretta (filtro dentro ofMany), periodi
  sovrapposti rifiutati in Assegna e Registro sotto lock, anno di servizio
  ricavato dalla data, niente date future nel Registro.
- Utenti: non si declassa sé stessi né l'ultimo amministratore.
- Entrypoint: non sovrascrive più la copia persistente di APP_KEY con un
  valore diverso; serve APP_KEY_ROTATE=1 per un cambio voluto.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:35:51 +02:00

501 lines
18 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\AnnoTeocratico;
use App\Models\Assegnazione;
use App\Models\Proclamatore;
use App\Models\Territorio;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Services\Registro\ModuloS13;
use Illuminate\Support\Collection;
use Tests\TestCase;
/**
* Modulo S-13-I: il ciclo da quattro gruppi "Assegnato a", la colonna
* "Restituito l'ultima volta il" e le assegnazioni a cavallo d'anno.
*
* E' la regola meno intuitiva del Registro — la quinta assegnazione non si
* aggiunge alle quattro precedenti, le sostituisce — e finisce su un modulo che
* viene firmato e archiviato: se sbaglia, sbaglia su carta.
*/
class RegistroS13Test extends TestCase
{
use RefreshDatabase;
private User $utente;
private Proclamatore $proclamatore;
private AnnoTeocratico $anno;
protected function setUp(): void
{
parent::setUp();
$this->utente = User::create([
'name' => 'Amministratore',
'email' => 'admin@example.test',
'password' => 'password',
]);
$this->proclamatore = Proclamatore::create([
'nome' => 'Mario',
'cognome' => 'Rossi',
'attivo' => true,
]);
$this->anno = AnnoTeocratico::create([
'label' => '2025-2026',
'start_date' => '2025-09-01',
'end_date' => '2026-08-31',
]);
}
// ─── Il primo ciclo ─────────────────────────────────────────
public function test_una_sola_assegnazione_riempie_la_prima_colonna_e_lascia_le_altre_in_bianco(): void
{
$territorio = $this->territorio('1');
$this->assegna($territorio, '2025-09-10', '2025-10-15');
$riga = $this->righe()->sole();
$this->assertSame('1', $riga['numero']);
$this->assertSame('', $riga['riporto']);
$this->assertCount(4, $riga['assegnazioni']);
$this->assertSame(
['nome' => 'Rossi M.', 'assegnato_il' => '10/09/2025', 'restituito_il' => '15/10/2025'],
$riga['assegnazioni'][0]
);
// Le colonne libere restano stampate: sul modulo cartaceo si compilano
// a mano.
foreach ([1, 2, 3] as $indice) {
$this->assertSame(
['nome' => '', 'assegnato_il' => '', 'restituito_il' => ''],
$riga['assegnazioni'][$indice]
);
}
}
public function test_il_nome_e_cognome_piu_iniziale_del_nome(): void
{
$proclamatore = Proclamatore::create([
'nome' => 'élena Maria',
'cognome' => "Dell'Acqua Bianchi",
'attivo' => true,
]);
$territorio = $this->territorio('1');
$this->assegna($territorio, '2025-09-10')->update(['proclamatore_id' => $proclamatore->id]);
$this->assertSame("Dell'Acqua Bianchi É.", $this->righe()->sole()['assegnazioni'][0]['nome']);
}
public function test_un_assegnazione_ancora_aperta_ha_la_data_di_rientro_vuota(): void
{
$territorio = $this->territorio('1');
$this->assegna($territorio, '2025-09-10');
$riga = $this->righe()->sole();
$this->assertSame('10/09/2025', $riga['assegnazioni'][0]['assegnato_il']);
$this->assertSame('', $riga['assegnazioni'][0]['restituito_il']);
}
public function test_tre_assegnazioni_occupano_le_prime_tre_colonne(): void
{
$territorio = $this->territorio('1');
$this->assegna($territorio, '2025-09-01', '2025-09-20');
$this->assegna($territorio, '2025-10-01', '2025-10-20');
$this->assegna($territorio, '2025-11-01', '2025-11-20');
$riga = $this->righe()->sole();
$this->assertSame(
['01/09/2025', '01/10/2025', '01/11/2025', ''],
array_column($riga['assegnazioni'], 'assegnato_il')
);
$this->assertSame('', $riga['riporto']);
}
public function test_quattro_assegnazioni_riempiono_la_riga_senza_riporto(): void
{
$territorio = $this->territorio('1');
$this->assegna($territorio, '2025-09-01', '2025-09-20');
$this->assegna($territorio, '2025-10-01', '2025-10-20');
$this->assegna($territorio, '2025-11-01', '2025-11-20');
$this->assegna($territorio, '2025-12-01', '2025-12-20');
$riga = $this->righe()->sole();
$this->assertSame(
['01/09/2025', '01/10/2025', '01/11/2025', '01/12/2025'],
array_column($riga['assegnazioni'], 'assegnato_il')
);
// Finche' il primo ciclo non e' esaurito non c'e' niente da riportare.
$this->assertSame('', $riga['riporto']);
}
// ─── Il ciclo che ricomincia ────────────────────────────────
/**
* Il caso che da' il nome alla regola: riempito il quarto gruppo si
* ricomincia da capo, come sul modulo cartaceo si aprirebbe una riga nuova.
* Le prime quattro assegnazioni escono dal modulo e restano solo nel
* riporto.
*/
public function test_la_quinta_assegnazione_apre_un_ciclo_nuovo_e_lascia_il_riporto(): void
{
$territorio = $this->territorio('1');
$this->assegna($territorio, '2025-09-01', '2025-09-20');
$this->assegna($territorio, '2025-10-01', '2025-10-20');
$this->assegna($territorio, '2025-11-01', '2025-11-20');
$this->assegna($territorio, '2025-12-01', '2025-12-24');
$this->assegna($territorio, '2026-01-05', '2026-01-25');
$riga = $this->righe()->sole();
$this->assertSame(
['05/01/2026', '', '', ''],
array_column($riga['assegnazioni'], 'assegnato_il')
);
// La colonna riporta la data di rientro della quarta, cioe' quella che
// chiudeva il giro precedente — senza il nome di chi lo aveva.
$this->assertSame('24/12/2025', $riga['riporto']);
}
public function test_otto_assegnazioni_mostrano_il_secondo_ciclo_per_intero(): void
{
$territorio = $this->territorio('1');
$date = [
['2025-09-01', '2025-09-20'],
['2025-10-01', '2025-10-20'],
['2025-11-01', '2025-11-20'],
['2025-12-01', '2025-12-24'],
['2026-01-05', '2026-01-25'],
['2026-02-05', '2026-02-25'],
['2026-03-05', '2026-03-25'],
['2026-04-05', '2026-04-25'],
];
foreach ($date as [$da, $a]) {
$this->assegna($territorio, $da, $a);
}
$riga = $this->righe()->sole();
$this->assertSame(
['05/01/2026', '05/02/2026', '05/03/2026', '05/04/2026'],
array_column($riga['assegnazioni'], 'assegnato_il')
);
$this->assertSame('24/12/2025', $riga['riporto']);
}
public function test_alla_nona_assegnazione_il_riporto_e_quello_dell_ottava(): void
{
$territorio = $this->territorio('1');
$date = [
['2025-09-01', '2025-09-20'],
['2025-10-01', '2025-10-20'],
['2025-11-01', '2025-11-20'],
['2025-12-01', '2025-12-24'],
['2026-01-05', '2026-01-25'],
['2026-02-05', '2026-02-25'],
['2026-03-05', '2026-03-25'],
['2026-04-05', '2026-04-28'],
['2026-05-05', null],
];
foreach ($date as [$da, $a]) {
$this->assegna($territorio, $da, $a);
}
$riga = $this->righe()->sole();
$this->assertSame(
['05/05/2026', '', '', ''],
array_column($riga['assegnazioni'], 'assegnato_il')
);
$this->assertSame('28/04/2026', $riga['riporto']);
}
/**
* Il riporto legge la data di rientro dell'ultima assegnazione del giro
* precedente: se quella e' ancora aperta la colonna resta vuota invece di
* inventarsi una data.
*/
public function test_il_riporto_resta_vuoto_se_l_ultima_del_giro_precedente_non_e_rientrata(): void
{
$territorio = $this->territorio('1');
$this->assegna($territorio, '2025-09-01', '2025-09-20');
$this->assegna($territorio, '2025-10-01', '2025-10-20');
$this->assegna($territorio, '2025-11-01', '2025-11-20');
$this->assegna($territorio, '2025-12-01', null);
$this->assegna($territorio, '2026-01-05', '2026-01-25');
$riga = $this->righe()->sole();
$this->assertSame('05/01/2026', $riga['assegnazioni'][0]['assegnato_il']);
$this->assertSame('', $riga['riporto']);
}
// ─── Cosa entra nel modulo ──────────────────────────────────
public function test_i_territori_disattivati_restano_fuori_dal_modulo(): void
{
$attivo = $this->territorio('1');
$disattivato = $this->territorio('2', attivo: false);
$this->assegna($attivo, '2025-09-01');
$this->assegna($disattivato, '2025-09-01');
$this->assertSame(['1'], $this->righe()->pluck('numero')->all());
}
public function test_i_territori_nel_cestino_restano_fuori_dal_modulo(): void
{
$vivo = $this->territorio('1');
$cestinato = $this->territorio('2');
$this->assegna($vivo, '2025-09-01');
$this->assegna($cestinato, '2025-09-01');
$cestinato->delete();
$this->assertSame(['1'], $this->righe()->pluck('numero')->all());
}
public function test_le_assegnazioni_chiuse_di_un_altro_anno_di_servizio_non_compaiono(): void
{
$territorio = $this->territorio('1');
$this->assegna($territorio, '2025-09-01', '2025-09-20');
$this->assegna($territorio, '2025-01-10', '2025-02-10', $this->annoPrecedente());
$riga = $this->righe()->sole();
$this->assertSame(
['01/09/2025', '', '', ''],
array_column($riga['assegnazioni'], 'assegnato_il')
);
}
/**
* Il registro serve a vedere anche i territori fermi: una riga vuota con
* la data dell'ultimo rientro dice da quanto non escono.
*/
public function test_un_territorio_senza_assegnazioni_nell_anno_ha_comunque_la_sua_riga(): void
{
$territorio = $this->territorio('1');
$this->assegna($territorio, '2025-03-01', '2025-04-12', $this->annoPrecedente());
$riga = $this->righe()->sole();
$this->assertSame('1', $riga['numero']);
$this->assertSame('12/04/2025', $riga['riporto']);
$this->assertSame(['', '', '', ''], array_column($riga['assegnazioni'], 'nome'));
}
public function test_un_territorio_mai_assegnato_ha_la_riga_vuota(): void
{
$this->territorio('1');
$riga = $this->righe()->sole();
$this->assertSame('', $riga['riporto']);
$this->assertSame(['', '', '', ''], array_column($riga['assegnazioni'], 'assegnato_il'));
}
// ─── Il riporto a inizio anno ───────────────────────────────
/**
* La nota del modulo: "quando si inizia un nuovo foglio, registrare in
* questa colonna l'ultima data in cui e' stato restituito ogni territorio".
*/
public function test_nel_primo_ciclo_il_riporto_e_l_ultimo_rientro_prima_dell_anno(): void
{
$precedente = $this->annoPrecedente();
$territorio = $this->territorio('1');
$this->assegna($territorio, '2025-01-10', '2025-02-10', $precedente);
$this->assegna($territorio, '2025-05-02', '2025-06-30', $precedente);
$this->assegna($territorio, '2025-10-01', '2025-10-20');
$riga = $this->righe()->sole();
$this->assertSame('30/06/2025', $riga['riporto']);
$this->assertSame('01/10/2025', $riga['assegnazioni'][0]['assegnato_il']);
}
// ─── A cavallo d'anno ───────────────────────────────────────
/**
* Uscito ad agosto, rientrato a ottobre: sul foglio nuovo occupa il primo
* gruppo con entrambe le date, e il riporto e' il rientro precedente.
*/
public function test_l_assegnazione_a_cavallo_apre_il_foglio_nuovo_con_entrambe_le_date(): void
{
$precedente = $this->annoPrecedente();
$territorio = $this->territorio('1');
$this->assegna($territorio, '2025-05-02', '2025-06-30', $precedente);
$this->assegna($territorio, '2025-08-10', '2025-10-05', $precedente);
$this->assegna($territorio, '2025-11-01', '2025-11-20');
$riga = $this->righe()->sole();
$this->assertSame('30/06/2025', $riga['riporto']);
$this->assertSame(
['nome' => 'Rossi M.', 'assegnato_il' => '10/08/2025', 'restituito_il' => '05/10/2025'],
$riga['assegnazioni'][0]
);
$this->assertSame('01/11/2025', $riga['assegnazioni'][1]['assegnato_il']);
}
public function test_sul_foglio_vecchio_l_assegnazione_a_cavallo_resta_senza_rientro(): void
{
$precedente = $this->annoPrecedente();
$territorio = $this->territorio('1');
$this->assegna($territorio, '2025-08-10', '2025-10-05', $precedente);
$riga = $this->righe($precedente)->sole();
$this->assertSame('10/08/2025', $riga['assegnazioni'][0]['assegnato_il']);
$this->assertSame('', $riga['assegnazioni'][0]['restituito_il']);
}
public function test_un_assegnazione_ancora_fuori_da_un_anno_precedente_compare_sul_foglio_nuovo(): void
{
$territorio = $this->territorio('1');
$this->assegna($territorio, '2025-07-01', null, $this->annoPrecedente());
$riga = $this->righe()->sole();
$this->assertSame('01/07/2025', $riga['assegnazioni'][0]['assegnato_il']);
$this->assertSame('', $riga['assegnazioni'][0]['restituito_il']);
}
/** L'assegnazione a cavallo conta nel ciclo: con altre quattro nell'anno, la riga ricomincia. */
public function test_l_assegnazione_a_cavallo_conta_nel_ciclo_da_quattro(): void
{
$territorio = $this->territorio('1');
$this->assegna($territorio, '2025-08-10', '2025-09-15', $this->annoPrecedente());
$this->assegna($territorio, '2025-10-01', '2025-10-20');
$this->assegna($territorio, '2025-11-01', '2025-11-20');
$this->assegna($territorio, '2025-12-01', '2025-12-24');
$this->assegna($territorio, '2026-01-05');
$riga = $this->righe()->sole();
$this->assertSame('24/12/2025', $riga['riporto']);
$this->assertSame(['05/01/2026', '', '', ''], array_column($riga['assegnazioni'], 'assegnato_il'));
}
/**
* Il foglio sceglie per data, non per anno_teocratico_id: una data
* corretta a mano nel Registro o un import legacy possono lasciare l'anno
* registrato incoerente con il giorno di uscita.
*/
public function test_conta_la_data_di_uscita_non_l_anno_registrato(): void
{
$precedente = $this->annoPrecedente();
$territorio = $this->territorio('1');
$this->assegna($territorio, '2025-10-01', '2025-10-20', $precedente);
$this->assertSame('01/10/2025', $this->righe()->sole()['assegnazioni'][0]['assegnato_il']);
$this->assertSame('', $this->righe($precedente)->sole()['assegnazioni'][0]['assegnato_il']);
}
public function test_l_ultimo_giorno_dell_anno_appartiene_al_foglio(): void
{
$territorio = $this->territorio('1');
$this->assegna($territorio, '2026-08-31', '2026-08-31');
$riga = $this->righe()->sole();
$this->assertSame('31/08/2026', $riga['assegnazioni'][0]['assegnato_il']);
$this->assertSame('31/08/2026', $riga['assegnazioni'][0]['restituito_il']);
}
// ─── Impaginazione e PDF ────────────────────────────────────
public function test_ogni_foglio_ha_venti_righe_come_il_modulo(): void
{
foreach (range(1, 21) as $numero) {
$this->territorio((string) $numero);
}
$pagine = (new ModuloS13())->pagine($this->righe());
$this->assertCount(2, $pagine);
$this->assertCount(20, $pagine[0]);
$this->assertCount(20, $pagine[1]);
$this->assertSame('21', $pagine[1][0]['numero']);
$this->assertSame('', $pagine[1][1]['numero']);
}
public function test_il_pdf_e_il_modulo_ufficiale_con_una_pagina_per_foglio(): void
{
foreach (range(1, 21) as $numero) {
$this->assegna($this->territorio((string) $numero), '2025-09-01');
}
$pdf = (new ModuloS13())->genera($this->anno);
$this->assertStringStartsWith('%PDF-', $pdf);
$this->assertSame(2, preg_match_all('#/Type /Page\b(?!s)#', $pdf));
}
/**
* I numeri di territorio sono stringhe ("1", "1 A", "10"): l'ordinamento e'
* naturale, non alfabetico, altrimenti il 10 finirebbe fra l'1 e il 2.
*/
public function test_le_righe_sono_in_ordine_naturale_di_numero(): void
{
foreach (['10', '2', '1 A', '1'] as $numero) {
$this->assegna($this->territorio($numero), '2025-09-01');
}
$this->assertSame(['1', '1 A', '2', '10'], $this->righe()->pluck('numero')->all());
}
// ─── helper ─────────────────────────────────────────────────
private function territorio(string $numero, bool $attivo = true): Territorio
{
return Territorio::create([
'numero' => $numero,
'attivo' => $attivo,
]);
}
private function assegna(
Territorio $territorio,
string $assegnatoIl,
?string $rientratoIl = null,
?AnnoTeocratico $anno = null
): Assegnazione {
return Assegnazione::create([
'territorio_id' => $territorio->id,
'proclamatore_id' => $this->proclamatore->id,
'anno_teocratico_id' => ($anno ?? $this->anno)->id,
'assigned_at' => $assegnatoIl,
'returned_at' => $rientratoIl,
'created_by' => $this->utente->id,
]);
}
/**
* @return Collection<int, array>
*/
private function righe(?AnnoTeocratico $anno = null): Collection
{
return (new ModuloS13())->righe($anno ?? $this->anno);
}
private function annoPrecedente(): AnnoTeocratico
{
return AnnoTeocratico::create([
'label' => '2024-2025',
'start_date' => '2024-09-01',
'end_date' => '2025-08-31',
]);
}
}