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.
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.
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.
| Tema | Prima di F1 | Dopo F1 |
|---|---|---|
| Struttura del codice | Tutto 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 permessi | Solo 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 appuntamento | Actions::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 appuntamento | Cestino 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 cosa | Audit 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. |
| Eventi | Solo 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 WP | Ogni 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 WP | Clients::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>. |
| Monitoraggio | Nessuna vista sulle scritture verso WP. | /admin/legacy_wp/proiezioni (stati, errori, "Riprova") e /admin/legacy_wp/riconciliazione. |
packs/<nome>/app/public/: comandi, query, errori, catalogo eventi. Il resto è privato, anche per la radice.packs/legacy_wp.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.richiedi_permesso!(:azione) nei controller, consentito?(:azione) in viste e policy.legacy_wp:importa_utenti), inviti solo ad admin e operation, abbinamento utenti ↔ operatori.users.admin è in ignored_columns; il drop è in un rilascio successivo.legacy_wp_proiezioni, ProiettaJob sulla coda wp_write.FissaAppuntamento, ModificaAppuntamento, AnnullaAppuntamento con MOTIVI.stato, annullato_il, annullato_da_id, motivo_annullamento; FK su hub.#ID, token di conferma, cancellazione definitiva.flowchart TD A[Controller o job] --> B[Actions::OnWp::CreateAppuntamento] B -->|REST| W[(WordPress)] W -->|webhook| P[PullWpPostJob] P --> H[AppuntamentoHandler] H --> L[(appuntamenti)]
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).
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.
Vale per ogni file nuovo.
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.
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.
packs/legacy_wpUn contesto non conosce WP. Non usare bin/packwerk update-todo per nascondere una violazione nuova: serve solo al codice storico.
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.
::Dentro un modulo annidato si scrive ::Eventi.pubblica. Dove convivono Query::X e un modello X si usa il nome completo (::Logistica::Query::Capienza).
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.
Comandi, eventi e attore.
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.
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.
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.
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.
MioJob.con_attore(attore).perform_later(...). Senza attore esplicito il job registra job:<NomeJob>, e l'attore implicito non passa ai job figli.
Dominio::RegolaViolataCodice simbolico stabile, messaggio in it.dominio.regole.<codice> dentro config/locales/dominio.it.yml, forzabile: true se un operatore autorizzato può passare oltre.
Prima bastava avere una sessione.
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.
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.
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.
users.admin non esiste più per il modelloSi usa User#admin?, che vale ruolo == 'admin'. Mai leggere o scrivere la colonna.
Hub, tecnici e periti non hanno account in Support (Q16): entreranno dal Campo tramite l'API.
Il modello più toccato da F1.
Appuntamento.programmatiGli 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*.
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.
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.
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ù.
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.
WP resta vivo fino a F2-07, ma Support ci parla in un modo solo.
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.
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.
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.
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.
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.
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.
Le regole di contorno che il rebase di F1 ha reso esplicite.
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.
Prefisso del pacchetto (pianificazione_, logistica_, rete_). Ogni nome italiano nuovo va in config/initializers/inflections.rb, altrimenti Rails cerca rete_contropartes.
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.
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.
Toccano chi lavora su tecnici, agenda e logistica. Non bloccano F1, ma vanno chiuse prima del passaggio di F2-07.
appuntamenti.tecnico_1_wp_id; F2 aggiunge tecnico_id. Due colonne o una ricavata dall'altra, e da quando i lettori passano alla nuova?acf_da_cache mandano tutti i campi alla prima proiezione. Va bene?messa_in_sicurezza è una perizia (piano, inspecta) o una lavorazione (main, TIPOLOGIE_PERIZIA)?interna; la capienza di Logistica conta solo le interne. Si allineano?/appuntamenti e analytics/appuntamenti_lavorazione restano senza permessi perché sono in sola lettura. Da confermare.