Por que editar uma despesa deve refazer a divisão
Os padrões de uma edição são bugs de dinheiro, então o Dimesum torna participants e split_type obrigatórios em todo PATCH de despesa, junto do base_version que rejeita uma edição desatualizada em vez de mesclá-la.
Corrigir um erro de digitação não deveria mudar quem deve dinheiro. No Dimesum, editar uma despesa compartilhada refaz toda a divisão. participants e split_type são obrigatórios em todo PATCH, e uma edição que omite qualquer um deles volta com 400 em vez de preencher um padrão. Os padrões de criação são todos os membros atuais e uma divisão igual, que numa edição são bugs de dinheiro.
Duas falhas tornam o caso concreto. Um colega de apartamento que entrou em agosto é puxado para o jantar de julho, porque "todos os membros atuais" é avaliado quando a edição chega, não quando a despesa foi gravada. Uma divisão de aluguel deliberada de 70/30 achata para 50/50, porque um split_type ausente significa EQUAL. Nenhuma das falhas gera um erro, e ambas movem dinheiro de verdade.
Os padrões de uma edição são bugs de dinheiro. Um padrão do momento da criação faz suposições sobre o grupo que o autor está vendo agora. Uma edição chega depois, contra um grupo que mudou, e a mesma suposição reescreve silenciosamente o que as pessoas devem.
Os padrões de criação descrevem uma despesa nova, não uma antiga
No momento da criação, os padrões são honestos. O autor está vendo o grupo como ele está, e uma divisão igual entre todos é o caso comum, então o Dimesum preenche os dois. A despesa armazena suas entradas em vez de apenas seus resultados: splits.percent_bp, splits.weight e splits.exact_minor guardam o que o usuário digitou, então uma edição posterior pode reabri-la.
Uma edição é um ato diferente. A mesma despesa pode ser corrigida semanas depois, quando um novo colega de apartamento entrou ou um membro fantasma foi reivindicado. A composição do grupo é um alvo móvel e a divisão não. Reutilizar os padrões de criação pede que o grupo de hoje responda uma pergunta que a despesa antiga já respondeu.
A recusa em adivinhar não é novidade aqui. Uma despesa com vários pagadores também deve refazer seus pagadores. soleStoredPayer reutiliza o pagador armazenado apenas quando a despesa tem exatamente um, então uma edição que fica em silêncio sobre dois pagadores é recusada em vez de reatribuída. Uma edição que não diz nada sobre pagadores mantém o próprio pagador da despesa, nunca a pessoa que está editando.
A API recusa uma edição que não refaz sua divisão
A verificação do gateway roda antes de qualquer dinheiro ser calculado. Quando participants está vazio ou split_type está em branco, a requisição volta com 400 com o código invalid_expense e a mensagem "an edit must restate the split: participants and split_type are required". O serviço de despesas repete a regra em validateAmend, então um chamador que alcança o serviço por outro caminho encontra a mesma recusa.
| Campo | Na criação | Numa edição | O que o padrão custaria |
|---|---|---|---|
participants | Opcional. Assume todos os membros atuais | Obrigatório | Um membro que entrou depois entra numa despesa antiga |
split_type | Opcional. Assume EQUAL | Obrigatório | Uma divisão 70/30 achata para 50/50 |
payers | Opcional. Assume o autor | Omitido mantém o único pagador da despesa; dois pagadores devem ser refeitos | O editor vira o pagador, invertendo quem deve a quem |
base_version | Não enviado | Obrigatório, e deve ser igual à versão atual | Uma edição desatualizada sobrescreve uma mudança que seu autor nunca leu |
revision_id | UUIDv7 do cliente, a chave de idempotência | Igual, uma por versão | Uma edição repetida cobra do grupo duas vezes |
currency | Declarada por despesa | Deve corresponder; uma mudança é recusada | Uma linha de saldos guarda uma moeda por membro |
A moeda pertence à mesma família de recusas. Uma edição não pode redenominar uma despesa, porque uma linha de saldos guarda uma moeda por membro. O lado de escrita rejeita a mudança, e o livro-razão append-only estaciona tal alteração se alguma vez chegar a ele. Recusar na porta impede que as duas metades discordem.
Uma edição desatualizada é rejeitada para seu autor, nunca mesclada
base_version é a outra metade do contrato. Todo PATCH carrega a versão que seu autor leu, e checkTransition a compara com a linha que a transação acabou de bloquear. Igual, e a edição se aplica na versão mais um. Diferente, e o chamador recebe HTTP 409 com o código stale_version.
Mesclar é a alternativa tentadora, e está errada. Duas edições numa despesa são duas declarações completas do que a conta significa. Mesclá-las produz uma terceira declaração que ninguém escreveu, com parcelas que nenhum dos autores reconheceria. A rejeição devolve o conflito à única pessoa que pode resolvê-lo.
Idempotência e concorrência são mantidas separadas de propósito. A inserção da revisão roda antes da verificação de versão, porque uma edição repetida carrega a versão base que leu originalmente, que agora está desatualizada. Uma repetição deve ser lida como repetição e não como conflito, então revision_id responde primeiro e retorna o resultado armazenado.
A resposta já carrega o que a próxima edição precisa
Exigir mais campos num PATCH só é justo se um cliente puder obtê-los de forma barata. Toda resposta de despesa carrega version, então um cliente que acabou de gravar uma despesa pode editá-la sem uma segunda leitura. A resposta de criação, a resposta de alteração e cada linha de lista carregam o mesmo campo.
Para o cliente que não acabou de gravar a despesa, GET /v1/groups/{id}/expenses/{id} retorna a versão, as parcelas calculadas e as entradas por trás delas. As entradas voltam sob os mesmos nomes que um PATCH aceita: participants, split_type, percents, weights, shares, items, pools. Um cliente lê um formato e o envia de volta com edições, em vez de traduzir entre dois vocabulários para uma conta.
A simetria importa mais para divisões que não podem ser reconstruídas. Uma divisão PERCENT ou uma conta detalhada por item não pode ser refeita apenas a partir de suas parcelas resolvidas, porque o arredondamento já foi aplicado e os basis points e os itens de linha se foram. Os instantâneos de revisão também carregam as entradas, então a folha de histórico pode mostrar o que foi detalhado em qualquer versão passada.
Obrigatório agora, porque uma exigência não pode ser adicionada depois
Exigir um campo no primeiro dia é uma decisão sobre o futuro, não sobre hoje. Afrouxar um campo obrigatório depois é retrocompatível: os clientes já o enviam, e o servidor passa a aceitar requisições sem ele. Adicionar uma exigência depois quebra todo cliente que dependia do padrão antigo.
Então a direção é escolhida uma vez, cedo. O Dimesum exige participants, split_type e base_version numa edição enquanto o número de clientes ainda é pequeno o suficiente para mudar. Se um padrão seguro para edições um dia for encontrado, os campos se tornam opcionais e nada já entregue para de funcionar.
Faça uma edição refazer o que ela significa
Os padrões pertencem à criação, onde o autor pode ver o grupo com o qual está concordando. Numa edição, os mesmos padrões são uma suposição sobre um grupo que já mudou. Seu próximo passo: abra seu próprio endpoint de leitura e verifique que ele devolve as entradas de divisão sob os nomes de campo exatos que seu endpoint de escrita aceita. Um cliente que precisa traduzir entre os dois acabará traduzindo um deles errado.
Perguntas frequentes
Por que editar uma despesa compartilhada exige participants e split_type?
O Dimesum exige os dois campos porque os padrões de criação estão errados para uma edição. Na criação, o Dimesum assume todos os membros atuais, divididos igualmente, o que corresponde ao grupo que o autor está vendo. Uma edição pode chegar semanas depois, após alguém entrar. Reutilizar esses padrões traria um novo colega de apartamento para um jantar antigo e achataria uma divisão de aluguel deliberada de 70/30 de volta para 50/50, sem mostrar nenhum erro.
O que acontece quando duas pessoas editam a mesma despesa ao mesmo tempo?
A segunda edição é recusada com HTTP 409 e o código de erro stale_version. Todo PATCH carrega base_version, a versão que seu autor leu, e o caminho de alteração a compara com a linha bloqueada. Uma divergência significa que a despesa mudou, então a edição volta para seu autor reaplicá-la sobre a versão que ele agora pode ver. Nada é mesclado.
Editar uma despesa atualiza as linhas do livro-razão no lugar?
Não, editar uma despesa nunca atualiza uma linha do livro-razão no Dimesum. Uma edição registra dois lançamentos em uma transação: um EXPENSE_REVERSAL que anula a versão antiga linha por linha, depois um lançamento EXPENSE para a nova versão. UPDATE e DELETE são revogados do próprio papel de banco de dados do livro-razão, então uma mutação é impossível mesmo para código com bugs. Você vê um selo de editado e uma folha de histórico.
Preciso fazer uma segunda chamada de API antes de editar uma despesa compartilhada?
Não, um cliente que acabou de gravar a despesa já tem a versão de que uma edição precisa. Toda resposta de despesa carrega version, que é o que o próximo PATCH envia como base_version. Um cliente que não gravou a despesa chama GET /v1/groups/{id}/expenses/{id}, que retorna a versão mais as entradas de divisão sob os mesmos nomes de campo que um PATCH aceita.
Por que exigir participants e split_type agora em vez de adicioná-los depois?
Exigir um campo desde o primeiro dia é reversível, e adicioná-lo depois não é. Afrouxar um campo obrigatório depois é retrocompatível: os clientes já o enviam, e o servidor passa a aceitar requisições sem ele. Adicionar uma exigência depois quebra todo cliente que dependia do padrão antigo, e em uma API de dinheiro a quebra é silenciosa até o saldo de alguém estar errado.
Posts populares
- O livro-razão append-only que mantém saldos exatos9 min de leitura
- Seis bugs de dinheiro em despesas multimoeda9 min de leitura
- Acerto de contas: menos transferências no grupo5 min de leitura
- Como dividir uma conta detalhada de restaurante10 min de leitura
- Como dividir o aluguel de forma justa6 min de leitura