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); // I profili extra si calcolano sul serramento con le finiture: uniforme 500 x 1,27 = 635, // coprifili +3% di 635 = 19,05; la cerniera (+100) viene dopo e non entra nel 3%. $this->prezzo('finestra_2_ante', $misura + ['finitura_interna' => '2', 'finitura_esterna' => '2', 'opzioni' => ['profili_extra' => ['coprifili_scatolati'], 'cerniere_a_scomparsa' => 'si']]) ->assertJsonPath('prezzo', 754.05) ->assertJsonPath('dettaglio.subtotali.finitura', 635) ->assertJsonPath('dettaglio.subtotali.profili', 654.05); } 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')); } }