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>
501 lines
18 KiB
PHP
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',
|
|
]);
|
|
}
|
|
}
|