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>
This commit is contained in:
2026-10-09 16:35:51 +02:00
parent 9c37c1ad7b
commit d433c9392d
17 changed files with 1361 additions and 457 deletions

View File

@@ -0,0 +1,211 @@
<?php
namespace Tests\Feature;
use App\Livewire\Assegnazioni\Assegna;
use App\Livewire\Registro;
use App\Models\AnnoTeocratico;
use App\Models\Assegnazione;
use App\Models\Proclamatore;
use App\Models\Territorio;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;
/**
* Un territorio è fuori al massimo una volta: nessuna doppia assegnazione
* aperta, nessuna uscita retrodatata a prima dell'ultimo rientro, nessun
* periodo accavallato dalle correzioni nel Registro.
*/
class AssegnazioneSovrapposizioneTest extends TestCase
{
use RefreshDatabase;
private User $admin;
private Territorio $territorio;
private Proclamatore $proclamatore;
protected function setUp(): void
{
parent::setUp();
Carbon::setTestNow('2026-10-09 10:00:00');
foreach (['settings.manage', 'territori.assign', 'territori.return', 'registro.view'] as $permesso) {
Permission::create(['name' => $permesso, 'guard_name' => 'web']);
}
Role::create(['name' => 'amministratore', 'guard_name' => 'web'])->givePermissionTo(Permission::all());
$this->admin = User::create(['name' => 'Admin', 'email' => 'admin@example.test', 'password' => 'password']);
$this->admin->assignRole('amministratore');
$this->actingAs($this->admin);
$this->territorio = Territorio::create(['numero' => '5', 'attivo' => true]);
$this->proclamatore = Proclamatore::create(['nome' => 'Mario', 'cognome' => 'Rossi', 'attivo' => true]);
}
protected function tearDown(): void
{
Carbon::setTestNow();
parent::tearDown();
}
// ─── L'assegnazione in corso ────────────────────────────────
/**
* Il caso che rendeva il territorio "libero" mentre era fuori: una chiusa
* con data più recente dell'aperta.
*/
public function test_l_assegnazione_in_corso_si_trova_anche_con_una_chiusa_piu_recente(): void
{
$aperta = $this->assegna('2026-08-20');
$this->assegna('2026-09-01', '2026-09-15');
$this->assertTrue($this->territorio->fresh()->assegnazioneCorrente?->is($aperta));
}
// ─── Il controllo di sovrapposizione ────────────────────────
public function test_rientro_e_nuova_uscita_nello_stesso_giorno_non_si_sovrappongono(): void
{
$this->assegna('2026-09-01', '2026-09-20');
$this->assertNull(Assegnazione::sovrappostaA($this->territorio->id, Carbon::parse('2026-09-20')));
}
public function test_un_uscita_prima_dell_ultimo_rientro_si_sovrappone(): void
{
$chiusa = $this->assegna('2026-09-01', '2026-09-20');
$this->assertTrue(Assegnazione::sovrappostaA($this->territorio->id, Carbon::parse('2026-09-10'))?->is($chiusa));
}
public function test_con_un_assegnazione_aperta_ogni_nuova_uscita_si_sovrappone(): void
{
$aperta = $this->assegna('2026-09-01');
$this->assertTrue(Assegnazione::sovrappostaA($this->territorio->id, Carbon::parse('2026-10-01'))?->is($aperta));
}
public function test_un_periodo_chiuso_inserito_prima_di_tutti_non_si_sovrappone(): void
{
$this->assegna('2026-09-01');
$this->assertNull(Assegnazione::sovrappostaA($this->territorio->id, Carbon::parse('2026-06-01'), Carbon::parse('2026-07-01')));
}
// ─── Pagina Assegna ─────────────────────────────────────────
public function test_assegna_rifiuta_un_uscita_retrodatata_a_prima_dell_ultimo_rientro(): void
{
$this->assegna('2026-09-01', '2026-09-20');
Livewire::test(Assegna::class, ['territorioId' => $this->territorio->id])
->set('proclamatore_id', $this->proclamatore->id)
->set('assigned_at', '2026-09-10')
->call('save')
->assertHasErrors('assigned_at');
$this->assertSame(1, Assegnazione::count());
}
public function test_assegna_accetta_l_uscita_dal_giorno_del_rientro(): void
{
$this->assegna('2026-09-01', '2026-09-20');
Livewire::test(Assegna::class, ['territorioId' => $this->territorio->id])
->set('proclamatore_id', $this->proclamatore->id)
->set('assigned_at', '2026-09-20')
->call('save')
->assertHasNoErrors();
$this->assertSame(2, Assegnazione::count());
}
// ─── Registro (correzioni dell'amministratore) ──────────────
public function test_il_registro_ricava_l_anno_di_servizio_dalla_data(): void
{
Livewire::test(Registro::class)
->call('openCreate')
->set('form_territorio_id', (string) $this->territorio->id)
->set('form_proclamatore_id', (string) $this->proclamatore->id)
->set('form_assigned_at', '2025-08-10')
->set('form_returned_at', '2025-09-05')
->call('save')
->assertHasNoErrors();
$this->assertSame('2024-2025', Assegnazione::sole()->annoTeocratico->label);
}
public function test_il_registro_non_crea_una_seconda_assegnazione_aperta(): void
{
$this->assegna('2026-09-01');
Livewire::test(Registro::class)
->call('openCreate')
->set('form_territorio_id', (string) $this->territorio->id)
->set('form_proclamatore_id', (string) $this->proclamatore->id)
->set('form_assigned_at', '2026-10-01')
->call('save')
->assertHasErrors('form_assigned_at');
$this->assertSame(1, Assegnazione::count());
}
public function test_il_registro_non_riapre_un_assegnazione_chiusa_se_il_territorio_e_fuori(): void
{
$chiusa = $this->assegna('2026-08-01', '2026-08-20');
$this->assegna('2026-09-01');
Livewire::test(Registro::class)
->call('openEdit', $chiusa->id)
->set('form_returned_at', '')
->call('save')
->assertHasErrors('form_assigned_at');
$this->assertNotNull($chiusa->fresh()->returned_at);
}
public function test_il_registro_rifiuta_le_date_future(): void
{
Livewire::test(Registro::class)
->call('openCreate')
->set('form_territorio_id', (string) $this->territorio->id)
->set('form_proclamatore_id', (string) $this->proclamatore->id)
->set('form_assigned_at', '2026-11-01')
->call('save')
->assertHasErrors('form_assigned_at');
}
public function test_il_registro_permette_di_correggere_la_stessa_assegnazione(): void
{
$aperta = $this->assegna('2026-09-01');
Livewire::test(Registro::class)
->call('openEdit', $aperta->id)
->set('form_assigned_at', '2026-09-03')
->call('save')
->assertHasNoErrors();
$this->assertSame('2026-09-03', $aperta->fresh()->assigned_at->toDateString());
}
// ─── helper ─────────────────────────────────────────────────
private function assegna(string $uscita, ?string $rientro = null): Assegnazione
{
return Assegnazione::create([
'territorio_id' => $this->territorio->id,
'proclamatore_id' => $this->proclamatore->id,
'anno_teocratico_id' => AnnoTeocratico::perData(Carbon::parse($uscita))->id,
'assigned_at' => $uscita,
'returned_at' => $rientro,
'created_by' => $this->admin->id,
]);
}
}