Blogue ·
Quinze coisas que uma migração de faturação não consegue levar
Uma migração de faturação não é uma cópia de dados. Quinze omissões medidas, o dinheiro em cêntimos inteiros e a impressão como única chave utilizável.
Por Provibr
Mudar um negócio de alojamento de um painel de faturação para outro parece uma cópia de dados e não é. Dois painéis que sabem ambos o que é um cliente, um serviço e uma fatura continuam a discordar sobre o que esses objetos contêm, e a discordância nunca é simétrica: a origem costuma saber mais do que a API do destino está disposta a aceitar. Uma migração que finge o contrário escreve documentos que não coincidem com os que o cliente já recebeu.
Por isso, a primeira coisa que esta ferramenta produz não são dados. É uma lista do que não vai chegar.
O dinheiro são cêntimos inteiros, e a conversão lê texto
Ambos os painéis entregam um valor como número JSON (9.95) ou como texto decimal ("9.95"). A conversão óbvia é multiplicar por cem.
A conversão óbvia está errada. Como número de vírgula flutuante de 64 bits, 9,95 vale 9,949999999999999289, e 9.95 * 100.0 dá 994,9999999999999. Truncado, são 994 cêntimos: um cêntimo a menos do que diz a fatura. Num servidor de jogo isso é um incómodo de arredondamento. Numa contabilidade é um documento incorreto, e aparece quando alguém fecha o exercício.
Por isso o modelo canónico transporta um valor como número inteiro de cêntimos mais um código de moeda, e a conversão conta algarismos na forma textual em vez de passar por vírgula flutuante. Mais de duas casas decimais arredondam afastando-se de zero em vez de truncar, porque meio cêntimo truncado faz desaparecer uma linha de fatura. A notação científica é recusada de imediato: suportar 1e2 pela metade transformaria uma fatura de cem euros num cêntimo, e recusar sai mais barato do que ser esperto. Existe um limite superior de 2^53 cêntimos, porque a outra ponta do fio conta em números de JavaScript, e um valor que aceitamos e que ali já não se representa com exatidão é um desvio silencioso em vez de um erro.
Quinze omissões, cada uma medida
Oito valem para qualquer migração de faturação, e sete são próprias da primeira rota, de FOSSBilling para Paymenter. As quinze foram medidas contra contentores em execução e não retiradas de documentação.
As oito gerais são as que se adivinham assim que se dizem em voz alta. As palavras-passe dos clientes são resumos calculados com a chave aplicacional da origem e não funcionariam em mais lado nenhum, e o ponto de entrada de utilizadores do destino exige uma palavra-passe nova de pelo menos oito caracteres. Os cartões e mandatos guardados são fichas pertencentes à conta do painel de origem junto do prestador de pagamento; levá-los significaria que sai um débito de uma conta que o cliente nunca assinou. A configuração das plataformas de pagamento transporta chaves e endereços de webhook do painel antigo, pelo que nunca é lida. Os modelos de correio estão escritos na linguagem de modelos do seu próprio painel, e um modelo transferido produz mensagens com marcadores por substituir. Os anexos de bilhetes existem como ficheiro no disco do painel de origem, sem qualquer ponto de entrada que devolva o conteúdo. A contabilidade de afiliados não tem equivalente com o mesmo sentido, e levá-la faria renascer comissões já pagas. Os códigos de desconto sem o seu historial de utilização estão francamente errados: um código limitado a dez usos e já usado quatro vezes chega como um código com dez usos disponíveis. Os segredos de dois fatores estão cifrados por painel e nunca são devolvidos.
As sete próprias da rota são a metade interessante, porque são todas limites da API de administração do destino e não do destino em si.
| O que fica para trás | Porquê, medido |
|---|---|
| Empresa, morada, número fiscal, telefone, idioma | O pedido de criação valida nome próprio, apelido, correio, palavra-passe, data de verificação e papel, e o controlador cria exatamente o conjunto validado. Tudo o resto cai. |
| Números de fatura | invoices.number é único na base de dados mas não aparece em nenhuma das duas regras de validação nem em qualquer recurso; não é escrevível nem relegível. |
| O imposto numa linha | Uma linha de fatura aceita descrição, quantidade, preço e uma referência. Não existe campo de imposto. |
| O catálogo de produtos | O controlador de produtos só tem index e show, e o ficheiro de permissões concede apenas view sobre produtos e categorias. |
| Os registos de pagamento | A API não publica qualquer ponto de entrada para pagamentos ou transações. |
| Um serviço já vencido | A data de expiração é validada como hoje ou posterior. |
| As identidades do pessoal | Uma mensagem de bilhete exige um user_id que exista como utilizador, e o pessoal da origem não é utilizador do destino. |
Duas dessas linhas transportam uma decisão que vale a pena nomear.
Os números de fatura vão para stderr. Como o destino atribui a sua própria série e não aceita nem devolve o número antigo, o migrador escreve uma linha por fatura na saída de erro com o número de origem, onde o registo do agente a recolhe. Não é elegante. É honesto: o número está num documento que o cliente tem na mão, e o único sítio onde ainda o podemos pôr é um registo que quem administra controla.
Uma fatura com imposto é bloqueada em vez de escrita. O destino calcula o imposto a partir das suas próprias definições, pelo que a mesma fatura ficaria ali com um total diferente do documento já enviado. Escrevê-la à mesma produz um segundo registo contraditório de uma única operação. O plano coloca essas faturas sob tax_not_carried e segue em frente.
Essa distinção atravessa toda a ferramenta. Um produto em falta bloqueia o serviço em vez de o arrumar noutro sítio. Bloquear é um resultado; adivinhar não.
A origem responde HTTP 200 quando falha
A API de administração do FOSSBilling responde a uma chamada falhada com estado 200 e um corpo de {"result":null,"error":{"message":"...","code":9999}}. Quem olha apenas para o código de estado lê uma falha como resposta vazia, e uma migração que não distingue vazio de falhado acabará um dia por sincronizar uma contabilidade até ficar sem nada. Existe exatamente uma função que abre esse envelope, e olha para error antes de olhar para result.
O leitor é ainda apenas de leitura por construção, e isso é um teste em vez de uma promessa. Cada método que pode invocar é uma constante nomeada terminada em get ou get_list, sete no total, e um teste lê o ficheiro de origem e confronta cada chamada client.get( com essa lista. Um leitor que ganhasse uma chamada _create seria um teste vermelho e não uma descoberta posterior.
Outra estranheza medida: um produto do FOSSBilling volta com os sete períodos de faturação marcados como ativos e com preço zero, seja o que for que o painel venda. O preço é o único sinal utilizável sobre qual ciclo é real.
Idempotência sobre uma impressão, por não haver chave externa
A forma habitual de tornar uma migração retomável é escrever um identificador próprio no destino e procurá-lo da próxima vez. O Paymenter não oferece onde o pôr: não há campo de referência externa que sobreviva a uma escrita e volte numa leitura.
A identidade é, portanto, uma impressão calculada a partir do conteúdo dos dois lados.
- Um cliente é o seu endereço de correio, em minúsculas.
- Um serviço é correio, nome do produto, preço em cêntimos, moeda e quantidade.
- Uma fatura é correio, moeda, data de vencimento e as suas linhas, com o total somado a partir dessas linhas.
- Um bilhete é correio mais assunto.
Duas delas transportam um aviso no código. A impressão do serviço exclui deliberadamente a próxima data de vencimento, porque o destino recusa uma data passada e guarda uma vazia, e então o mesmo serviço teria ao retomar uma impressão diferente da que tinha ao ser escrito e seria criado uma segunda vez: um campo que o destino pode alterar não pode fazer parte da chave. O total da fatura é somado a partir das linhas em vez de lido do campo de total da origem, porque no destino esse valor é um cálculo e não uma coluna, e dois lados que somam de maneira diferente produzem uma chave que nunca coincide. As linhas são ordenadas antes da composição, porque a ordem em que voltam é a dos seus identificadores, que não tem de ser a ordem de escrita se uma delas tiver caído.
A chave do bilhete é a fraca, e está escrito assim: dois bilhetes do mesmo cliente com exatamente o mesmo assunto fundem-se num só. Incluir a primeira mensagem resolveria isso e custa uma segunda chamada por bilhete no destino.
Três maneiras de tratar uma colisão
Quando um objeto já existe no destino, a execução faz uma de três coisas, e a escolha é prévia: skip deixa em paz a linha existente, overwrite_empty preenche apenas os campos que ali estão vazios, e stop para a execução inteira antes de escrever seja o que for.
Medido contra um FOSSBilling 0.8 real e um Paymenter real: uma simulação planeou quatro clientes, quatro serviços, quatro faturas, cinco produtos e dois bilhetes; a execução criou quatro clientes, quatro serviços, quatro faturas e dois bilhetes, com recontagem posterior. Matar o processo a meio com kill -9 e arrancá-lo de novo criou zero objetos e deixou o destino idêntico byte a byte. Com stop houve uma paragem sem uma única escrita. Com overwrite_empty foram preenchidos apenas os campos de nome vazios e os já preenchidos ficaram intactos. Uma fatura numa moeda não suportada voltou como um 422 que nomeava o campo, ficou anotada como currency_unknown, e a execução continuou.
Porque a rota não se chama live
A ferramenta está terminada e medida, e a rota continua sem poder ser iniciada a partir do painel. A razão é uma fronteira que se revelou importante: as credenciais de uma migração são lidas da tabela de integrações, e a coluna de tipo dessa tabela é o registo da infraestrutura que nós operamos. Uma entrada live ali transporta uma página de documentação, um percurso de configuração, um esquema de credenciais, um teste de ligação e nove traduções, e aparece no sítio comercial como algo com que se governam servidores. Um painel de faturação não é isso. Lemo-lo uma vez e nunca mais lhe tocamos.
Chamar live à rota poria em cima um distintivo que diz outra coisa do que o ecrã por baixo, pelo que existe um terceiro estado, adapter_ready, que diz o que é verdade: o adaptador existe e foi medido contra painéis reais, e o lugar onde introduzir os dois painéis ainda não existe. Esse lugar é uma ronda própria, porque guardar um painel de faturação como ligação é uma decisão de esquema e não um subproduto de uma ferramenta de migração.
Do trabalho de migração da Provibr. Porque uma API que responde 200 perante uma falha é uma classe de erro e não um acidente está em porque um 200 do Proxmox não é um resultado, e porque uma migração que saltou em silêncio é pior do que uma que falhou está em a migração que nunca correu.