Perché modificare una spesa ridefinisce la divisione
In Dimesum una modifica di una spesa condivisa deve ridefinire l'intera divisione, perché i valori predefiniti della creazione, riutilizzati in una modifica, spostano soldi in silenzio.
Correggere un refuso non dovrebbe cambiare chi deve dei soldi. In Dimesum, modificare una spesa condivisa ridefinisce l'intera divisione. participants e split_type sono obbligatori in ogni PATCH, e una modifica che ne omette uno torna con 400 invece di applicare un valore predefinito. I valori predefiniti alla creazione sono tutti i membri attuali e una divisione in parti uguali, che in una modifica sono errori sui soldi.
Due errori rendono il caso concreto. Un coinquilino entrato ad agosto viene incluso nella cena di luglio, perché "tutti i membri attuali" viene valutato quando la modifica arriva, non quando la spesa è stata scritta. Una divisione dell'affitto voluta 70/30 si appiattisce a 50/50, perché un split_type assente significa EQUAL. Nessuno dei due errori solleva un'eccezione, ed entrambi spostano soldi veri.
I valori predefiniti di una modifica sono errori sui soldi. Un valore predefinito al momento della creazione tira a indovinare sul gruppo che l'autore sta guardando in questo momento. Una modifica arriva dopo, contro un gruppo che è cambiato, e la stessa ipotesi riscrive in silenzio ciò che le persone devono.
I valori predefiniti alla creazione descrivono una spesa nuova, non una vecchia
Al momento della creazione i valori predefiniti sono onesti. L'autore sta guardando il gruppo così com'è, e una divisione in parti uguali tra tutti è il caso comune, quindi Dimesum compila entrambi. La spesa memorizza i suoi input invece dei soli risultati: splits.percent_bp, splits.weight e splits.exact_minor conservano ciò che l'utente ha digitato, così una modifica successiva può riaprirla.
Una modifica è un atto diverso. La stessa spesa può essere corretta settimane dopo, una volta che un nuovo coinquilino è entrato o un membro fantasma è stato reclamato. L'appartenenza al gruppo è un bersaglio mobile, la divisione no. Riutilizzare i valori predefiniti alla creazione chiede al gruppo di oggi di rispondere a una domanda a cui la vecchia spesa ha già risposto.
Il rifiuto di indovinare non è una novità qui. Anche una spesa con più pagatori deve ridefinire i suoi pagatori. soleStoredPayer riutilizza il pagatore memorizzato solo quando la spesa ne ha esattamente uno, così una modifica che tace su due pagatori viene rifiutata invece di essere riassegnata. Una modifica che non dice nulla sui pagatori mantiene il pagatore della spesa stessa, mai la persona che sta modificando.
L'API rifiuta una modifica che non ridefinisce la sua divisione
Il controllo del gateway viene eseguito prima che venga calcolato qualsiasi importo. Quando participants è vuoto o split_type è assente, la richiesta torna con 400 con il codice invalid_expense e il messaggio "an edit must restate the split: participants and split_type are required". Il servizio delle spese ripete la regola in validateAmend, così un chiamante che raggiunge il servizio in un altro modo incontra lo stesso rifiuto.
| Campo | Alla creazione | In una modifica | Cosa costerebbe il valore predefinito |
|---|---|---|---|
participants | Opzionale. Predefinito a tutti i membri attuali | Obbligatorio | Un membro entrato in seguito si unisce a una vecchia spesa |
split_type | Opzionale. Predefinito a EQUAL | Obbligatorio | Una divisione 70/30 si appiattisce a 50/50 |
payers | Opzionale. Predefinito all'autore | Se omesso mantiene l'unico pagatore della spesa; due pagatori devono essere ridefiniti | Chi modifica diventa il pagatore, invertendo chi deve a chi |
base_version | Non inviato | Obbligatorio, e deve essere uguale alla versione attuale | Una modifica obsoleta sovrascrive una modifica che il suo autore non ha mai letto |
revision_id | UUIDv7 del client, la chiave di idempotenza | Uguale, uno per versione | Una modifica ritentata addebita il gruppo due volte |
currency | Indicata per spesa | Deve corrispondere; una modifica viene rifiutata | Una riga dei saldi contiene una sola valuta per membro |
La valuta appartiene alla stessa famiglia di rifiuti. Una modifica non può ridenominare una spesa, perché una riga dei saldi contiene una sola valuta per membro. Il lato di scrittura rifiuta la modifica, e il registro append-only mette da parte una tale rettifica se mai dovesse raggiungerlo. Rifiutare alla porta impedisce alle due metà di essere in disaccordo.
Una modifica obsoleta viene rifiutata per il suo autore, mai unita
base_version è l'altra metà del contratto. Ogni PATCH porta la versione che il suo autore ha letto, e checkTransition la confronta con la riga che la transazione ha appena bloccato. Se sono uguali, la modifica viene applicata alla versione più uno. Se sono diverse, il chiamante riceve HTTP 409 con il codice stale_version.
L'unione è l'alternativa allettante, ed è sbagliata. Due modifiche a una spesa sono due dichiarazioni complete di ciò che il conto significa. Unirle produce una terza dichiarazione che nessuno ha scritto, con quote che nessuno dei due autori riconoscerebbe. Il rifiuto restituisce il conflitto all'unica persona che può risolverlo.
Idempotenza e concorrenza sono tenute separate di proposito. L'inserimento della revisione viene eseguito prima del controllo di versione, perché una modifica ritentata porta la versione base che aveva letto in origine, che ora è obsoleta. Una riproposizione deve essere letta come una riproposizione e non come un conflitto, quindi revision_id risponde per primo e restituisce il risultato memorizzato.
La risposta contiene già ciò di cui la modifica successiva ha bisogno
Richiedere più campi in un PATCH è giusto solo se un client può ottenerli a basso costo. Ogni risposta di spesa contiene version, così un client che ha appena scritto una spesa può modificarla senza una seconda lettura. La risposta di creazione, la risposta di modifica e ogni riga di elenco contengono lo stesso campo.
Per il client che non ha appena scritto la spesa, GET /v1/groups/{id}/expenses/{id} restituisce la versione, le quote calcolate e gli input che le determinano. Gli input tornano con gli stessi nomi che un PATCH accetta: participants, split_type, percents, weights, shares, items, pools. Un client legge un'unica forma e la reinvia con le modifiche, invece di tradurre tra due vocabolari per un solo conto.
La simmetria conta di più per le divisioni che non possono essere ricostruite. Una divisione PERCENT o un conto suddiviso per voci non può essere ridefinito dalle sole quote risolte, perché l'arrotondamento è già stato applicato e i basis point e le singole voci sono spariti. Gli snapshot di revisione portano anche gli input, così il foglio della cronologia può mostrare cosa era suddiviso per voci in qualsiasi versione passata.
Obbligatorio ora, perché un requisito non può essere aggiunto dopo
Richiedere un campo fin dal primo giorno è una decisione sul futuro più che sull'oggi. Allentare un campo obbligatorio in seguito è retrocompatibile: i client lo inviano già, e il server inizia ad accettare richieste che ne sono prive. Aggiungere un requisito in seguito rompe ogni client che si affidava al vecchio valore predefinito.
Quindi la direzione viene scelta una volta sola, presto. Dimesum richiede participants, split_type e base_version in una modifica mentre il numero di client è ancora abbastanza piccolo da poter cambiare. Se mai si troverà un valore predefinito sicuro per le modifiche, i campi diventeranno opzionali e nulla di ciò che è già stato rilasciato smetterà di funzionare.
Fai in modo che una modifica ridefinisca ciò che significa
I valori predefiniti appartengono alla creazione, dove l'autore può vedere il gruppo a cui sta acconsentendo. In una modifica gli stessi valori predefiniti sono un'ipotesi su un gruppo che nel frattempo è cambiato. Il tuo prossimo passo: apri il tuo endpoint di lettura e verifica che restituisca gli input della divisione con gli stessi identici nomi di campo che il tuo endpoint di scrittura accetta. Un client che deve tradurre tra i due prima o poi ne tradurrà uno in modo sbagliato.
Domande frequenti
Perché per modificare una spesa condivisa servono participants e split_type?
Dimesum richiede entrambi i campi perché i valori predefiniti della creazione sono sbagliati per una modifica. Alla creazione, Dimesum imposta come predefiniti tutti i membri attuali, con divisione in parti uguali, il che corrisponde al gruppo che l'autore sta guardando. Una modifica può arrivare settimane dopo, dopo che qualcuno è entrato. Riutilizzare quei valori predefiniti includerebbe un nuovo coinquilino in una vecchia cena e riporterebbe una divisione dell'affitto voluta 70/30 a 50/50, senza mostrare alcun errore.
Cosa succede se due persone modificano la stessa spesa contemporaneamente?
La seconda modifica viene rifiutata con HTTP 409 e il codice di errore stale_version. Ogni PATCH porta base_version, la versione che il suo autore ha letto, e il percorso di modifica la confronta con la riga bloccata. Una discrepanza significa che la spesa è cambiata, quindi la modifica torna al suo autore perché la riapplichi sulla versione che ora può vedere. Nulla viene unito.
Modificare una spesa aggiorna le righe del registro?
No, modificare una spesa non aggiorna mai una riga del registro in Dimesum. Una modifica registra due scritture in un'unica transazione: un EXPENSE_REVERSAL che annulla la vecchia versione voce per voce, poi una scrittura EXPENSE per la nuova versione. UPDATE e DELETE sono revocati dal ruolo di database del registro stesso, così una mutazione è impossibile anche per codice difettoso. Vedi un contrassegno di modifica e un foglio della cronologia.
Serve una seconda chiamata API prima di modificare una spesa condivisa?
No, un client che ha appena scritto la spesa possiede già la versione di cui una modifica ha bisogno. Ogni risposta di spesa contiene version, che è ciò che il PATCH successivo invia come base_version. Un client che non ha scritto la spesa chiama GET /v1/groups/{id}/expenses/{id}, che restituisce la versione più gli input della divisione con gli stessi nomi di campo che un PATCH accetta.
Perché rendere obbligatori participants e split_type ora invece di aggiungerli dopo?
Rendere obbligatorio un campo fin dal primo giorno è reversibile, aggiungerne uno dopo no. Allentare un campo obbligatorio in seguito è retrocompatibile: i client lo inviano già, e il server inizia ad accettare richieste che ne sono prive. Aggiungere un requisito in seguito rompe ogni client che si affidava al vecchio valore predefinito, e in un'API sui soldi il guasto è silenzioso finché il saldo di qualcuno non è sbagliato.
Articoli più letti
- Il registro append-only per spese condivise esatte9 min di lettura
- Sei bug di denaro nelle spese multivaluta9 min di lettura
- Saldare le spese di gruppo in pochi bonifici5 min di lettura
- Come dividere un conto con un piatto non condiviso9 min di lettura
- Come dividere l'affitto tra coinquilini in modo equo6 min di lettura