# Nuovo listino: un credito = 4 ore d'aula fino a 30 discenti

Status: validated (16/09/2026; deciso dall'utente il 15/09/2026 dopo `docs/marketing/analisi-strategia-pricing.md` §3.6)

## Context

La formula `ceil(discenti × ore / 5)` esplode sui corsi lunghi (un corso base da 250 ore con 20 discenti costa
1.000 crediti, tre Enterprise mensili), fattura su un tetto dichiarato invece che sui discenti reali e lascia un
buco nella scala 40/120/350 esattamente dove sta il cliente tipico. Il credito deve tornare a valere una cosa
riconoscibile: **mezza giornata d'aula**. Posizionamento confermato: EasyForma resta snella (aula BBB + registro +
invio agli enti), non un gestionale.

Formula nuova (configurabile in `config/easyform.php`):
`crediti = ceil(ore_massime / credit_hours_per_credit) × ceil(discenti_massimi / credit_participants_band)`
con `credit_hours_per_credit = 4` e `credit_participants_band = 30`. Esempi: 12 discenti × 8 h = 2 crediti;
15 × 16 h = 4; 20 × 40 h = 10; 45 × 8 h = 4; 20 × 250 h = 63.

## Requirements

- `CreditService::costFor(participants, hours)` applica la formula nuova; `divisor()` sparisce; le due costanti sono
  esposte al frontend (prop Inertia condivisa `pricing` con `hours_per_credit` e `participants_band`) e
  `resources/js/lib/credits.ts` usa la stessa formula con gli stessi default (4, 30). Chiave
  `credit_participant_hours` rimossa da config, `.env.example` e docs.
- `PackageSeeder`: piani Base 59 €/10 crediti, Pro 189 €/40, Enterprise 499 €/150 (descrizione "da 499 €" sulla
  vetrina, contratto annuale su richiesta); ricariche `ricarica-10` 69 € e `ricarica-50` 249 €; i vecchi slug
  `ricarica-30` e `ricarica-100` vengono disattivati, non cancellati (ordini esistenti). Descrizioni in una frase
  con un esempio in ore d'aula ("40 ore d'aula al mese: cinque corsi da 8 ore fino a 30 discenti").
- Rollover: alla concessione dei crediti di un nuovo periodo (`invoice.payment_succeeded`), i crediti del piano non
  consumati del periodo precedente vengono riportati con scadenza a fine del nuovo periodo, fino a un massimo pari
  ai crediti mensili del piano (`credit_grants.rolled_over_from_id` nullable per tracciarli; movimento `kind =
rollover`). Solo per partite `source = plan`; le ricariche seguono `credits_expire_days` come oggi.
- Form aula (`credit-cost-field.tsx`, create/edit): l'etichetta spiega la regola ("1 credito = 4 ore d'aula fino a 30
  discenti"), il costo si aggiorna in tempo reale; i messaggi "Ti mancano N crediti" restano.
- Vetrina `/prezzi`: card con "X crediti al mese = Y ore d'aula"; riga di confronto "Corsi da 8 ore al mese" =
  `floor(credits / 2)` (sostituisce `floor(credits / 24)`); calcolatore rovesciato: input "corsi al mese" e "ore per
  corso" (default 4 e 8), output crediti al mese, piano consigliato (il più economico che copre il fabbisogno,
  altrimenti Enterprise + ricarica) e costo per corso. FAQ e testo dei crediti aggiornati; home e onboarding
  passo 3 aggiornati; `docs/marketing-copy.md` §8, §9, §10, §13 riscritti; `docs/ARCHITECTURE.md`, `README.md`,
  `CLAUDE.md` e `specs/piani-e-crediti.md` allineati (nessun riferimento a "/ 5" o "24 crediti").
- Demo seeder: aule e saldi coerenti col nuovo listino (saldo demo = Pro 40 crediti meno le aule seedate).
- Nessuna migrazione di conversione dei dati: lo schema resta quello atomico, la colonna `rolled_over_from_id` va
  aggiunta al file `create_credit_grants_table` (progetto non ancora in produzione).

## Acceptance criteria

1. `CreditService::costFor(12, 8) === 2`, `costFor(15, 16) === 4`, `costFor(20, 40) === 10`, `costFor(45, 8) === 4`,
   `costFor(20, 250) === 63`, `costFor(0, 8) === 0`; `creditCost(45, 8)` in `credits.ts` restituisce 4.
2. `php artisan migrate:fresh --seed` produce 3 piani attivi (base 5900/10, pro 18900/40, enterprise 49900/150) e 2
   ricariche attive (`ricarica-10` 6900/10, `ricarica-50` 24900/50); `ricarica-30` e `ricarica-100` non esistono o
   sono `is_active = false`; `POST /classrooms` con `max_participants = 20, max_hours = 40` e saldo 40 crea l'aula
   con `credits_cost = 10` e saldo 30.
3. Company con piano Pro, partita del periodo corrente da 40 crediti con 25 rimanenti: un nuovo
   `invoice.payment_succeeded` (fattura `in_2`, periodo successivo) crea la partita da 40 con scadenza a fine periodo
   più una partita `rollover` da 25 con la stessa scadenza; con 55 rimanenti (piano + ricariche non contano: solo
   partite `source = plan`) il rollover è al massimo 40; lo stesso evento ripetuto non crea altre partite.
4. GET /prezzi contiene "1 credito = 4 ore", "59 €", "189 €", "499 €", "40 ore d'aula" e il calcolatore con i campi
   "Corsi al mese" e "Ore per corso"; con 6 corsi da 16 ore il calcolatore mostra 24 crediti e consiglia "Pro"; la
   riga "Corsi da 8 ore al mese" mostra 5 / 20 / 75. GET / non contiene "24 crediti" né "/ 5".
5. `grep -rn "credit_participant_hours\|ore / 5\|hours / 5\|\* max_hours / 5" app config resources/js tests docs README.md CLAUDE.md` non trova
   riferimenti alla formula vecchia (escluso `docs/marketing/analisi-strategia-pricing.md`, storico; il `/ 5` di
   `weekly-chart.tsx` è l'arrotondamento dell'asse, non la formula);
   `composer test`, `npm run types:check`, `npm run check`, `npm run build` passano.

## Out of scope

Prova gratuita 14 giorni, piano annuale scontato, fatturazione a bonifico, prezzi Enterprise personalizzati oltre
il "da 499 €", OTP di riconoscimento del discente, accesso ispettivo (tutti candidati al giro successivo).
