feat: implement discount and transport features in the quote system
- Added maximum discount settings configurable by the administrator. - Implemented discount calculations in the quote confirmation process. - Introduced transport options with associated costs, including handling for exceeding maximum piece limits. - Updated frontend to display discounts and transport information in the quote summary. - Created new database migrations for storing discount and transport settings. - Developed a new Filament page for managing global discount settings. - Enhanced email templates to include discount and transport details. - Added tests to ensure proper functionality of discounts and transport calculations.
This commit is contained in:
@@ -13,10 +13,16 @@ use Illuminate\Validation\ValidationException;
|
||||
/**
|
||||
* Prezzo di una configurazione, calcolato solo lato server dalle regole del backoffice:
|
||||
*
|
||||
* prezzo = base x (1 + supplementi % / 100) + supplementi fissi + variazione vetro
|
||||
* a fasi, ciascuna calcolata sul totale della precedente:
|
||||
*
|
||||
* serramento = base x (1 + finitura % / 100) + finitura fissi
|
||||
* con profili = serramento x (1 + profili extra % / 100) + profili extra fissi/al ml
|
||||
* prezzo = con profili x (1 + altre opzioni % / 100) + altre opzioni fisse + variazione vetro
|
||||
*
|
||||
* - base: prima misura del listino >= a quella richiesta (mm), entro min/max del listino;
|
||||
* - supplementi: tipo finitura (bianco/uniforme/bicolore) e opzioni scelte (gruppi di Opzione);
|
||||
* - finitura: tipo finitura (bianco/uniforme/bicolore), cioè solo misure + finiture;
|
||||
* - profili extra: sul serramento finito, prima di vetro e accessori;
|
||||
* - altre opzioni: telaio, apertura, accessori (gruppi di Opzione);
|
||||
* - variazione vetro: somma delle aree fatturabili delle lastre (max(area, minimo) per lastra)
|
||||
* per la maggiorazione €/m² del riempimento, con le lastre date dalla regola vetro del modello.
|
||||
*
|
||||
@@ -24,6 +30,9 @@ use Illuminate\Validation\ValidationException;
|
||||
*/
|
||||
class CalcolatorePrezzo
|
||||
{
|
||||
/** Gruppo calcolato sul serramento finito (misure + finiture), prima di vetro e accessori. */
|
||||
private const PROFILI_EXTRA = 'profili_extra';
|
||||
|
||||
/**
|
||||
* @param array{larghezza?:mixed,altezza?:mixed,finitura_interna?:mixed,finitura_esterna?:mixed,maniglia?:mixed,vetro?:mixed,opzioni?:array<string,mixed>} $scelte
|
||||
* @return array{prezzo:float,scelte:array,configurazione:array,dettaglio:array}
|
||||
@@ -37,7 +46,8 @@ class CalcolatorePrezzo
|
||||
[$base, $misuraListino] = $this->prezzoBase($modello, $larghezza, $altezza);
|
||||
|
||||
$opzioni = Opzione::perModello($modello);
|
||||
$supplementi = [];
|
||||
// Opzioni scelte per fase: [voce, Opzione, valore scelto]; gli importi dopo, a cascata.
|
||||
$fasi = ['finitura' => [], 'profili' => [], 'altro' => []];
|
||||
$configurazione = [
|
||||
'nome' => $modello->titolo ?: ($modello->descrizione ?: $modello->nome),
|
||||
'width' => $larghezza,
|
||||
@@ -60,7 +70,7 @@ class CalcolatorePrezzo
|
||||
default => 'uniforme',
|
||||
};
|
||||
$voce = $opzioni->get('tipo_finitura', collect())->firstWhere('valore', $tipo);
|
||||
$supplementi[] = $this->supplemento('Tipo finitura', $voce, $base, $larghezza, $altezza, $tipo);
|
||||
$fasi['finitura'][] = ['Tipo finitura', $voce, $tipo];
|
||||
|
||||
$configurazione['tipo_finitura'] = $voce?->etichetta ?? ucfirst($tipo);
|
||||
$configurazione['finitura_interna'] = $interna->descrizione;
|
||||
@@ -87,10 +97,12 @@ class CalcolatorePrezzo
|
||||
continue;
|
||||
}
|
||||
|
||||
$fase = $gruppo === self::PROFILI_EXTRA ? 'profili' : 'altro';
|
||||
|
||||
if ($definizione->ui === 'multipla') {
|
||||
$scelteGruppo = $this->scegliMultiple($righe, $scelte['opzioni'][$gruppo] ?? [], "opzioni.{$gruppo}");
|
||||
foreach ($scelteGruppo as $scelta) {
|
||||
$supplementi[] = $this->supplemento($definizione->etichetta, $scelta, $base, $larghezza, $altezza);
|
||||
$fasi[$fase][] = [$definizione->etichetta, $scelta, null];
|
||||
}
|
||||
if ($scelteGruppo->isNotEmpty()) {
|
||||
$configurazione[$gruppo] = $scelteGruppo->pluck('etichetta')->implode(', ');
|
||||
@@ -101,7 +113,7 @@ class CalcolatorePrezzo
|
||||
}
|
||||
|
||||
$scelta = $this->scegli($righe, $scelte['opzioni'][$gruppo] ?? null, "opzioni.{$gruppo}", null, 'valore');
|
||||
$supplementi[] = $this->supplemento($definizione->etichetta, $scelta, $base, $larghezza, $altezza);
|
||||
$fasi[$fase][] = [$definizione->etichetta, $scelta, null];
|
||||
$configurazione[$gruppo] = $scelta->etichetta;
|
||||
$normalizzate['opzioni'][$gruppo] = $scelta->valore;
|
||||
}
|
||||
@@ -117,11 +129,22 @@ class CalcolatorePrezzo
|
||||
}
|
||||
}
|
||||
|
||||
$supplementi = array_values(array_filter($supplementi));
|
||||
$percentuale = array_sum(array_column(array_filter($supplementi, fn ($s) => $s['tipo'] === 'percentuale'), 'valore'));
|
||||
// Importi fissi e al metro lineare (già calcolati sulle misure).
|
||||
$fisso = array_sum(array_column(array_filter($supplementi, fn ($s) => $s['tipo'] !== 'percentuale'), 'importo'));
|
||||
$prezzo = $base * (1 + $percentuale / 100) + $fisso + ($vetro['importo'] ?? 0);
|
||||
// Ogni fase applica le sue percentuali al totale della precedente.
|
||||
$supplementi = [];
|
||||
$subtotali = [];
|
||||
$totale = $base;
|
||||
foreach ($fasi as $fase => $voci) {
|
||||
$daAggiungere = 0.0;
|
||||
foreach ($voci as [$etichetta, $opzione, $valore]) {
|
||||
if ($supplemento = $this->supplemento($etichetta, $opzione, $totale, $larghezza, $altezza, $valore)) {
|
||||
$supplementi[] = $supplemento + ['fase' => $fase];
|
||||
$daAggiungere += $supplemento['importo'];
|
||||
}
|
||||
}
|
||||
$totale += $daAggiungere;
|
||||
$subtotali[$fase] = $totale;
|
||||
}
|
||||
$prezzo = $totale + ($vetro['importo'] ?? 0);
|
||||
|
||||
return [
|
||||
'prezzo' => round($prezzo, 2),
|
||||
@@ -131,8 +154,8 @@ class CalcolatorePrezzo
|
||||
'prezzo_base' => $base,
|
||||
'misura_listino' => $misuraListino,
|
||||
'supplementi' => $supplementi,
|
||||
'percentuale_totale' => $percentuale,
|
||||
'fisso_totale' => $fisso,
|
||||
// Totale dopo ogni fase (finitura = serramento, profili = con profili extra).
|
||||
'subtotali' => $subtotali,
|
||||
'vetro' => $vetro,
|
||||
],
|
||||
];
|
||||
@@ -215,6 +238,7 @@ class CalcolatorePrezzo
|
||||
return $righe->filter(fn (Opzione $o) => $scelte->contains(fn (Opzione $s) => $s->is($o)))->values();
|
||||
}
|
||||
|
||||
/** @param float $base totale della fase precedente, su cui si applicano le percentuali */
|
||||
private function supplemento(string $voce, ?Opzione $opzione, float $base, int $larghezza, int $altezza, ?string $valore = null): ?array
|
||||
{
|
||||
if (! $opzione || ! $opzione->supplemento_tipo || ! $opzione->supplemento) {
|
||||
|
||||
Reference in New Issue
Block a user