Migrazione WP → Storm Support · Fase F1 «Fondamenta minime»

Storm Support dopo F1

Cosa cambia nel codice di Support quando i quattro piani di F1 sono chiusi, e quali regole deve rispettare da ora in poi chi sviluppa una nuova funzionalità su Support.

F1-01 convenzioni · in epic F1-02 accessi · in epic F1-03 sync inversa · in epic F1-04 comandi agenda · in corso

Stato al 12 novembre 2026. epic/migrazione-F1 è a 88e74270, riallineato su main: 5371 esempi verdi, rubocop e packwerk puliti. F1-04 è al Task 1. Questa pagina descrive Support a F1-04 concluso, come da piano.

In sintesi

Prima e dopo F1

F1 non porta schermate nuove per gli operatori. Cambia il modo in cui Support scrive: gli appuntamenti nascono in Support e arrivano a WP per proiezione, ogni scrittura ha un autore, ogni utente ha un ruolo, e i contesti nuovi hanno confini controllati in CI.

TemaPrima di F1Dopo F1
Struttura del codiceTutto in app/, diviso per tipo tecnico (services, reactors, subscribers). Nessun confine.Sei pacchetti packwerk (kernel, accessi, rete, logistica, pianificazione, legacy_wp) con facciata pubblica in app/public/. packwerk check gira in CI. Il codice esistente resta nella radice, senza controlli.
Utenti e permessiSolo il flag users.admin. Qualunque sessione entra in agenda, /wp_posts, /jobs.users.ruolo con 8 ruoli e la matrice Accessi::Permessi. Le aree che scrivono sono protette. I 54 utenti WP sono importati; i ruoli senza azioni non possono entrare.
Creare un appuntamentoActions::OnWp::CreateAppuntamento scrive su WP, poi il webhook crea il record locale.Pianificazione::Comandi::FissaAppuntamento scrive in Support e pubblica pia.appuntamento.fissato. LegacyWp proietta su WP dopo il commit. Tutto dietro la feature agenda_comandi_locali.
Annullare un appuntamentoCestino su WP; il record locale sparisce o resta orfano.stato = annullato con motivo, autore e data. Su WP il post viene cancellato in modo definitivo. Tutti i lettori usano Appuntamento.programmati.
Chi ha fatto cosaAudit con l'utente della richiesta, se c'è. Job e webhook anonimi.Attore su ogni scrittura: utente, job, utente WP risolto sull'utente Support, import, form pubblico. Viaggia anche nei job.
EventiSolo eventi tecnici ss.wp.* su Rails.event, consegnati in memoria.Eventi di dominio <contesto>.<aggregato>.<fatto> salvati nell'outbox kernel_eventi nella transazione del comando, consegnati dopo il commit e riconsegnati da un job.
Webhook da WPOgni webhook viene applicato, anche quello generato da una scrittura di Support.Filtro anti-eco (utente di sync 121, solo record proiettati). Registro delle scritture WP per 90 giorni. Update rinviato finché c'è una proiezione in attesa.
Client WPClients::Wp ripete da solo un POST interrotto: rischio di post doppi.Il POST non si ripete da solo. La creazione è idempotente con slug ss-<uuid>.
MonitoraggioNessuna vista sulle scritture verso WP./admin/legacy_wp/proiezioni (stati, errori, "Riprova") e /admin/legacy_wp/riconciliazione.
Cosa c'è nel codice

Le fondamenta introdotte

F1-01 · confini

Pacchetti packwerk

  • Facciata in packs/<nome>/app/public/: comandi, query, errori, catalogo eventi. Il resto è privato, anche per la radice.
  • Dipendenze dichiarate, cicli vietati. Nessun contesto dipende da packs/legacy_wp.
  • Tra pacchetti si tengono gli id con FK a DB, senza associazioni ActiveRecord.
F1-01 · kernel

Attore, eventi, regole

  • Attore e Current.attore; nei job con ApplicationJob.con_attore.
  • Eventi.pubblica con outbox, Eventi::RiconsegnaJob ogni 5 minuti, Eventi::Idempotente per i subscriber.
  • Dominio::RegolaViolata(codice, forzabile:), messaggi in config/locales/dominio.it.yml.
F1-02 · accessi

Ruoli e permessi

  • richiedi_permesso!(:azione) nei controller, consentito?(:azione) in viste e policy.
  • Import utenti WP (legacy_wp:importa_utenti), inviti solo ad admin e operation, abbinamento utenti ↔ operatori.
  • users.admin è in ignored_columns; il drop è in un rilascio successivo.
F1-03 · legacy_wp

Sync inversa verso WP

  • Registro dei kind, adattatori, legacy_wp_proiezioni, ProiettaJob sulla coda wp_write.
  • "Sconosciuto ≠ vuoto": si inviano solo i campi posseduti e cambiati.
  • Anti-eco, registro delle scritture, autore WP risolto, cruscotto e riconciliazione.
F1-04 · pianificazione

Comandi dell'agenda

  • FissaAppuntamento, ModificaAppuntamento, AnnullaAppuntamento con MOTIVI.
  • Colonne stato, annullato_il, annullato_da_id, motivo_annullamento; FK su hub.
  • Adattatore WP dell'appuntamento: titolo #ID, token di conferma, cancellazione definitiva.
Il cambio più visibile

Il giro di un appuntamento

Prima: Support aspetta WP

flowchart TD
  A[Controller o job] --> B[Actions::OnWp::CreateAppuntamento]
  B -->|REST| W[(WordPress)]
  W -->|webhook| P[PullWpPostJob]
  P --> H[AppuntamentoHandler]
  H --> L[(appuntamenti)]

Dopo: Support scrive, WP segue

flowchart TD
  A[Controller o job] --> C[FissaAppuntamento]
  C --> L[(appuntamenti)]
  C -->|stessa transazione| O[(kernel_eventi)]
  O -->|dopo il commit| S[RichiestaProiezioneSubscriber]
  S --> J[ProiettaJob · wp_write]
  J -->|REST| W[(WordPress)]
  W -->|webhook dell'utente 121| E[FiltroEco: scartato]

Con agenda_comandi_locali spenta il comportamento resta quello di prima. Fa eccezione CreaAppuntamentoSelfAppointmentJob, che scrive su WP anche a feature attiva fino a F2-04 (decisione R1).

Per chi sviluppa su Support

Attenzioni per le nuove funzionalità

Prima di F1 una funzionalità su Support poteva scrivere dove voleva, leggere ogni appuntamento e chiamare WP direttamente. Dopo F1 non è più così. Ogni voce dice cosa succede se la regola viene ignorata.

CI ROSSA lo blocca packwerk, rubocop o un test ERRORE A RUNTIME si rompe in produzione ERRORE SILENZIOSO nessun errore, dati sbagliati

Dove mettere il codice

Vale per ogni file nuovo.

CI ROSSA
Il codice di un contesto nuovo va nel suo pacchetto

Tecnici in packs/rete, flotta in packs/logistica, agenda nuova in packs/pianificazione. Quello che serve ad altri va in app/public/; anche controller e viste nuove stanno nel pacchetto, perché la radice non vede i modelli privati.

CI ROSSA
Niente associazioni tra pacchetti

Si tiene pratica_id con FK, non belongs_to verso un modello privato di un altro pacchetto. Verso i modelli della radice (Appuntamento, Hub, Pratica, User) le associazioni sono ammesse.

CI ROSSA
Mai dipendere da packs/legacy_wp

Un contesto non conosce WP. Non usare bin/packwerk update-todo per nascondere una violazione nuova: serve solo al codice storico.

ERRORE A RUNTIME
Packwerk non vede le stringhe

constantize, class_name: 'X', i job in recurring.yml, i nomi degli eventi e il registro di LegacyWp sfuggono al controllo. Una classe rinominata rompe solo a runtime: serve un test che risolva il nome.

ERRORE A RUNTIME
Costanti omonime con ::

Dentro un modulo annidato si scrive ::Eventi.pubblica. Dove convivono Query::X e un modello X si usa il nome completo (::Logistica::Query::Capienza).

ERRORE SILENZIOSO
La radice resta senza controlli

Il legacy WP nella radice (WpPost, Wp::*, Actions::OnWp::*) è usabile dai pacchetti senza che packwerk lo segnali. È una deroga accettata per F1–F2: va controllata in code review.

Come si scrive un dato

Comandi, eventi e attore.

ERRORE SILENZIOSO
Si scrive solo con un comando

Stile self.call(**kw) → new(**kw).call, con attore: obbligatorio. I controller non contengono logica di dominio. Una scrittura fuori dai comandi non pubblica l'evento e non arriva a WP.

ERRORE SILENZIOSO
Ogni comando pubblica il suo evento nella stessa transazione

Eventi.pubblica('pia.appuntamento.modificato', aggregato:, attore:, **dati). Senza l'evento, o senza la voce nel registro, il record non arriva mai su WP e nessuno se ne accorge.

ERRORE SILENZIOSO
I subscriber sono idempotenti e rimandano gli effetti esterni

La consegna è "almeno una volta". Usare Eventi::Idempotente con una_volta(event) { … }; job accodati, chiamate HTTP e messaggi vanno in ActiveRecord.after_all_transactions_commit, altrimenti alla riconsegna partono due volte.

ERRORE SILENZIOSO
Un subscriber che fallisce non blocca la consegna

Rails.event.notify segnala l'errore e segna comunque l'evento come consegnato. L'outbox protegge dalla perdita dell'evento, non dal bug del subscriber: va monitorato con Rails.error.

ERRORE SILENZIOSO
L'attore non è un argomento del job

MioJob.con_attore(attore).perform_later(...). Senza attore esplicito il job registra job:<NomeJob>, e l'attore implicito non passa ai job figli.

CI ROSSA
Regole di dominio con Dominio::RegolaViolata

Codice simbolico stabile, messaggio in it.dominio.regole.<codice> dentro config/locales/dominio.it.yml, forzabile: true se un operatore autorizzato può passare oltre.

Permessi

Prima bastava avere una sessione.

ERRORE SILENZIOSO
Ogni azione che scrive va protetta

before_action -> { richiedi_permesso!(:azione) } nel controller, consentito?(:azione) nella policy al posto di user.present?. Senza, qualunque ruolo che entra in Support (operation, gestore_tecnici, logistica) può scrivere.

ERRORE A RUNTIME
Un'azione nuova va aggiunta alla matrice

Accessi::Permessi.consentito? solleva ArgumentError per un'azione che non è in AZIONI. Aggiungere l'azione, assegnarla ai ruoli e aggiornare la tabella in doc/dev/accessi.md.

ERRORE SILENZIOSO
Dare un'azione a un ruolo gli apre Support

Expert, amministrazione, commerciale e callcenter oggi non entrano perché non hanno azioni. Appena ne ricevono una entrano e vedono anche le aree ancora aperte (SMS, template, import sinistri, capacità hub, export). Prima di dare un'azione a un ruolo nuovo bisogna proteggere quelle aree.

ERRORE A RUNTIME
users.admin non esiste più per il modello

Si usa User#admin?, che vale ruolo == 'admin'. Mai leggere o scrivere la colonna.

ERRORE SILENZIOSO
Nessun utente esterno in Support

Hub, tecnici e periti non hanno account in Support (Q16): entreranno dal Campo tramite l'API.

Appuntamenti

Il modello più toccato da F1.

ERRORE SILENZIOSO
Ogni lettore parte da Appuntamento.programmati

Gli annullati restano nella tabella. Capacità hub, saturazione, candidati, remind, calendari, disponibilità del Self Appointment, /appuntamenti, analytics: una query nuova su Appuntamento senza lo scope conta anche gli annullati. Le associazioni che includono tutto si chiamano tutti_gli_appuntamenti*.

ERRORE A RUNTIME
L'id WP arriva dopo

appuntamenti.wp_post_original_id è nullo finché la proiezione non è riuscita, e appointment_confirmations.wp_post_id è nullable. Il codice nuovo non deve dare per scontato l'id WP.

ERRORE SILENZIOSO
Due rami finché c'è la feature

Fino a F2-07 ogni chiamante ha un ramo con agenda_comandi_locali attiva (comandi) e uno spenta (Actions::OnWp::*). Una funzionalità nuova che crea o modifica appuntamenti va testata in entrambi i casi. Il codice nuovo non chiama Actions::OnWp::CreateAppuntamento.

ERRORE SILENZIOSO
Annullare non è cancellare

Si usa AnnullaAppuntamento con un motivo di MOTIVI. Su WP il post sparisce del tutto, quindi una funzione che legge WP direttamente non lo troverà più.

ERRORE SILENZIOSO
Self Appointment: due strade fino a F2-04

Il job del Self Appointment scrive ancora su WP. AzioniCliente::SelfAppointment::Fissa aspetta la rilettura se l'appuntamento ha una proiezione in attesa; se la proiezione fallisce, aspetta il prossimo webhook o un "Riprova" dal cruscotto.

WordPress

WP resta vivo fino a F2-07, ma Support ci parla in un modo solo.

ERRORE SILENZIOSO
Il codice nuovo non scrive su WP

Il comando pubblica l'evento e LegacyWp fa il resto. Verso WP solo REST standard (wp/v2/<cpt> + ACF) con Clients::Wp: niente rotte custom, niente SQL, WP locale in sola lettura.

ERRORE A RUNTIME
Un kind nuovo da proiettare si aggiunge in quattro passi

Voce in LegacyWp::Registro::VOCI con il suo adattatore (campi_posseduti, acf, eventualmente acf_da_cache ed elimina?); eventi della famiglia dai comandi; test del registro verde; kind in config/legacy_wp/proprieta.yml se non è wp.

ERRORE SILENZIOSO
Sconosciuto ≠ vuoto

Nell'adattatore una chiave assente vuol dire "non toccare", nil vuol dire "svuota". Mandare vuoto un campo che Support non possiede cancella dati su WP.

ERRORE SILENZIOSO
Le modifiche da WP possono arrivare in ritardo

Finché un record ha una proiezione in_attesa, l'update da WP viene rinviato e riletto a proiezione finita. Se la proiezione è fallita vince WP e la modifica di Support si perde in locale: va guardato il cruscotto.

ERRORE SILENZIOSO
L'anti-eco dipende dall'utente 121

Support scrive su WP come utente 121 (config.x.legacy_wp.utente_sync_id) in ogni ambiente. Nessun id cablato: l'utente 13 è un amministratore umano. pratica resta kind wp e i suoi webhook non si scartano mai.

ERRORE SILENZIOSO
Le richieste lunghe verso WP pesano sulla coda

POST e PUT hanno un timeout di lettura di 120 s e girano su wp_write. Un job nuovo che scrive tanto su WP rallenta le proiezioni.

Database, configurazione, documentazione

Le regole di contorno che il rebase di F1 ha reso esplicite.

CI ROSSA
Migrazioni con la data reale, in coda a main

YYYYMMDDHHMM00 dopo l'ultima di db/migrate, Migration[8.1], PK uuid, check constraint sugli invarianti numerici. A ogni rebase di un epic le sue migrazioni si rinominano in coda e db/schema.rb si rigenera: il merge testuale dello schema è inaffidabile anche senza conflitti.

ERRORE A RUNTIME
Tabelle con prefisso e plurali italiani

Prefisso del pacchetto (pianificazione_, logistica_, rete_). Ogni nome italiano nuovo va in config/initializers/inflections.rb, altrimenti Rails cerca rete_contropartes.

CI ROSSA
Test e lingua

RSpec in TDD, factory in packs/<nome>/spec/factories/, client WP con instance_double(Clients::Wp), request spec al posto dei system spec. Italiano per il dominio e i commenti, inglese per i commit. I modelli esistenti non si rinominano.

ERRORE SILENZIOSO
Un dominio nuovo ha il suo documento

File in doc/dev/ e riga in doc/index.md e doc/mappa_progetto.md. Riferimenti per nome di metodo, non per numero di riga.

Da chiudere

Decisioni ancora aperte

Toccano chi lavora su tecnici, agenda e logistica. Non bloccano F1, ma vanno chiuse prima del passaggio di F2-07.

R2
Main ha già appuntamenti.tecnico_1_wp_id; F2 aggiunge tecnico_id. Due colonne o una ricavata dall'altra, e da quando i lettori passano alla nuova?
R3
Il segnaposto WP 1714 vuol dire "nessun tecnico": va escluso dall'import e mai scritto dall'adattatore. Proposta: sì.
R4
Gli adattatori F2 (tecnico, agenda, sostitutiva) senza acf_da_cache mandano tutti i campi alla prima proiezione. Va bene?
R6
messa_in_sicurezza è una perizia (piano, inspecta) o una lavorazione (main, TIPOLOGIE_PERIZIA)?
R7
La saturazione hub conta le sostitutive senza guardare interna; la capienza di Logistica conta solo le interne. Si allineano?
R8
Bacino della bisarca: le pratiche importanti in 3.1–3.6 e quelle con Fast Pass e Self Appointment già fissato vanno proposte?
D-F2-4
Operation può assegnare tecnici alle perizie, mentre inspecta riserva la plancia ad admin e gestore_tecnici.
Accessi
/appuntamenti e analytics/appuntamenti_lavorazione restano senza permessi perché sono in sola lettura. Da confermare.