toulouse-maelis: notification asynchrone du paiement des factures (#76394) #182
No reviewers
Labels
No Label
No Milestone
No Assignees
3 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: entrouvert/passerelle#182
Loading…
Reference in New Issue
No description provided.
Delete Branch "wip/76394-parsifal-pay-invoice-async"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Enregistrer les paiement notifiés par Lingo en base.
Appeler de façon asynchrone le WS payInvoices pour palier à des erreurs réseau.
Renvoyer à Lingo la liste des factures à payer en tenant compte que Maélis n'a peut-être pas reçu/traité la notification.
(ça n'est pas encore une relecture totalement attentive mais c'est déjà ça)
@ -3783,0 +3786,4 @@
invoice.family_id = family_id
invoice.save()
else:
if invoice.regie_id != regie_id:
En cas de mise à jour il ne faut pas mettre updated à jour ?
Le code a changé ici. Je teste ça dans le nouveau code.
@ -3783,0 +3789,4 @@
if invoice.regie_id != regie_id:
raise APIError(
"invoice '%s' already exist on '%s' regie"
% (invoice.invoice_id, invoice.regie_id)
Il me semble que des régies différentes pourraient émettre des factures avec le même numéro, que l'unicité ici devrait être sur (regie_id, invoice_id).
À priori non, numInvoice est une clé primaire de la table des factures, mais on devrait leur demander.
Je pensais comme Benjamin, mais non, l'identifiant de facture est unique pour 1 régie donnée.
https://redmine.sigec.fr/issues/1573
Au final c'est bien globalement unique https://redmine.sigec.fr/issues/1573#note-21 , le contraire m'aurait étonné parc que c'est pas évident de générer des id de table non unique par rapport à une foreign key (ça demande un trigger au minimum).
C'est juste que la SIGEC ne connait pas ses propres web-service. Des fois j'ai l'impression qu'ils répondent au pif.
@ -3793,2 +3812,2 @@
'amount': str(float(item['amountInvoice']) - float(item['amountPaid'])),
'amount_paid': item['amountPaid'],
'total_amount': amount_invoice,
'amount': str(float(amount_invoice) - float(amount_paid)),
Il ne faut jamais manipuler de l'argent avec float(). (il faut utiliser Decimal)
Fait.
@ -3903,0 +3919,4 @@
invoice_obj.save(update_fields=['updated', 'lingo_data'])
if not invoice_obj.notify(timeout=10):
self.add_job(
'notify_invoice_paid_job',
Ça me semble une complexité superflue, et 10 secondes de perdues, on peut juste tout le temps créer le job asynchrone.
Plus globalement, je serais ici pour 1/ faire le job asynchrone, puis 2/ si dans le job asynchrone ça échoue, tant pis, 3/ avoir un job hourly qui prenne les factures en attente depuis plus de 15 minutes et qui essaie de les notifier. (histoire de ne pas empiler des jobs lors d'une indispo Maélis).
(et plus tard on pourrait aussi avoir l'info des factures qui sont en attente de notification depuis plus de N heures (ou jours?), ce qui révélerait une situation anormale côté Maélis)
Fait.
@ -3999,0 +4028,4 @@
return 'notified'
if self.lingo_data:
return 'paid'
if self.canceled:
s/canceled/cancelled/
Fait.
@ -3999,0 +4039,4 @@
'payInvoices',
numDossier=self.maelis_data['numFamily'],
codeRegie=self.regie_id,
amount=str(float(self.maelis_data['amountInvoice']) - float(self.maelis_data['amountPaid'])),
Ici aussi, ne pas manipuler de l'argent avec float(), utiliser Decimal.
Fait.
J'ai fait des commentaires aussi mais on ne le voit pas, je dois rater un truc.
@ -3789,4 +3809,3 @@
'created': item['dateInvoice'][:10],
'pay_limit_date': item['dateDeadline'][:10],
'display_id': str(item['numInvoice']),
'total_amount': item['amountInvoice'],
Jamais de calcul de monnaie avec float, il faut utiliser decimal.Decimal. Ensuite je laisserai total_amount comme il est, le changement n'amène rien. On aurait pu avoir un objet Payment en prévision des paiements en plusieurs fois au lieu de travailler sur Invoice.status.
avec un modèle
Payment(invoice_id, reference_bank, amount, notification_date)
, ça aurait pu donner ça :J'ai fait ça, sans introduire le modèle Payment (pavé du haut).
(dans un premier temps, sauf si tu y tiens)
Ok dans ce cas je pense qu'il faudrait à minima stocker le montant reçu lors de la notification par lingo, que ça représente le
amount_locally_paid
que j'introduis pour le modèle Payment, qu'on puisse faire un calcul honnête de amountPaid.Si on a pas encore notifié, on devrait avoir amountPaid=0, amount_localy_paid=amontInvoice.
Si on a notifié, on devrait avoir amountPaid=amountInvoice, amount_localy_paid=amountInvoice.
Et si la facture a été payée par ailleurs par un autre moyen, etc.. ça se verra,
et plus tard ça permettra d'introduire Payment et les paiements multiples en virant le champ amount_locally_paid et en le remplaçant par une property
...
Bon voir mon commentaire suivant, je pensais sincèrement qu'amount_locally_paid était quelque chose qu'on avait.
Tu m'as perdu.
Donc j'attends #76572 pour avoir un modèle Payment stabilisé ?
Non c'est bon on verra apr
@ -3771,3 +3774,3 @@
last_update = now()
for item in result:
self.invoice_set.update_or_create(
invoice, created = self.invoice_set.update_or_create(
Pourquoi ne pas juste rajouter
['regie_id','invoice_id']
et['family_id', 'invoice_id']
à Invoice.Meta.unique_together si c'est cela que tu veux exprimer ? Note que tu as déjà une unicité globale pour resource_id je ne vois pas comment on pourrait avoir un doublon en base.Ou sinon plus simple (pas la peine de repréciser `resource_id quand on travaille dans self.invoice_set):
Aussi il faudrait logger création/mise à jour de facture (si possible en faisant un diff de maelis_data pour la mise ) au niveau info.
Je voulais me prémunir d'un appel au endpoint avec de mauvais paramètres dans les tests, tout en ayant un message d'erreur intelligible.
J'ai essayé de suivre tes recommandations :
@ -3802,3 +3822,3 @@
'maelis_item': item,
}
if item['amountInvoice'] == item['amountPaid']:
if amount_invoice == amount_paid:
Remplacer par
if paid
et ne pas corrigeramount
les comptes doivent être justes ou rapportés honnêtement (si trop payé, l'afficher).Fait.
@ -3902,1 +3911,3 @@
)
invoice_obj = self.invoice_set.get(regie_id=regie_id, invoice_id=invoice['maelis_item']['numInvoice'])
if invoice_obj.status() == 'canceled':
Je ne vois pas à quoi ça correspond, vu que rien ne pose jamais @self.canceled = True@. Comme pour les autres cas c'est bien de préférer une DateTime plutôt qu'un booléen pour tous les champs de statut, avec une méthode:
Mais je serai pour attendre de savoir si on en a besoin, je ne vois pas de notion d'annulation dans les web-services de maelis ni chez nous.
Globalement les factures ne devraient pas avoir de statut, et les paiements un seul, notifié ou pas.
Ça correspondra à #76394.
Si la facture est annulée, il faudrait la conserver dans le connecteur, si l'on souhaite remonter jusqu'à la demande pour l'indiquer.
Si tu n'as pas implémenté effectivement l'annulation des factures je préfère que tu vires ce champ et toute la logique qui y a trait parce que tu ne sais pas encore ce que tu devras faire à ce sujet; je vois déjà un besoin de deux champ, un d'annulation et un de notification de l'annulation à Maelis parce qu'il faudra le faire en asynchrone et s'assurer qu'on finit toujours par y arriver, comme pour les paiements, ça passera par un cron unique de synchro sur les factures. Pour moi ça mérite bien un ticket clair, ici on parle juste de notifier les paiements.
C'était pour indiquer aux relecteurs que les factures devraient avoir à minima un statut "annulée" dans lequel elles seraient conservées, bien qu'elles ne soient plus remontées par Maélis.
Peut-être d'abord traiter ce point, oui.
Non, ça vient après : là on peut aussi payer des factures "traditionnelles" qui n'ont rien à voir avec le panier Maélis.
Donc fait : j'ai retiré tout ce qui touche à l'annulation des factures.
@ -3996,6 +4023,35 @@ class Invoice(models.Model):
def __repr__(self):
return '<Invoice "%s/%s">' % (self.regie_id, self.invoice_id)
def status(self):
Préférer un champ 'notification_date', une fois que c'est notifi
C'était déjà un champ date, je l'ai renommé.
@ -3999,0 +4041,4 @@
codeRegie=self.regie_id,
amount=str(float(self.maelis_data['amountInvoice']) - float(self.maelis_data['amountPaid'])),
datePaiement=self.lingo_data['transaction_date'],
refTransaction=self.lingo_data['transaction_id'],
Je crois que je l'ai déjà dit, mais la référence du paiement est dans
lingo_data['bank_transaction_id']
lingo_date['transaction_id
]` c(j'ai un doute là)
https://git.entrouvert.org/entrouvert/combo/src/branch/main/combo/apps/lingo/models.py#L405
Tu as raison, je me fais avoir à chaque fois parce que les deux chemins de paiement sont devenus bien différents mais ce serait bien de les faire converger, on va rester sur transaction_id pour l'instant. On verra si quelqu'un se plaint, visiblement pour Axel ça suffit, mais dans le cas du paiement d'un BasketItem Cyril avait demandé plus de données.
@ -3999,0 +4046,4 @@
numPerson=self.maelis_data['payer']['num'],
timeout=timeout,
)
except RequestException:
soap_client ne lève pas de RequestException mais des
passerelle.utils.soap.SOAPError
.J'ai retiré ce code.
Ok, mais c'est un bug alors il faut les corriger #76609.
cd9a90a923
tod4fc07eb47
toulouse-maelis: notification asynchrone du paiement des factures (#76394)to WIP: toulouse-maelis: notification asynchrone du paiement des factures (#76394)d4fc07eb47
toc384014a0f
WIP: toulouse-maelis: notification asynchrone du paiement des factures (#76394)to toulouse-maelis: notification asynchrone du paiement des factures (#76394)toulouse-maelis: notification asynchrone du paiement des factures (#76394)to WIP: toulouse-maelis: notification asynchrone du paiement des factures (#76394)c384014a0f
tod1e5f2815d
d1e5f2815d
tof3e75060d6
WIP: toulouse-maelis: notification asynchrone du paiement des factures (#76394)to toulouse-maelis: notification asynchrone du paiement des factures (#76394)@ -3897,4 +3911,0 @@
amount=invoice['amount'],
datePaiement=post_data['transaction_date'],
refTransaction=post_data['transaction_id'],
numInvoices=[invoice['display_id']],
Dans le InvoiceService.wsdl on a ça :
mais aussi
C'est pas cohérent, mais si c'est testé ça veut dire que zeep a converti automatiquement la chaîne en entier parce qu'au niveau XSD c'est syntaxiquement pareil, ça mériterait un petit commentaire pour plus tard.
En regardant le wsdl qu'on a dans les sources et qui est quasi à jour :
Quoi qu'il en soit j'ai toujours pas re-testé ce code (plus de facture sous la main) et je fais ça aujourd'hui.
Testé en vrai.
Perso j'aimerais bien pousser, même si on n'est pas à l'abri que Sigec change tout une fois de plus.
https://redmine.sigec.fr/issues/1573#note-24
Oui vas y pousse.
@ -3894,0 +3897,4 @@
invoice.save()
complete_diff = difflib.ndiff(content_two.split('\n'), content_one.split('\n'))
diff = [x for x in complete_diff if x[0] in ['-', '+']]
self.logger.info("update %s on familly '%s': %s", repr(invoice), family_id, diff)
Tu peux logger des messages traduits.
Fait.
@ -4085,0 +4143,4 @@
numDossier=self.family_id,
codeRegie=self.regie_id,
amount=str(
Decimal(self.maelis_data['amountInvoice']) - Decimal(self.maelis_data['amountPaid'])
Et là je me rend compte que le code idiot dans lingo ne remonte pas le montant payé, je serai franchement pour ouvrir tout de suite un ticket pour avoir ça dans la notif et ici pouvoir écrite
self.lingo_data['amount']
parce que si la facture a été partiellement payée par ailleurs on ne sait quel montant a été payé.J'ai ouvert #76572 que je vais traiter de ce pas.
f3e75060d6
to67c94387bf
67c94387bf
to29f4774d2b
J'ai ajouté l'enregistrement du numéro de règlement renvoyé par maélis.
J'ai ajouté un verrou sur notify() pour départager les notifications par job et par cron.