dimesum

Home / Blog / Denaro

Denaro

Perché modificare una spesa ridefinisce la divisione

· 9 min di lettura ·

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.

La posizione

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.

Due versioni della stessa modifica: i valori predefiniti ereditati cambiano ogni quota, mentre una divisione ridefinita cambia solo la descrizione LA MODIFICA EREDITA I VALORI PREDEFINITI LA MODIFICA RIDEFINISCE LA DIVISIONE Affitto, 20.000 euro, luglio Affitto, 20.000 euro, luglio v1 Asha 70%, 14.000 Bhavna 30%, 6.000 v1 Asha 70%, 14.000 Bhavna 30%, 6.000 Chetan entra nell'appartamento ad agosto. Chetan entra nell'appartamento ad agosto. PATCH corregge un refuso, non invia la divisione. PATCH corregge un refuso, invia la divisione: participants: Asha, Bhavna split_type: PERCENT 7000 / 3000 v2 Asha 6.666,67 Bhavna 6.666,67 Chetan 6.666,66 v2 Asha 70%, 14.000 Bhavna 30%, 6.000 Chetan deve un mese in cui non ha mai abitato. È cambiata solo la descrizione.
La stessa modifica di un carattere, due volte. Ereditare i valori predefiniti alla creazione ridivide 20.000 euro in tre parti e addebita un membro entrato un mese dopo; ridefinire la divisione lascia ogni quota intatta.

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.

Ciò che invia una creazione rispetto a ciò che una modifica deve ridefinire, su POST e PATCH /v1/groups/{id}/expenses.
CampoAlla creazioneIn una modificaCosa costerebbe il valore predefinito
participantsOpzionale. Predefinito a tutti i membri attualiObbligatorioUn membro entrato in seguito si unisce a una vecchia spesa
split_typeOpzionale. Predefinito a EQUALObbligatorioUna divisione 70/30 si appiattisce a 50/50
payersOpzionale. Predefinito all'autoreSe omesso mantiene l'unico pagatore della spesa; due pagatori devono essere ridefinitiChi modifica diventa il pagatore, invertendo chi deve a chi
base_versionNon inviatoObbligatorio, e deve essere uguale alla versione attualeUna modifica obsoleta sovrascrive una modifica che il suo autore non ha mai letto
revision_idUUIDv7 del client, la chiave di idempotenzaUguale, uno per versioneUna modifica ritentata addebita il gruppo due volte
currencyIndicata per spesaDeve corrispondere; una modifica viene rifiutataUna 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.

Due autori leggono la versione 3; la prima modifica registra la versione 4 e la seconda viene rifiutata con 409 stale_version spesa, versione 3 Autore A PATCH base_version: 3 200 OK, versione 4 Autore B PATCH base_version: 3 409 stale_version Registro, una transazione: EXPENSE_REVERSAL di v3 EXPENSE v4 L'autore B rilegge la versione 4, riapplica la modifica, invia base_version: 4
Concorrenza ottimistica su una spesa. La modifica perdente viene restituita al suo autore con la versione su cui deve essere ricostruita. La modifica vincente raggiunge il registro come uno storno più una nuova registrazione, mai un aggiornamento.

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 risposta GET e la richiesta PATCH usano gli stessi nomi di campo, con version rinominato in base_version GET RESTITUISCE PATCH ACCETTA version base_version participants participants split_type split_type percents, weights, shares percents, weights, shares items, pools items, pools rinominato
Un solo vocabolario, due direzioni. Solo il campo version cambia nome nel viaggio di andata e ritorno, così un client di modifica non mantiene mai un livello di traduzione tra ciò che legge e ciò che scrive.

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.