La guida riluttante alle migrazioni Shopify

Atto IV: L'incastro

Giù le mani dal checkout

Il team ci aveva passato sopra due sprint, e questo ti dice che la consideravano una cosa importante, perché nessuno passa due sprint su qualcosa che ritiene irrilevante. Il brand vendeva regali—di quelli che mandi a qualcun altro, in una data che scegli tu, con un biglietto—e il requisito era semplice da pronunciare ad alta voce: un cliente che comprava un regalo doveva scegliere una data di consegna, e l’ordine non poteva andare a buon fine senza. Così gli ingegneri costruirono una Checkout UI Extension che mostrava un date picker, e la collegarono in modo da bloccare l’ordine finché il campo non aveva un valore. block_progress, dichiarato. L’acquirente non poteva andare avanti senza scegliere un giorno. La testarono come si testano le cose di cui si va fieri, cioè a fondo. La QA passò. Andò in produzione.

Nel giro di una settimana, arrivavano ordini senza nessuna data di consegna.

Non tanti, all’inizio, ed è questa la parte crudele, perché un rivolo sembra un caso limite e un caso limite sembra qualcosa di cui ti occuperai. Ma il rivolo aveva una forma. Gli ordini senza data erano quasi tutti da mobile. Erano quasi tutti pagati con Apple Pay. E quando finalmente qualcuno si sedette e ne tracciò uno fino in fondo, la spiegazione fu insieme del tutto banale e leggermente agghiacciante: gli acquirenti che pagavano con Apple Pay non avevano mai visto il date picker. Non visto-e-saltato. Mai renderizzato. La validazione che il team aveva costruito—la cosa su cui erano andati due sprint, la cosa che bloccava l’avanzamento, la cosa su cui la QA aveva dato l’ok—era completamente invisibile al flusso del wallet. Girava in una parte del checkout che gli acquirenti che convertivano di più sul sito semplicemente non visitavano mai.

Nessuno aveva fatto niente di sbagliato. È questa la frase su cui fermarsi a riflettere. Il requisito era ragionevole. Il codice era corretto. I test erano onesti. E la feature andò comunque in errore in produzione proprio per i clienti per cui era più importante che funzionasse, perché il team l’aveva costruita su un’assunzione che era vera sulla loro vecchia piattaforma e silenziosamente, catastroficamente falsa su questa.


Ecco l’assunzione, detta chiaramente, perché nominarla è quasi tutta la cura. Sulle piattaforme da cui questi team arrivano—Magento, Salesforce Commerce Cloud, uno stack su misura che qualcuno cura da un decennio—il checkout è un posto dove gira il tuo codice. È tuo. È un punto di estensione universale: una serie di controller e template e hook dove puoi calcolare quello che vuoi, chiamare quello che vuoi, bloccare quello che vuoi, nella richiesta dal vivo, davanti all’acquirente, ogni volta. Dieci anni di sviluppo su una piattaforma del genere cablano una convinzione dentro un team così in profondità che smettono di accorgersi che è una convinzione: se ho bisogno che qualcosa succeda al checkout, lo faccio succedere al checkout. Quella convinzione è stata corretta per un decennio. Si è ripagata cento volte. Ed è la singola cosa più costosa che un team enterprise si porta dietro su Shopify, perché su Shopify è sbagliata.

Su Shopify, il checkout non è un posto dove gira il tuo codice. È un posto dove i dati vengono mostrati.

Questo è l’intero capitolo, e potresti smettere di leggere qui se la frase ti è arrivata abbastanza forte, ma raramente succede al primo passaggio, perché suona come un limite anziché come una descrizione, e l’istinto è di trattare i limiti come cose da aggirare. Allora lasciami disegnare la forma vera della cosa, perché la forma è più utile dello slogan, ed è la forma a dirti dove al tuo codice è concesso vivere.

Pensa al viaggio dell’acquirente attraverso il tuo store come fatto di tre superfici, e pensale in ordine. C’è un prima, c’è un mezzo e c’è un dopo, e quasi tutto quello che va storto in una build di checkout enterprise è logica che apparterrebbe al prima o al dopo, spinta a forza nel mezzo.

Il prima è tutto quello che precede il checkout: il carrello, lo storefront, le pagine prodotto e—andando ancora più indietro—i sistemi che li alimentano, l’ERP e l’OMS e il PIM e il pricing engine e qualunque altra cosa stia nel back office a decidere cosa è vero della tua attività. È qui che vive la business logic. È qui che calcoli. Se un prezzo dipende dal tier contrattuale del cliente, il tier contrattuale viene deciso qui. Se uno sconto dipende dal contenuto del carrello e dalla fase lunare e dal lifetime value del cliente, quella decisione viene presa qui. Il prima è tuo, completamente, come una volta era tuo il vecchio checkout.

Sezione di tre stanze adiacenti: a sinistra un laboratorio affollato, al centro una sala espositiva spoglia con una sola finestra, a destra una sala di reazione. Una figura piccola sta sulla soglia tra il laboratorio e la sala espositiva.
Il prima calcola. Il mezzo mostra. Il dopo reagisce. La figura sta decidendo in quale stanza vive davvero il suo codice.

Il mezzo è il checkout vero e proprio, e il mezzo non è tuo. È di Shopify. È in sandbox—il tuo codice gira in un ambiente vincolato, con un performance budget e un perimetro di sicurezza, non nel campo aperto che ti dà il prima. È auditabile, nel senso che un security team può rendere conto di ogni pezzo di codice che viene eseguito nella sessione di pagamento di un acquirente, e questa è una feature, non una scocciatura, anche se ti sembrerà una scocciatura la prima volta che ti ferma. Nel mezzo, le cose vengono mostrate. Il prezzo calcolato nel prima viene visualizzato. Lo sconto deciso a monte viene renderizzato. La data di consegna che l’acquirente deve scegliere viene presentata come un campo. Il mezzo è una finestra, non un laboratorio.

Il dopo è tutto quello che succede una volta che l’ordine esiste: Shopify Flow, webhook, app action, tutta la macchina delle reazioni. L’ordine è stato effettuato; ora il magazzino viene avvisato, l’ERP riceve il record, i punti loyalty vengono assegnati, il biglietto regalo viene instradato al sistema di fulfillment. È qui che vivono le reazioni. Se ti accorgi di voler fare qualcosa come conseguenza di un acquisto, il dopo è quasi sempre il posto a cui appartiene quel qualcosa, e quasi mai il mezzo.

Calcola a monte, renderizza a valle. Dillo due volte, perché il resto di questo capitolo non è che quella frase applicata a errori specifici. Calcola a monte, renderizza a valle. Il prima calcola. Il mezzo renderizza. Il dopo reagisce. Quando una build di checkout va storta, è quasi sempre perché qualcuno ha provato a far fare al mezzo il lavoro di uno degli altri due.


Ci sono due modi in cui quell’errore si manifesta, e vale la pena nominarli singolarmente, perché dall’interno sembrano diversi anche se sono lo stesso errore.

Il primo è duplicare una primitiva che Shopify ti dà già. La versione canonica di questo, quella che abbiamo visto costruire con grande cura a più di un team, è la Cart Validation Function che chiama il magazzino per la giacenza dal vivo al momento del checkout. Il ragionamento è a prova di bomba, se concedi la premessa. Non possiamo vendere sotto giacenza. L’overselling è un incubo per il customer service e un mal di testa per la finanza. Quindi nel momento della verità—il checkout—contatteremo il WMS, gli chiederemo se l’articolo è davvero, veramente disponibile in questo momento, e bloccheremo l’ordine se non lo è. Ogni parola di questo ragionamento è sensata. Ed è memoria muscolare Magento nella sua forma più pura, perché assume che i numeri di inventario della piattaforma stessa non siano affidabili e che l’unica fonte di verità sicura sia il magazzino, interrogato dal vivo, nella sessione dell’acquirente.

Ma Shopify ha già un inventario. Tiene già traccia delle giacenze, le decrementa già, previene già l’overselling quando glielo lasci fare. Il motivo per cui il team non si fida di quei numeri non è che i numeri siano inaffidabili—è che il sync tra Shopify e il magazzino è inaffidabile, e invece di sistemare il sync, il team ha costruito un controllo di giacenza dal vivo al checkout per tamponare il problema. Adesso ci sono due fonti di verità, una fragile chiamata in tempo reale piazzata nel percorso critico di ogni acquisto, e un nuovo modo di fallire in cui il WMS è lento o giù e il checkout rallenta o si rompe con lui. La soluzione non è mai stata una Function. La soluzione era rendere affidabile il sync a monte, nel prima, così che si potesse credere all’inventario di Shopify stesso—che è l’unico posto in cui il problema poteva davvero essere risolto, perché è l’unico posto in cui il problema davvero stava.

Un tubo da giardino rattoppato e bucato porta a un secchio forato messo sotto un rubinetto che gocciola. Un annaffiatoio perfettamente funzionante e mai usato sta lì accanto.
Il rattoppo sul tubo non è la soluzione. La soluzione è il tubo. Ma il secchio sembrava un pomeriggio più veloce.

Il secondo modo in cui l’errore si manifesta è calcolare dentro il checkout—fare i conti nel mezzo invece che a monte. Una regola di pricing quasi esprimibile con gli strumenti di sconto nativi di Shopify, così un team allunga le mani dentro il checkout per finire il lavoro in codice. Una sfumatura fiscale, un calcolo di bundle, un aggiustamento specifico di contratto, calcolato dal vivo in una extension perché lì sembrava naturale metterlo. La disciplina qui è esatta ed è la stessa ogni volta: decidi la cosa nel sistema di record, spingi il risultato dentro Shopify come metafield o line item property o attributo di draft order, e lascia che il checkout renderizzi il risultato che gli è stato passato. Il pricing engine decide il prezzo; Shopify mostra il prezzo. L’ERP decide lo sconto; Shopify mostra lo sconto. Nessuno calcola nel mezzo. Il mezzo non è mai stato costruito per calcolare. È stato costruito per mostrare.


Ora, prima che l’engineering lead che legge questo alle undici di sera—quello che ha una Cart Validation Function scritta a metà in un’altra tab, quello che già sa che il suo sync del WMS sta in piedi grazie a un cron job e una preghiera—prima che tu chiuda questo libro e vada a sistemare il sync, una precisazione onesta, perché la regola ha un’eccezione vera e fingere il contrario farebbe di me un bugiardo.

Le Shopify Functions esistono, e sono buone, e dovresti usarle. L’eccezione è questa: le Functions sono per la logica a forma di Shopify. Combinazioni di sconti. Filtraggio dei metodi di spedizione. Visibilità dei metodi di pagamento. Le decisioni che riguardano davvero come si comporta il checkout di Shopify stesso e che non hanno nessun altro posto sensato in cui vivere—quelle appartengono alle Functions, e Shopify ha costruito le Functions proprio perché avessero un posto in cui vivere. L’errore non è usare le Functions. L’errore è piegarle in un motore di regole di business generico—usare una Function per contattare il tuo WMS, per chiamare il tuo servizio di pricing, per far girare logica che riguarda la tua attività anziché il checkout di Shopify. Una Function che filtra quali metodi di spedizione compaiono usa lo strumento come è stato progettato. Una Function che telefona al magazzino è lo strumento che urla. Impara la differenza, perché la differenza è tutta la disciplina, e la piattaforma non te la insegnerà—ti lascerà semplicemente costruire quella sbagliata e passare la QA.


Il che ci riporta al regalo, e alla parte di tutto questo che i team genuinamente non vedono arrivare, perché è invisibile esattamente nel modo in cui era invisibile la validazione fallita. Il viaggio dell’acquirente attraverso il checkout non è un solo percorso. Sono diversi, e non fanno girare tutti lo stesso codice.

Il percorso contro cui il team costruisce—quello nella demo, quello nella QA, quello nella testa di tutti quando dicono “il checkout”—è il flusso a pagina intera, quello in cui l’acquirente arriva su una pagina di checkout e la tua UI Extension si renderizza e il tuo date picker compare e il tuo block_progress ha la possibilità di fare il suo lavoro. Ma quello non è il percorso che prendono i soldi. I soldi prendono sempre più gli Accelerated Checkout: Apple Pay, Shop Pay, i flussi wallet a un tocco che convertono meglio di qualsiasi altra cosa tu abbia proprio perché saltano quasi tutto. Saltano le tue UI Extension. Non le renderizzano—non “le renderizzano e le ignorano”, non le renderizzano, per niente. Nessun block_progress viene mai controllato, perché il blocco era attaccato a un campo che non è mai stato disegnato. L’acquirente che pagava con Apple Pay è andato dal carrello alla conferma senza che il tuo codice avesse il suo turno, e la tua validazione, che vive nel layer UI, semplicemente non faceva parte della conversazione.

Queste sono le cuciture invisibili del ciclo di vita del checkout, e i team le trovano in produzione, che è il posto peggiore possibile per trovarle, perché la produzione è il posto dove gli ordini senza data di consegna si stanno già accumulando. La cucitura non è un bug. È il design. I flussi wallet sono veloci perché sono essenziali, e sono essenziali perché si rifiutano di far girare UI arbitraria nel percorso dell’acquirente.

Quindi dove va la validazione? Va giù di un layer, fino ai dati. Il requisito della data di consegna deve essere imposto da una Cart Validation Function—che gira a prescindere da quale percorso d’acquisto venga preso, perché vive al data layer che tutti loro attraversano, wallet o meno. La UI Extension renderizza ancora il date picker, perché l’acquirente ha comunque bisogno di un modo per scegliere la data, e di un posto gradevole in cui farlo. Ma il picker non è più la cosa che impone. La Function impone. Il picker offre.

La Function è il contratto; la UI Extension è l’affordance. Dilla due volte anche questa, perché è la frase che avrebbe risparmiato due sprint. Il contratto—la regola che deve valere, non importa come arriva l’acquirente—vive nella Function, al data layer, dove ogni percorso ne è vincolato. L’affordance—il modo amichevole e visibile in cui l’acquirente soddisfa la regola—vive nella UI Extension, dove gli acquirenti che la vedono ne sono aiutati. Costruisci solo l’affordance, e i flussi wallet aggirano la tua regola. Costruisci solo il contratto, e gli acquirenti che vedono davvero una UI ricevono un rifiuto secco senza nessun modo di adeguarsi. Ti servono entrambi, e devi sapere quale è quale, e il team che ha perso due sprint non conosceva nessuno dei due, perché sulla loro vecchia piattaforma la distinzione non esisteva—la UI e la regola erano lo stesso codice nella stessa richiesta, e non avevano mai dovuto chiedersi nemmeno una volta dove viva davvero una regola.

Un pesante lucchetto chiuso e un'elegante maniglia da porta stanno fianco a fianco su una superficie piatta, separati da un piccolo spazio. Nessuno dei due è attaccato a una porta.
Il lucchetto impone la regola. La maniglia ti mostra dove spingere. Costruiscine solo uno, e hai o un mistero o una bugia.

Sarebbe facile leggere tutto questo come Shopify che ti toglie qualcosa, e provarci sopra quello che provi per qualsiasi capacità che avevi e che non hai più, cioè di essere stato derubato. Resisti, perché è una lettura sbagliata dello scambio. I vincoli sono il prezzo del biglietto, e il biglietto compra qualcosa di reale. Il mezzo è in sandbox e con un performance budget e auditato per la sicurezza perché è questo che permette a Shop Pay di convertire come fa, che permette a un acquirente di fidarsi di un acquisto a un tocco, che permette al tuo stesso security team di rendere conto di ogni riga di codice eseguita in una sessione di pagamento senza dover auditare un decennio di personalizzazioni del checkout sedimentate che nessuno ricorda del tutto di aver scritto. Le vecchie piattaforme ti lasciavano far girare qualsiasi cosa nel checkout, e il conto di quella libertà arrivava sotto forma di fragilità, di onere di audit, di lento accumulo di codice nella parte più sensibile del funnel che nessuno aveva il coraggio di toccare. Shopify ha preso la libertà e ha restituito le garanzie. Se sia un buono scambio dipende da cosa ci facevi con la libertà, e la maggior parte dei team enterprise, a essere onesti, la usava per tamponare un sync inaffidabile.

È la stessa forma di tanto altro nella piattaforma, e ormai hai visto la forma abbastanza volte da riconoscerla a colpo d’occhio: la scorciatoia sembra sempre piccola nel planning doc, e raramente resta piccola. Il controllo di giacenza dal vivo è una sola Function, nel planning doc. In produzione è una dipendenza fissa nel percorso critico di ogni ordine. La validazione nel layer UI è una sola extension, nel planning doc. In produzione è due sprint e una settimana di ordini misteriosi e un acquirente che paga prezzo pieno per un regalo che arriverà quando il magazzino ci penserà, perché nessuno ha detto al magazzino quando.

I team che fanno bene il replatform non sono quelli che personalizzano il checkout con astuzia. Sono quelli che smettono di chiedersi come personalizzo il checkout e cominciano a chiedersi dove in questo ciclo di vita appartiene questa personalizzazione—prima, mezzo o dopo—e poi la mettono lì, e solo lì. Il prima calcola. Il mezzo mostra. Il dopo reagisce. La Function tiene il contratto; la extension offre l’affordance. E l’assunzione che ti sei portato dietro dalla vecchia piattaforma—che il checkout sia tuo, da riempire di codice—non sopravvive al contatto con questa, ed è per questo che il lavoro non è migrare il codice. Il lavoro è migrare l’assunzione. Migra le assunzioni, non solo il codice, perché il codice si può riscrivere in un pomeriggio e l’assunzione ti costerà due sprint e un incidente in produzione prima ancora che ti accorga di portartela ancora dietro.

All’acquirente che paga con Apple Pay devi la stessa regola di tutti gli altri. Non vedrà il tuo date picker. Assicurati che sia comunque vincolato dal tuo contratto.

Qui giace la validazione del date picker. Due sprint. Due layer. block_progress, dichiarato. Il flusso wallet non ne ha mai visto niente.