feat: add previous configuration handling for user selections in quotes

- Implemented session storage management for the last configuration choices made by the user.
- Added functionality to prompt users when changing models to retain their previous selections.
- Introduced methods to apply previous choices to the newly loaded model, including finishes, handles, glass types, and options.
- Enhanced user experience by ensuring selections are automatically applied where applicable.
This commit is contained in:
2026-10-19 19:49:44 +02:00
parent 863e6ca920
commit 06617a985a
197 changed files with 4892 additions and 400 deletions
@@ -58,6 +58,38 @@ class CatalogoAdminTest extends TestCase
->assertCanNotSeeTableRecords([$modelloAltro]);
}
public function test_ordine_del_file_con_i_disattivi_in_fondo(): void
{
$codici = fn () => Modello::orderBy('ordine')->pluck('codice')->all();
// Ordine di OrdineSerramenti per gli attivi, poi i 4 disattivi nello stesso ordine.
$this->assertSame(['finestra_1_anta', 'finestra_2_ante', 'finestra_3_ante'], array_slice($codici(), 0, 3));
$this->assertSame(
['scorrevole_hst_4_ante', 'scorrevole_sf_3_ante', 'scorrevole_sf_4_ante', 'traslante_scorrevole_1_anta_f'],
array_slice($codici(), -4)
);
// Disattivando un modello passa nel gruppo in fondo (dopo i 25 attivi rimasti),
// mantenendo l'ordine relativo con gli altri disattivi; riattivandolo resta
// ultimo degli attivi, da riposizionare con "Riordina".
$bilico = Modello::where('codice', 'bilico')->firstOrFail();
Livewire::test(ListModelli::class)->call('updateTableColumnState', 'attivo', $bilico->getKey(), false);
$this->assertSame('celino', $codici()[24]);
$this->assertSame(['bilico', 'scorrevole_hst_4_ante'], array_slice($codici(), 25, 2));
$bilico->refresh()->update(['attivo' => true]);
$this->assertSame(['celino', 'bilico', 'scorrevole_hst_4_ante'], array_slice($codici(), 24, 3));
// In tabella i disattivi stanno in fondo anche se l'ordine salvato li metterebbe in mezzo.
Modello::where('codice', 'scorrevole_sf_3_ante')->update(['ordine' => 2]);
Livewire::test(ListModelli::class)
->set('tableRecordsPerPage', 50)
->assertCanSeeTableRecords(
Modello::orderByDesc('attivo')->orderBy('ordine')->get(),
inOrder: true
);
}
public function test_un_modello_attivo_richiede_la_configurazione_3d(): void
{
$sistema = Modello::firstOrFail()->sistema_id;
+13 -2
View File
@@ -2,6 +2,7 @@
namespace Tests\Feature;
use App\Models\ListinoPrezzo;
use App\Models\Modello;
use App\Models\Sistema;
use Database\Seeders\CatalogoSeeder;
@@ -46,9 +47,11 @@ class CatalogoApiTest extends TestCase
$this->assertSame(30, Modello::count());
}
public function test_api_sistemi_espone_solo_modelli_attivi_in_ordine(): void
public function test_api_sistemi_espone_solo_modelli_attivi_e_con_listino_in_ordine(): void
{
$this->seed(CatalogoSeeder::class);
// Senza listino il bilico non compare nel configuratore.
Modello::where('codice', '!=', 'bilico')->each(fn (Modello $m) => ListinoPrezzo::create(['modello_id' => $m->id, 'larghezza' => 1000, 'altezza' => 1000, 'prezzo' => 100]));
Sistema::create(['codice' => 'legno', 'nome' => 'Legno', 'attivo' => false]);
$response = $this->getJson('/api/sistemi')->assertOk();
@@ -57,13 +60,21 @@ class CatalogoApiTest extends TestCase
$response->assertJsonPath('0.codice', 'pvc');
$modelli = collect($response->json('0.modelli'));
$this->assertSame(26, $modelli->count());
$this->assertSame(25, $modelli->count());
$this->assertNotContains('bilico', $modelli->pluck('id'));
$this->assertSame('finestra_1_anta', $modelli->first()['id']);
$this->assertNotContains('scorrevole_sf_3_ante', $modelli->pluck('id'));
$this->assertStringEndsWith('/storage/modelli/icone/pvc/finestra_1_anta.jpg', $modelli->first()['icona']);
$this->assertSame('finestra_1_anta.glb', $modelli->first()['modello']);
}
public function test_configurazione_di_un_modello_senza_listino_restituisce_404(): void
{
$this->seed(CatalogoSeeder::class);
$this->getJson('/api/sistemi/pvc/modelli/bilico/configurazione')->assertNotFound();
}
public function test_api_sistema_inesistente_restituisce_404(): void
{
$this->getJson('/api/sistemi/inesistente')->assertNotFound();
@@ -0,0 +1,197 @@
<?php
namespace Tests\Feature;
use App\Filament\Resources\FinituraResource\Pages\ManageFiniture;
use App\Filament\Resources\GruppoOpzioniResource\Pages\ManageGruppiOpzioni;
use App\Filament\Resources\ModelloResource\Pages\EditModello;
use App\Filament\Resources\ModelloResource\RelationManagers\FinitureRelationManager;
use App\Filament\Resources\ModelloResource\RelationManagers\OpzioniRelationManager;
use App\Filament\Resources\OpzioneResource;
use App\Filament\Resources\OpzioneResource\Pages\ManageOpzioni;
use App\Filament\Resources\RegolaVetroResource\Pages\ManageRegoleVetro;
use App\Filament\Resources\VetroResource\Pages\ManageVetri;
use App\Models\Finitura;
use App\Models\Modello;
use App\Models\Opzione;
use App\Models\RegolaVetro;
use App\Models\User;
use App\Models\Vetro;
use Database\Seeders\CatalogoSeeder;
use Database\Seeders\ConfiguratoreSeeder;
use Database\Seeders\NuoveOpzioniSeeder;
use Database\Seeders\ProfiliExtraSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Livewire\Livewire;
use Tests\TestCase;
class ConfiguratoreAdminTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Storage::fake('public');
$this->seed([CatalogoSeeder::class, ConfiguratoreSeeder::class, ProfiliExtraSeeder::class, NuoveOpzioniSeeder::class]);
$this->actingAs(User::factory()->create());
}
public function test_le_pagine_del_configuratore_si_aprono(): void
{
Livewire::test(ManageFiniture::class)->assertOk()->assertSee('Noce Rivestito');
Livewire::test(ManageVetri::class)->assertOk()->assertSee('Acustico base');
Livewire::test(ManageRegoleVetro::class)->assertOk()->assertSee('ALZ_AF')->assertSee('(L − 312) ÷ 2');
// Solo le opzioni del sistema: le personalizzazioni stanno nel modello.
Livewire::test(ManageOpzioni::class)
->assertOk()
->set('tableRecordsPerPage', 50)
->assertCanSeeTableRecords(Opzione::whereNull('modello_id')->where('gruppo', 'telaio')->get())
->assertCanNotSeeTableRecords(Opzione::whereNotNull('modello_id')->get());
$modello = Modello::where('codice', 'finestra_2_ante')->firstOrFail();
Livewire::test(EditModello::class, ['record' => $modello->getRouteKey()])
->assertOk()
->assertFormSet(['regola_vetro_id' => $modello->regola_vetro_id]);
}
public function test_modifica_maggiorazione_di_un_vetro(): void
{
$vetro = Vetro::where('codice', 'V2')->firstOrFail();
Livewire::test(ManageVetri::class)
->callTableAction('edit', $vetro, ['maggiorazione_mq' => 55, 'minimo_mq' => 0.6])
->assertHasNoTableActionErrors();
$this->assertEquals(55, $vetro->fresh()->maggiorazione_mq);
$this->assertEquals(0.6, $vetro->fresh()->minimo_mq);
}
public function test_personalizza_gruppo_copia_le_opzioni_del_sistema_nel_modello(): void
{
$modello = Modello::where('codice', 'bilico')->firstOrFail();
Livewire::test(OpzioniRelationManager::class, ['ownerRecord' => $modello, 'pageClass' => EditModello::class])
->assertOk()
->callTableAction('personalizza', data: ['gruppo' => 'tipo_finitura'])
->assertHasNoTableActionErrors();
$copiate = $modello->opzioni()->where('gruppo', 'tipo_finitura')->orderBy('ordine')->get();
$this->assertSame(['bianco', 'uniforme', 'bicolore'], $copiate->pluck('valore')->all());
$this->assertEquals(27, $copiate[1]->supplemento);
}
public function test_opzione_personalizzata_con_valore_del_sistema_ne_copia_i_dati(): void
{
$modello = Modello::where('codice', 'bilico')->firstOrFail();
Livewire::test(OpzioniRelationManager::class, ['ownerRecord' => $modello, 'pageClass' => EditModello::class])
->mountTableAction('create')
->setTableActionData(['gruppo' => 'profili_extra'])
->set('mountedTableActionsData.0.valore', '50811')
->assertTableActionDataSet(['etichetta' => '50811_GUIDA 34', 'supplemento_tipo' => 'metro_lineare', 'alternativa' => 'guida'])
->setTableActionData(['supplemento' => 18])
->callMountedTableAction()
->assertHasNoTableActionErrors();
$opzione = $modello->opzioni()->where('gruppo', 'profili_extra')->sole();
$this->assertSame('50811', $opzione->valore);
$this->assertEquals(18, $opzione->supplemento);
$this->assertEquals(2, $opzione->ml_per_altezza);
$this->assertNotNull($opzione->icona);
}
public function test_opzione_personalizzata_con_valore_altro(): void
{
$modello = Modello::where('codice', 'bilico')->firstOrFail();
$manager = fn () => Livewire::test(OpzioniRelationManager::class, ['ownerRecord' => $modello, 'pageClass' => EditModello::class]);
$manager()
->callTableAction('create', data: ['gruppo' => 'profili_extra', 'valore' => OpzioneResource::ALTRO, 'valore_altro' => 'non valido!', 'etichetta' => 'X'])
->assertHasTableActionErrors(['valore_altro']);
$manager()
->callTableAction('create', data: ['gruppo' => 'profili_extra', 'valore' => OpzioneResource::ALTRO, 'valore_altro' => 'nuovo_profilo', 'etichetta' => 'Nuovo profilo'])
->assertHasNoTableActionErrors();
$opzione = $modello->opzioni()->where('valore', 'nuovo_profilo')->sole();
// In modifica un valore che il sistema non ha compare come "Altro".
$manager()
->mountTableAction('edit', $opzione)
->assertTableActionDataSet(['valore' => OpzioneResource::ALTRO, 'valore_altro' => 'nuovo_profilo']);
}
public function test_la_prova_della_regola_vetro_mostra_le_lastre(): void
{
$regola = RegolaVetro::where('codice', 'F2')->firstOrFail();
Livewire::test(ManageRegoleVetro::class)
->mountTableAction('edit', $regola)
->setTableActionData(['prova_larghezza' => 1200, 'prova_altezza' => 1400])
->assertSee('2 × 444,0 × 1.211,0 mm');
}
public function test_scelta_delle_finiture_nel_modello(): void
{
$modello = Modello::where('codice', 'finestra_2_ante')->firstOrFail();
$noce = Finitura::where('tipo', 'profilo')->where('codice', '2')->firstOrFail();
$altre = Finitura::where('tipo', 'profilo')->whereIn('codice', ['3', '4'])->get();
$tabella = fn () => Livewire::test(FinitureRelationManager::class, ['ownerRecord' => $modello, 'pageClass' => EditModello::class]);
$tabella()->assertOk()
->assertCanSeeTableRecords([$noce])
->call('updateTableColumnState', 'scelta_modello', $noce->getKey(), 'interno');
$this->assertSame('interno', DB::table('finiture_modelli')->where('finitura_id', $noce->id)->value('lato'));
$tabella()->callTableBulkAction('imposta', $altre, ['lato' => 'nessuno']);
$this->assertSame(3, DB::table('finiture_modelli')->where('modello_id', $modello->id)->count());
// "Come il sistema" rimuove la scelta del modello.
$tabella()->call('updateTableColumnState', 'scelta_modello', $noce->getKey(), '');
$this->assertFalse(DB::table('finiture_modelli')->where('finitura_id', $noce->id)->exists());
$tabella()->callTableAction('ripristina');
$this->assertFalse(DB::table('finiture_modelli')->where('modello_id', $modello->id)->exists());
}
public function test_imposta_lato_sulle_finiture_del_sistema(): void
{
$finiture = Finitura::where('tipo', 'profilo')->whereIn('codice', ['2', '3'])->get();
Livewire::test(ManageFiniture::class)->callTableBulkAction('lato', $finiture, ['lato' => 'esterno']);
$this->assertSame(['esterno'], Finitura::whereIn('id', $finiture->modelKeys())->distinct()->pluck('lato')->all());
}
public function test_profilo_extra_al_metro_lineare_dal_pannello(): void
{
$guida = Opzione::whereNull('modello_id')->where('valore', '50811')->firstOrFail();
Livewire::test(ManageOpzioni::class)
->callTableAction('edit', $guida, ['supplemento' => 18, 'ml_per_altezza' => 2, 'ml_aggiunta_mm' => 300, 'alternativa' => 'guida'])
->assertHasNoTableActionErrors();
$guida->refresh();
$this->assertEquals(18, $guida->supplemento);
$this->assertEquals(3.1, $guida->metriLineari(1200, 1400));
$this->assertSame('€ 18,00/ml × (H × 2 + 300 mm)', $guida->descriviSupplemento());
}
public function test_gruppi_di_opzioni_dal_pannello(): void
{
Livewire::test(ManageGruppiOpzioni::class)
->assertOk()
->assertSee('Soglia ribassata')
->callAction('create', ['codice' => 'zanzariera', 'etichetta' => 'Zanzariera', 'ui' => 'select', 'sezione' => 'accessori', 'modulo' => 'zanzariera'])
->assertHasNoActionErrors()
// I moduli base non si possono usare per un gruppo.
->callAction('create', ['codice' => 'altro', 'etichetta' => 'Altro', 'ui' => 'select', 'sezione' => 'accessori', 'modulo' => 'vetro'])
->assertHasActionErrors(['modulo' => 'not_in']);
$this->assertArrayHasKey('zanzariera', Modello::moduliDisponibili());
}
}
@@ -0,0 +1,426 @@
<?php
namespace Tests\Feature;
use App\Models\Finitura;
use App\Models\ListinoPrezzo;
use App\Models\Modello;
use App\Models\Opzione;
use App\Models\PreventivoItem;
use App\Models\RegolaVetro;
use App\Models\Vetro;
use Database\Seeders\CatalogoSeeder;
use Database\Seeders\ConfiguratoreSeeder;
use Database\Seeders\NuoveOpzioniSeeder;
use Database\Seeders\ProfiliExtraSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class ConfiguratorePrezzoTest extends TestCase
{
use RefreshDatabase;
private const BASE = 500;
protected function setUp(): void
{
parent::setUp();
Storage::fake('public');
$this->seed([CatalogoSeeder::class, ConfiguratoreSeeder::class, ProfiliExtraSeeder::class, NuoveOpzioniSeeder::class]);
}
/** Listino 1000-1400 x 1200-1400 mm: la cella 1200 x 1400 vale BASE. */
private function conListino(string $codice): Modello
{
$modello = Modello::where('codice', $codice)->firstOrFail();
foreach ([1000, 1200, 1400] as $l) {
foreach ([1200, 1400] as $h) {
ListinoPrezzo::create(['modello_id' => $modello->id, 'larghezza' => $l, 'altezza' => $h, 'prezzo' => $l === 1200 && $h === 1400 ? self::BASE : 100]);
}
}
return $modello;
}
private function prezzo(string $codice, array $scelte)
{
return $this->postJson("/api/sistemi/pvc/modelli/{$codice}/prezzo", $scelte);
}
public function test_il_seeder_crea_il_catalogo_pvc_con_le_immagini(): void
{
$this->assertSame(48, Finitura::where('tipo', 'profilo')->count());
$this->assertSame(4, Finitura::where('tipo', 'maniglia')->count());
$this->assertSame(14, Vetro::count());
$this->assertSame(19, RegolaVetro::count());
$this->assertSame('V3', Vetro::where('predefinito', true)->value('codice'));
$finestra = Modello::where('codice', 'finestra_2_ante')->firstOrFail();
$this->assertSame('F2', $finestra->regolaVetro->codice);
$this->assertSame('FINESTRA 2 ANTE PVC - SALAMANDER BE 82', $finestra->titolo);
$this->assertTrue($finestra->haModulo('finitura_esterna') && $finestra->haModulo('apertura'));
$this->assertNull(Modello::where('codice', 'cassonetto_80')->value('regola_vetro_id'));
// Icone di apertura proprie del modello; un file per record.
$apertura = $finestra->opzioni()->where('gruppo', 'apertura')->pluck('icona');
$this->assertCount(2, $apertura);
$apertura->each(fn ($p) => Storage::disk('public')->assertExists($p));
Storage::disk('public')->assertExists(Finitura::where('codice', '2')->value('texture'));
$this->assertCount(4, Vetro::whereNotNull('texture_bump')->pluck('texture_bump')->unique());
}
public function test_il_seeder_non_sovrascrive_il_catalogo_esistente(): void
{
Vetro::where('codice', 'V1')->update(['maggiorazione_mq' => 99]);
$this->seed(ConfiguratoreSeeder::class);
$this->assertSame(14, Vetro::count());
$this->assertEquals(99, Vetro::where('codice', 'V1')->value('maggiorazione_mq'));
}
public function test_prezzo_base_con_scelte_predefinite(): void
{
$this->conListino('finestra_2_ante');
// 1150 x 1300 -> prima misura a listino >= : 1200 x 1400. Bianco, vetro standard (0 €/m²), cerniere no.
$this->prezzo('finestra_2_ante', ['larghezza' => 1150, 'altezza' => 1300])
->assertOk()
->assertJsonPath('prezzo', self::BASE)
->assertJsonPath('dettaglio.misura_listino', ['larghezza' => 1200, 'altezza' => 1400])
->assertJsonPath('configurazione.tipo_finitura', 'Bianco liscio')
->assertJsonPath('configurazione.vetro', 'Standard trasparente')
->assertJsonPath('scelte.opzioni.apertura', 'dx');
}
public function test_supplementi_tipo_finitura_e_opzioni(): void
{
$this->conListino('finestra_2_ante');
$misura = ['larghezza' => 1200, 'altezza' => 1400];
// Stessa finitura non predefinita dentro e fuori: uniforme 27%.
$this->prezzo('finestra_2_ante', $misura + ['finitura_interna' => '2', 'finitura_esterna' => '2'])
->assertJsonPath('prezzo', 635)
->assertJsonPath('configurazione.tipo_finitura', 'Rivestimento uniforme');
// Finiture diverse: bicolore 30%, più cerniere a scomparsa (+100 € sulla finestra 2 ante).
$this->prezzo('finestra_2_ante', $misura + ['finitura_interna' => '1', 'finitura_esterna' => '2', 'opzioni' => ['cerniere_a_scomparsa' => 'si']])
->assertJsonPath('prezzo', 750)
->assertJsonPath('configurazione.cerniere_a_scomparsa', 'SI');
}
public function test_variazione_vetro_con_regola_standard(): void
{
$this->conListino('finestra_2_ante');
// F2, 1200 x 1400: 2 lastre (1200 - 312) / 2 = 444 x (1400 - 189) = 1211 -> 0,537684 m² ciascuna.
// Acustico base 80 €/m²: 2 x 0,537684 x 80 = 86,02944.
$this->prezzo('finestra_2_ante', ['larghezza' => 1200, 'altezza' => 1400, 'vetro' => 'V1'])
->assertOk()
->assertJsonPath('prezzo', 586.03)
->assertJsonPath('dettaglio.vetro.regola', 'F2')
->assertJsonPath('dettaglio.vetro.lastre.0.larghezza', 444)
->assertJsonPath('dettaglio.vetro.lastre.0.mq', 0.538)
->assertJsonPath('dettaglio.vetro.mq_fatturabili', 1.075);
// Senza vetro: -90 €/m², minimo 0 -> 500 - 96,78312.
$this->prezzo('finestra_2_ante', ['larghezza' => 1200, 'altezza' => 1400, 'vetro' => 'V7'])
->assertJsonPath('prezzo', 403.22);
}
public function test_il_minimo_fatturabile_vale_per_singola_lastra(): void
{
$modello = Modello::where('codice', 'finestra_1_anta')->firstOrFail();
ListinoPrezzo::create(['modello_id' => $modello->id, 'larghezza' => 600, 'altezza' => 600, 'prezzo' => 200]);
// F1, 600 x 600: 411 x 411 = 0,168921 m² < 0,5 -> si fattura 0,5 x 40 €/m² (satinato).
$this->prezzo('finestra_1_anta', ['larghezza' => 600, 'altezza' => 600, 'vetro' => 'V2'])
->assertJsonPath('prezzo', 220)
->assertJsonPath('dettaglio.vetro.lastre.0.mq_fatturabili', 0.5);
}
public function test_traslante_anta_piu_fisso_calcola_le_due_lastre_separatamente(): void
{
$modello = Modello::where('codice', 'traslante_scorrevole_1_anta_f')->firstOrFail();
$modello->update(['attivo' => true, 'cartella' => 'x', 'file_modello' => 'x.glb', 'moduli' => [...$modello->moduli, 'listino', 'vetro']]);
ListinoPrezzo::create(['modello_id' => $modello->id, 'larghezza' => 2000, 'altezza' => 2200, 'prezzo' => 1000]);
// Fisso (2000/2 - 81) x (2200 - 105) = 919 x 2095; anta (2000/2 - 181) x (2200 - 205) = 819 x 1995.
// (1,925305 + 1,633905) x 80 €/m² = 284,7368.
$this->prezzo('traslante_scorrevole_1_anta_f', ['larghezza' => 2000, 'altezza' => 2200, 'vetro' => 'V1'])
->assertOk()
->assertJsonPath('prezzo', 1284.74)
->assertJsonPath('dettaglio.vetro.lastre.0.descrizione', 'Vetro fisso')
->assertJsonPath('dettaglio.vetro.lastre.1.larghezza', 819);
}
public function test_misura_fuori_listino_o_scelta_inesistente_restituiscono_422(): void
{
$this->conListino('finestra_2_ante');
$this->prezzo('finestra_2_ante', ['larghezza' => 900, 'altezza' => 1400])->assertUnprocessable()->assertJsonValidationErrors('larghezza');
$this->prezzo('finestra_2_ante', ['larghezza' => 1500, 'altezza' => 1400])->assertUnprocessable();
$this->prezzo('finestra_2_ante', ['larghezza' => 1200, 'altezza' => 1400, 'vetro' => 'V99'])->assertJsonValidationErrors('vetro');
$this->prezzo('finestra_2_ante', ['larghezza' => 1200, 'altezza' => 1400, 'opzioni' => ['apertura' => 'su']])->assertJsonValidationErrors('opzioni.apertura');
}
public function test_le_opzioni_del_modello_sostituiscono_il_gruppo_del_sistema(): void
{
$modello = $this->conListino('finestra_2_ante');
Opzione::create([
'sistema_id' => $modello->sistema_id, 'modello_id' => $modello->id, 'gruppo' => 'tipo_finitura',
'valore' => 'uniforme', 'etichetta' => 'Uniforme speciale', 'supplemento_tipo' => 'percentuale', 'supplemento' => 40,
]);
$misura = ['larghezza' => 1200, 'altezza' => 1400];
$this->prezzo('finestra_2_ante', $misura + ['finitura_interna' => '2', 'finitura_esterna' => '2'])->assertJsonPath('prezzo', 700);
// Il gruppo personalizzato non ha "bicolore": nessun supplemento.
$this->prezzo('finestra_2_ante', $misura + ['finitura_interna' => '1', 'finitura_esterna' => '2'])->assertJsonPath('prezzo', self::BASE);
}
public function test_configurazione_espone_menu_e_immagini_senza_prezzi(): void
{
$this->conListino('finestra_2_ante');
$json = $this->getJson('/api/sistemi/pvc/modelli/finestra_2_ante/configurazione')
->assertOk()
->assertJsonCount(48, 'finiture')
->assertJsonCount(14, 'vetri')
->assertJsonCount(6, 'misure')
->assertJsonPath('form.radios.apertura.1.scale_x', -1)
->assertJsonPath('form.select.cerniere_a_scomparsa.1.func', 'nascondiCerniere')
->assertJsonPath('form.finiture.1.nome', 'finitura_esterna')
->json();
$this->assertStringContainsString('/api/media/configuratore/pvc/opzioni/apertura/finestra_2_ante/', $json['form']['radios']['apertura'][0]['icon']);
$this->assertStringContainsString('/api/media/configuratore/pvc/finiture/texture/2/2_A.jpg', $json['finiture'][1]['texture_map']);
$this->assertArrayNotHasKey('maggiorazione_mq', $json['vetri'][0]);
$this->assertArrayNotHasKey('supplemento', $json['form']['select']['cerniere_a_scomparsa'][1]);
// Modello senza finitura esterna né vetro (cassonetto).
$this->conListino('cassonetto_80');
$this->getJson('/api/sistemi/pvc/modelli/cassonetto_80/configurazione')
->assertJsonCount(1, 'form.finiture')
->assertJsonCount(0, 'vetri')
->assertJsonPath('form.radios', []);
}
public function test_media_serve_le_immagini_con_cors_e_blocca_altri_percorsi(): void
{
$percorso = Vetro::where('codice', 'V2')->value('texture_bump');
$this->get("/api/media/{$percorso}", ['Origin' => 'https://www.kreiosinfissi.it'])
->assertOk()
->assertHeader('Access-Control-Allow-Origin');
Storage::disk('public')->put('preventivi/segreto.png', 'x');
$this->get('/api/media/preventivi/segreto.png')->assertNotFound();
$this->get('/api/media/configuratore/../preventivi/segreto.png')->assertNotFound();
$this->get('/api/media/configuratore/pvc/inesistente.jpg')->assertNotFound();
}
public function test_il_carrello_ricalcola_il_prezzo_e_ignora_quello_del_client(): void
{
$this->conListino('finestra_2_ante');
$response = $this->postJson('/api/carrello/aggiungi', [
'sistema' => 'pvc',
'type' => 'finestra_2_ante',
'qty' => 2,
'prezzo' => 1,
'scelte' => ['larghezza' => 1200, 'altezza' => 1400, 'finitura_interna' => '2', 'finitura_esterna' => '2', 'vetro' => 'V1'],
'payload' => ['vetro' => 'Vetro inventato', 'altezza_maniglia' => '1050 mm'],
])->assertCreated();
$item = PreventivoItem::firstOrFail();
// 500 x 1,27 + 86,02944
$this->assertEquals(721.03, $item->prezzo_unitario);
$this->assertEquals(1442.06, $item->prezzo_totale);
$this->assertSame('Acustico base', $item->payload['vetro']);
$this->assertSame('1050 mm', $item->payload['altezza_maniglia']);
$this->assertSame('FINESTRA 2 ANTE PVC - SALAMANDER BE 82', $item->nome_modello);
$response->assertJsonPath('carrello.items.0.prezzo_unitario', 721.03);
// Senza scelte non si può aggiungere: il prezzo non arriva più dal client.
$this->postJson('/api/carrello/aggiungi', ['sistema' => 'pvc', 'type' => 'finestra_2_ante', 'prezzo' => 1])
->assertJsonValidationErrors('scelte');
}
public function test_lato_delle_finiture_sul_sistema(): void
{
$this->conListino('finestra_2_ante');
Finitura::where('tipo', 'profilo')->where('codice', '2')->update(['lato' => 'interno']);
Finitura::where('tipo', 'profilo')->where('codice', '3')->update(['lato' => 'esterno']);
$json = $this->getJson('/api/sistemi/pvc/modelli/finestra_2_ante/configurazione')->assertOk()->json();
$interne = array_column($json['finiture_interne'], 'id');
$esterne = array_column($json['finiture_esterne'], 'id');
$this->assertContains('2', $interne);
$this->assertNotContains('2', $esterne);
$this->assertContains('3', $esterne);
$this->assertNotContains('3', $interne);
$this->assertSame('data_finiture_esterne', $json['form']['finiture'][1]['data']);
$misura = ['larghezza' => 1200, 'altezza' => 1400];
$this->prezzo('finestra_2_ante', $misura + ['finitura_interna' => '2', 'finitura_esterna' => '3'])->assertOk();
$this->prezzo('finestra_2_ante', $misura + ['finitura_esterna' => '2'])->assertJsonValidationErrors('finitura_esterna');
$this->prezzo('finestra_2_ante', $misura + ['finitura_interna' => '3'])->assertJsonValidationErrors('finitura_interna');
}
public function test_attivazione_e_lato_delle_finiture_per_modello(): void
{
$modello = $this->conListino('finestra_2_ante');
$this->conListino('finestra_1_anta');
$finitura = fn (string $codice) => Finitura::where('tipo', 'profilo')->where('codice', $codice)->value('id');
Finitura::whereKey($finitura('5'))->update(['attivo' => false]);
DB::table('finiture_modelli')->insert([
// Attiva nel sistema, disattivata nel modello.
['modello_id' => $modello->id, 'finitura_id' => $finitura('4'), 'lato' => 'nessuno'],
// Disattiva nel sistema, attivata nel modello solo sull'esterno.
['modello_id' => $modello->id, 'finitura_id' => $finitura('5'), 'lato' => 'esterno'],
]);
$json = $this->getJson('/api/sistemi/pvc/modelli/finestra_2_ante/configurazione')->json();
$this->assertNotContains('4', array_column($json['finiture_interne'], 'id'));
$this->assertNotContains('4', array_column($json['finiture_esterne'], 'id'));
$this->assertContains('5', array_column($json['finiture_esterne'], 'id'));
$this->assertNotContains('5', array_column($json['finiture_interne'], 'id'));
$misura = ['larghezza' => 1200, 'altezza' => 1400];
$this->prezzo('finestra_2_ante', $misura + ['finitura_esterna' => '4'])->assertJsonValidationErrors('finitura_esterna');
$this->prezzo('finestra_2_ante', $misura + ['finitura_esterna' => '5'])->assertOk()->assertJsonPath('configurazione.tipo_finitura', 'Bicolore');
// Gli altri modelli seguono il sistema.
$altro = $this->getJson('/api/sistemi/pvc/modelli/finestra_1_anta/configurazione')->json();
$this->assertContains('4', array_column($altro['finiture_interne'], 'id'));
$this->assertNotContains('5', array_column($altro['finiture_esterne'], 'id'));
}
public function test_profili_extra_percentuale_metro_lineare_e_cadauno(): void
{
$this->conListino('finestra_2_ante');
$misura = ['larghezza' => 1200, 'altezza' => 1400];
// Nessun profilo: nessun supplemento.
$this->prezzo('finestra_2_ante', $misura)->assertJsonPath('prezzo', self::BASE)->assertJsonPath('scelte.opzioni.profili_extra', []);
// Coprifili scatolati +3% = 15; guida 34: (1400 + 100) x 2 = 3 ml x 15 = 45; HO9320: 85 cad.;
// NP8300: perimetro (1200 + 1400) x 2 = 5,2 ml x 25 = 130.
$this->prezzo('finestra_2_ante', $misura + ['opzioni' => ['profili_extra' => ['NP8300', 'coprifili_scatolati', '50811', '9320']]])
->assertOk()
->assertJsonPath('prezzo', 775)
->assertJsonPath('configurazione.profili_extra', 'COPRIFILI SCATOLATI, 50811_GUIDA 34, HO9320_T/M, NP8300_ALL.30')
->assertJsonPath('dettaglio.supplementi.1.metri_lineari', 3);
}
public function test_profili_extra_alternativi_e_per_modello(): void
{
$this->conListino('finestra_2_ante');
$this->conListino('celino');
$misura = ['larghezza' => 1200, 'altezza' => 1400];
// Piatti e scatolati sono alternativi; il frontale è solo del cielino.
$this->prezzo('finestra_2_ante', $misura + ['opzioni' => ['profili_extra' => ['coprifili_piatti', 'coprifili_scatolati']]])
->assertJsonValidationErrors('opzioni.profili_extra');
$this->prezzo('finestra_2_ante', $misura + ['opzioni' => ['profili_extra' => ['406200']]])
->assertJsonValidationErrors('opzioni.profili_extra');
// Cielino: frontale 4062000 solo sulla larghezza, 1,2 ml x 145 = 174.
$this->prezzo('celino', $misura + ['opzioni' => ['profili_extra' => ['406200']]])
->assertOk()
->assertJsonPath('prezzo', 674);
$this->prezzo('celino', $misura + ['opzioni' => ['profili_extra' => ['9320']]])
->assertJsonValidationErrors('opzioni.profili_extra');
$finestra = $this->getJson('/api/sistemi/pvc/modelli/finestra_2_ante/configurazione')
->assertJsonCount(8, 'form.multiple.profili_extra')
->assertJsonPath('form.multiple.profili_extra.0.alternativa', 'coprifili')
->json();
$this->assertStringContainsString('/api/media/configuratore/pvc/opzioni/sistema/profili_extra/coprifili_piatti.jpg', $finestra['form']['multiple']['profili_extra'][0]['icon']);
$this->getJson('/api/sistemi/pvc/modelli/celino/configurazione')
->assertJsonCount(1, 'form.multiple.profili_extra')
->assertJsonPath('form.multiple.profili_extra.0.valore', '406200');
$this->conListino('scuretto_base');
$this->getJson('/api/sistemi/pvc/modelli/scuretto_base/configurazione')->assertJsonPath('form.multiple', []);
}
public function test_il_seeder_dei_profili_extra_gira_una_volta(): void
{
Opzione::where('gruppo', 'profili_extra')->whereNull('modello_id')->where('valore', '9320')->update(['supplemento' => 90]);
$this->seed(ProfiliExtraSeeder::class);
$this->assertSame(9, Opzione::where('gruppo', 'profili_extra')->count());
$this->assertEquals(90, Opzione::where('gruppo', 'profili_extra')->where('valore', '9320')->value('supplemento'));
$this->assertFalse(Modello::where('codice', 'cassonetto_80')->firstOrFail()->haModulo('profili_extra'));
$this->assertTrue(Modello::where('codice', 'celino')->firstOrFail()->haModulo('profili_extra'));
}
public function test_cerniere_soglia_e_motore_per_modello(): void
{
$misura = ['larghezza' => 1200, 'altezza' => 1400];
foreach (['finestra_1_anta', 'finestra_3_ante', 'portafinestra_2_ante', 'finestra_vasistas', 'porta_1_anta'] as $codice) {
$this->conListino($codice);
}
// Cerniere a scomparsa: 50 € di sistema, 150 € sulla finestra 3 ante.
$this->prezzo('finestra_1_anta', $misura + ['opzioni' => ['cerniere_a_scomparsa' => 'si']])->assertJsonPath('prezzo', 550);
$this->prezzo('finestra_3_ante', $misura + ['opzioni' => ['cerniere_a_scomparsa' => 'si']])->assertJsonPath('prezzo', 650);
// Portafinestra 2 ante: cerniere 100 + soglia ribassata 105.
$this->prezzo('portafinestra_2_ante', $misura + ['opzioni' => ['cerniere_a_scomparsa' => 'si', 'soglia_ribassata' => 'si']])
->assertJsonPath('prezzo', 705)
->assertJsonPath('configurazione.soglia_ribassata', 'SI');
// Wasistas: motore 150 €, nasconde la maniglia nel 3D.
$this->prezzo('finestra_vasistas', $misura + ['opzioni' => ['motore' => 'si']])
->assertJsonPath('prezzo', 650)
->assertJsonPath('configurazione.motore', 'SI');
$this->getJson('/api/sistemi/pvc/modelli/finestra_vasistas/configurazione')
->assertJsonPath('form.select.motore.1.func', 'nascondiCategoria')
->assertJsonPath('form.select.motore.1.categoria', 'maniglia')
->assertJsonPath('form.gruppi.motore.sezione', 'accessori')
->assertJsonPath('form.gruppi.motore.etichetta', 'Motore');
// Porta: niente cerniere a scomparsa né soglia (foglio "Nomi": NO).
$porta = $this->getJson('/api/sistemi/pvc/modelli/porta_1_anta/configurazione')->json('form.select');
$this->assertSame([], $porta);
$this->prezzo('porta_1_anta', $misura + ['opzioni' => ['soglia_ribassata' => 'si']])->assertJsonPath('prezzo', self::BASE);
}
public function test_un_gruppo_creato_dal_backoffice_compare_nel_configuratore(): void
{
$modello = $this->conListino('finestra_2_ante');
\App\Models\GruppoOpzioni::create(['codice' => 'zanzariera', 'etichetta' => 'Zanzariera', 'ui' => 'select', 'sezione' => 'accessori', 'modulo' => 'zanzariera', 'ordine' => 9]);
foreach ([['no', 'NO', 0], ['si', 'SI', 80]] as [$valore, $etichetta, $prezzo]) {
Opzione::create(['sistema_id' => $modello->sistema_id, 'gruppo' => 'zanzariera', 'valore' => $valore, 'etichetta' => $etichetta, 'supplemento_tipo' => 'fisso', 'supplemento' => $prezzo]);
}
$misura = ['larghezza' => 1200, 'altezza' => 1400, 'opzioni' => ['zanzariera' => 'si']];
// Senza il modulo nel modello il gruppo non c'è.
$this->prezzo('finestra_2_ante', $misura)->assertJsonPath('prezzo', self::BASE);
$modello->update(['moduli' => [...$modello->moduli, 'zanzariera']]);
$this->assertArrayHasKey('zanzariera', Modello::moduliDisponibili());
$this->prezzo('finestra_2_ante', $misura)->assertJsonPath('prezzo', 580);
$this->getJson('/api/sistemi/pvc/modelli/finestra_2_ante/configurazione')->assertJsonPath('form.gruppi.zanzariera.etichetta', 'Zanzariera');
}
public function test_il_seeder_delle_nuove_opzioni_gira_una_volta(): void
{
Opzione::where('gruppo', 'motore')->where('valore', 'si')->update(['supplemento' => 170]);
$this->seed(NuoveOpzioniSeeder::class);
$this->assertEquals(170, Opzione::where('gruppo', 'motore')->where('valore', 'si')->value('supplemento'));
$this->assertSame(2, Opzione::where('gruppo', 'motore')->count());
$vasistas = Modello::where('codice', 'finestra_vasistas')->firstOrFail();
$this->assertTrue($vasistas->haModulo('motore') && $vasistas->haModulo('cerniere'));
$this->assertFalse(Modello::where('codice', 'finestra_anta_fissa')->firstOrFail()->haModulo('cerniere'));
}
}