toulouse-maelis: add a for_payment parameter to reduce cancellation delay (#76856) #227

Merged
nroche merged 2 commits from wip/76856-parsifal-param-for-invoice-cancel-delay into main 2023-04-28 07:53:53 +02:00
3 changed files with 184 additions and 21 deletions

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.18 on 2023-04-21 13:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('toulouse_maelis', '0010_toulousemaelis_max_payment_delay'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='start_payment_date',
field=models.DateTimeField(null=True),
),
]

View File

@ -300,11 +300,15 @@ class ToulouseMaelis(BaseResource, HTTPResource):
lingo_notification_date__isnull=True,
basket_generation_date__isnull=False,
maelis_cancel_notification_date__isnull=True,
created__lte=now()
- datetime.timedelta(minutes=(self.cancel_invoice_delay + self.max_payment_delay)),

Garde created__lte=now() - datetime.timedelta(minutes=self.cancel_invoice_delay) mais à ajoute :

  .filter(Q(start_payment_date__isnull=True) | Q(start_payment_date__lte=now() -  datetime.timedelta(minutes=self.max_payment_delay))

faut prendre le max (my bad) pas le min. Désolé.

Garde `created__lte=now() - datetime.timedelta(minutes=self.cancel_invoice_delay)` mais à ajoute : ``` .filter(Q(start_payment_date__isnull=True) | Q(start_payment_date__lte=now() - datetime.timedelta(minutes=self.max_payment_delay)) ``` faut prendre le max (my bad) pas le min. Désolé.

faut prendre le max (my bad) pas le min. Désolé.

oui (si j'ai bien interprété), donc on change rien ici.
---8<---

Actuellement on a :

  • cancel_invoice_delay=30 : Délai de conservation des factures issues d'un panier
  • max_payment_delay=20 : Délai maximum pour payer une facture via Lingo
  • à T la fature est crée
  • à T+30, la facture n'est plus affichée dans Lingo, mais reste payable
  • à T+30+20, on envoie l'ordre d'annulation de la facture à Maélis

Le but de ce ticket c'est :

  • à T'>T, on commence le paiement (/invoice/xxx/?for_payment)
  • à T'+20, on envoie l'ordre d'annulation de la facture à Maélis

Ce qui est résumé dans la description du ticket par :
delais avant annulation = min(T+30, T'+20)

Si l'on réduit le delais avant annulation (pas d'affichage lingo) à min(T+30, T'+20),
alors on doit annuler (ordre qui est passé à maélis) les factures qui sont dans ces deux cas (OU logique)
soit (T <= t - (30+20) ) OU (T' <= t - 20).

> faut prendre le max (my bad) pas le min. Désolé. oui (si j'ai bien interprété), donc on change rien ici. ---8<--- Actuellement on a : * cancel_invoice_delay=30 : Délai de conservation des factures issues d'un panier * max_payment_delay=20 : Délai maximum pour payer une facture via Lingo * à T la fature est crée * à T+30, la facture n'est plus affichée dans Lingo, mais reste payable * à T+30+20, on envoie l'ordre d'annulation de la facture à Maélis Le but de ce ticket c'est : * à T'>T, on commence le paiement (/invoice/xxx/?for_payment) * à T'+20, on envoie l'ordre d'annulation de la facture à Maélis Ce qui est résumé dans la description du ticket par : delais avant annulation = min(T+30, T'+20) Si l'on réduit le delais avant annulation (pas d'affichage lingo) à min(T+30, T'+20), alors on doit annuler (ordre qui est passé à maélis) les factures qui sont dans ces deux cas (OU logique) soit (T <= t - (30+20) ) OU (T' <= t - 20).
)
for invoice in invoices:
invoice.cancel()
if (

La condition ne sera plus nécessaire.

La condition ne sera plus nécessaire.

Merci de m'avoir donné la formule (que j'avais demandé), je la comprends, mais en fait je ne souhaite pas optimiser dès à présent (pour moi c'est encore trop embrouillé).

Merci de m'avoir donné la formule (que j'avais demandé), je la comprends, mais en fait je ne souhaite pas optimiser dès à présent (pour moi c'est encore trop embrouillé).
invoice.start_payment_date is not None
and invoice.start_payment_date <= now() - datetime.timedelta(minutes=self.max_payment_delay)
) or invoice.created <= now() - datetime.timedelta(
minutes=(self.cancel_invoice_delay + self.max_payment_delay)
):
invoice.cancel()
def hourly(self):
self.notify_invoices_paid()
@ -3992,9 +3996,11 @@ class ToulouseMaelis(BaseResource, HTTPResource):
def invoices(self, request, regie_id, NameID=None, family_id=None):
family_id = family_id or self.get_link(NameID).family_id
invoices = [
i.format_content() for i in self.get_invoices(family_id, regie_id) if i.status() == 'created'
i.format_content()
for i in self.get_invoices(family_id, regie_id)
if i.status() in ['created', 'for_payment']
]
return {'data': invoices}
return {'has_invoice_for_payment': True, 'data': invoices}
@endpoint(
display_category='Facture',
@ -4037,13 +4043,23 @@ class ToulouseMaelis(BaseResource, HTTPResource):
parameters={
'regie_id': {'description': 'Identifiant de la régie', 'example_value': '102'},
'invoice_id': {'description': 'Identifiant de facture', 'example_value': 'IDFAM-42'},
'for_payment': {
'description': "Si présent, annuler la facture panier à l'expiration du delai maximum de paiement depuis la date de l'appel"
},
},
)
def invoice(self, request, regie_id, invoice_id, **kwargs):
def invoice(self, request, regie_id, invoice_id, for_payment=None, **kwargs):
invoice = self.get_invoice(regie_id, invoice_id)
if invoice.status() == 'cancelled':
raise APIError('Invoice cancelled')

Je pense que tu peux mettre à jour start_payment_date sur n'importe quel type de facture ça ne mange pas de pain et ça nous donne une information dans tous les cas.

Je pense que tu peux mettre à jour start_payment_date sur n'importe quel type de facture ça ne mange pas de pain et ça nous donne une information dans tous les cas.

Fait.

Fait.
return {'data': invoice.format_content()}
if for_payment is not None:
invoice.start_payment_date = now()
invoice.save()
if invoice.status() == 'cancelling':

Non c'est sur invoices qu'il faut ajouter has_invoice_for_payment et c'est toujours True.

Non c'est sur invoices qu'il faut ajouter has_invoice_for_payment et c'est toujours True.

Ce point est important, sans ça ça ne marchera pas.

Ce point est important, sans ça ça ne marchera pas.

Oups, j'ai compris tout de travers.
Il s'agit ici d'ajouter une indication comme quoi le connecteur gère bien le paramètre '?for_payment'.

J'ai corrigé : je le renvoie toujours à True sur /invoies et il n'est pas renvoyé sur invoices/history.

Oups, j'ai compris tout de travers. Il s'agit ici d'ajouter une indication comme quoi le connecteur gère bien le paramètre '?for_payment'. J'ai corrigé : je le renvoie toujours à True sur `/invoies` et il n'est pas renvoyé sur `invoices/history`.
raise APIError('Invoice cancelling')
return {
'data': invoice.format_content(),
}
@endpoint(
display_category='Facture',
@ -4068,7 +4084,7 @@ class ToulouseMaelis(BaseResource, HTTPResource):
invoice = self.get_invoice(regie_id, invoice_id)
if invoice.status() in ['paid', 'notified']:
raise APIError('Invoice already paid')
if invoice.maelis_cancel_notification_date is not None:
if invoice.status() == 'cancelled':
raise APIError('Invoice cancelled')
invoice.lingo_data = post_data
@ -4179,6 +4195,7 @@ class Invoice(models.Model):
created = models.DateTimeField('Created', auto_now_add=True)
updated = models.DateTimeField('Updated', auto_now=True)
maelis_data_update_date = models.DateTimeField(null=True)
start_payment_date = models.DateTimeField(null=True)
lingo_notification_date = models.DateTimeField(null=True)
maelis_notification_date = models.DateTimeField(null=True)
basket_generation_date = models.DateTimeField(null=True)
@ -4192,15 +4209,26 @@ class Invoice(models.Model):
self.maelis_data['amountInvoice'] == self.maelis_data['amountPaid']
or self.maelis_notification_date is not None
):
# payInvoice was sent to Maelis
return 'notified'
if self.lingo_notification_date is not None:
# pay order received from Lingo
return 'paid'
if (
self.lingo_notification_date is None
and self.basket_generation_date is not None
and self.created <= now() - datetime.timedelta(minutes=self.resource.cancel_invoice_delay)
):
return 'cancelled'
# basket invoice only
if self.basket_generation_date is not None:
if self.maelis_cancel_notification_date is not None:
# cancelInvoiceAndDeleteSubscribeList was sent to Maelis
return 'cancelled'
if self.lingo_notification_date is None:
if self.start_payment_date is not None:
# displayed into Lingo but no more payable
return 'for_payment'
if self.created <= now() - datetime.timedelta(minutes=self.resource.cancel_invoice_delay):
# hide invoice to Lingo
return 'cancelling'
# new invoice
return 'created'
def format_content(self):
@ -4224,8 +4252,10 @@ class Invoice(models.Model):
'reference_id': item['numInvoice'],
'maelis_item': item,
}
if paid:
if paid or self.status() == 'for_payment':
invoice.update({'pay_limit_date': '', 'online_payment': False})
if self.status() == 'for_payment':
invoice['no_online_payment_reason'] = 'Transation de payement en cours'
return invoice
@transaction.atomic

View File

@ -8821,7 +8821,7 @@ def test_cancel_basket_invoice_cron(activity_service, invoice_service, con, app,
freezer.move_to('2023-03-03 19:00:00')
con.cancel_basket_invoices()
invoice = con.invoice_set.get(regie_id=109, invoice_id=18)
assert invoice.status() == 'cancelled'
assert invoice.status() == 'cancelling'
assert invoice.maelis_cancel_notification_date is None
resp = app.get(get_endpoint('regie/109/invoices') + '?family_id=1312')
@ -8834,9 +8834,67 @@ def test_cancel_basket_invoice_cron(activity_service, invoice_service, con, app,
assert caplog.records[-1].levelno == logging.INFO
assert caplog.records[-1].message == 'Annulation de <Invoice "109/18"> sur la famille \'1312\''
invoice = con.invoice_set.get(regie_id=109, invoice_id=18)
assert invoice.status() == 'cancelled'
assert invoice.maelis_cancel_notification_date.strftime('%Y-%m-%d %H:%M:%S') == '2023-03-03 19:20:00'
def test_cancel_basket_invoice_cron_having_for_payment_date(
activity_service, invoice_service, con, app, freezer, caplog
):
def request_check(request):
assert request == 'F10055641671'
activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket.xml'))
activity_service.add_soap_response('validateBasket', get_xml_file('R_validate_basket.xml'))
invoice_service.add_soap_response('readInvoices', get_xml_file('R_read_invoices_regie_109.xml'))
activity_service.add_soap_response(
'cancelInvoiceAndDeleteSubscribeList',
get_xml_file('R_cancel_invoice_and_delete_subscribe_list.xml'),
request_check=request_check,
)
Link.objects.create(resource=con, family_id='1312', name_id='local')
assert con.cancel_invoice_delay == 30
assert con.max_payment_delay == 20
# invoice created on validate basket
freezer.move_to('2023-03-03 18:30:00')
resp = app.post_json(
get_endpoint('validate-basket') + '?NameID=local', params={'basket_id': 'S10055641661'}
)
assert resp.json['err'] == 0
invoice = con.invoice_set.get(regie_id=109, invoice_id=18)
assert invoice.status() == 'created'
assert invoice.basket_generation_date.strftime('%Y-%m-%d %H:%M:%S') == '2023-03-03 18:30:00'
assert invoice.maelis_cancel_notification_date is None
# notificate payment starts
freezer.move_to('2023-03-03 18:40:00')
resp = app.get(get_endpoint('regie/109/invoice/1312-18') + '?for_payment')
assert resp.json['err'] == 0
invoice = con.invoice_set.get(regie_id=109, invoice_id=18)
assert invoice.start_payment_date.strftime('%Y-%m-%d %H:%M:%S') == '2023-03-03 18:40:00'
# invoice is still displayed before cancellation order is sent to maelis
# (but no more payable)
con.cancel_basket_invoices()
invoice = con.invoice_set.get(regie_id=109, invoice_id=18)
assert invoice.status() == 'for_payment'
assert invoice.maelis_cancel_notification_date is None
resp = app.get(get_endpoint('regie/109/invoices') + '?family_id=1312')
assert resp.json['err'] == 0
assert '1312-18' in [x['id'] for x in resp.json['data']]
# cancellation order is now sent to maelis
freezer.move_to('2023-03-03 19:10:00')
con.cancel_basket_invoices()
assert caplog.records[-1].levelno == logging.INFO
assert caplog.records[-1].message == 'Annulation de <Invoice "109/18"> sur la famille \'1312\''
invoice = con.invoice_set.get(regie_id=109, invoice_id=18)
assert invoice.status() == 'cancelled'
assert invoice.maelis_cancel_notification_date.strftime('%Y-%m-%d %H:%M:%S') == '2023-03-03 19:10:00'
def test_cancel_basket_invoice_cron_keep_paid_invoices(
activity_service, invoice_service, con, app, freezer, caplog
):
@ -8946,7 +9004,7 @@ def test_cancel_basket_invoice_cron_maelis_error(activity_service, invoice_servi
else:
assert False, 'cron should raise an exception'
invoice = con.invoice_set.get(regie_id=109, invoice_id=18)
assert invoice.status() == 'cancelled'
assert invoice.status() == 'cancelling'
assert invoice.maelis_cancel_notification_date is None
resp = app.get(get_endpoint('regie/109/invoices') + '?family_id=1312')
@ -8990,7 +9048,7 @@ def test_cancel_basket_invoice_on_get_baskets(activity_service, con, app, freeze
resp = app.get(url + '?family_id=1312')
assert resp.json['err'] == 0
invoice = con.invoice_set.get(regie_id=109, invoice_id=18)
assert invoice.status() == 'cancelled'
assert invoice.status() == 'cancelling'
assert invoice.maelis_cancel_notification_date is None
# cancellation order is now sent to maelis
@ -9216,6 +9274,7 @@ def test_invoices(invoice_service, con, app, caplog, freezer):
Link.objects.create(resource=con, family_id='1312', name_id='local')
resp = app.get(url + '?NameID=local')
assert resp.json['err'] == 0
assert resp.json['has_invoice_for_payment'] is True
assert len(resp.json['data'])
for invoice in resp.json['data']:
assert invoice['display_id']
@ -9281,7 +9340,7 @@ def test_invoices(invoice_service, con, app, caplog, freezer):
freezer.move_to('2023-03-03 18:30:00')
invoice.basket_generation_date = invoice.created
invoice.save()
assert invoice.status() == 'cancelled'
assert invoice.status() == 'cancelling'
resp = app.get(url + '?NameID=local')
assert resp.json['err'] == 0
assert resp.json['data'] == []
@ -9441,6 +9500,10 @@ def test_invoice_if_cancelled(activity_service, invoice_service, con, app, freez
activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket.xml'))
activity_service.add_soap_response('validateBasket', get_xml_file('R_validate_basket.xml'))
invoice_service.add_soap_response('readInvoices', get_xml_file('R_read_invoices_regie_109.xml'))
activity_service.add_soap_response(
'cancelInvoiceAndDeleteSubscribeList',
get_xml_file('R_cancel_invoice_and_delete_subscribe_list.xml'),
)
url = get_endpoint('regie/109/invoice/1312-18')
assert con.cancel_invoice_delay == 30
@ -9456,9 +9519,62 @@ def test_invoice_if_cancelled(activity_service, invoice_service, con, app, freez
assert resp.json['data']['display_id'] == '18'
assert resp.json['data']['label'] == 'DSBL TEST'
# cancelled basket invoice is no more returned
# cancelling basket invoice is no more returned
freezer.move_to('2023-03-03 19:00:00')
invoice = con.invoice_set.get(regie_id=109, invoice_id=18)
assert invoice.status() == 'cancelling'
resp = app.get(url + '?NameID=local')
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'Invoice cancelling'
# cancelled basket invoice is no more returned
freezer.move_to('2023-03-03 19:20:00')
con.cancel_basket_invoices()
invoice = con.invoice_set.get(regie_id=109, invoice_id=18)
assert invoice.status() == 'cancelled'
resp = app.get(url + '?NameID=local')
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'Invoice cancelled'
def test_invoice_for_payment(activity_service, invoice_service, con, app, freezer):
activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket.xml'))
activity_service.add_soap_response('validateBasket', get_xml_file('R_validate_basket.xml'))
activity_service.add_soap_response(
'cancelInvoiceAndDeleteSubscribeList',
get_xml_file('R_cancel_invoice_and_delete_subscribe_list.xml'),
)
invoice_service.add_soap_response('readInvoices', get_xml_file('R_read_invoices_regie_109.xml'))
url = get_endpoint('regie/109/invoice/1312-18')
assert con.cancel_invoice_delay == 30
# invoice created on validate basket
freezer.move_to('2023-03-03 18:30:00')
resp = app.post_json(
get_endpoint('validate-basket') + '?family_id=1312', params={'basket_id': 'S10055641661'}
)
assert resp.json['err'] == 0
resp = app.get(url + '?NameID=ignored&for_payment')
assert resp.json['err'] == 0
assert resp.json['data']['display_id'] == '18'
assert resp.json['data']['label'] == 'DSBL TEST'
# basket invoice is still returned but is no more payable
freezer.move_to('2023-03-03 18:50:00')
invoice = con.invoice_set.get(regie_id=109, invoice_id=18)
assert invoice.start_payment_date is not None
assert invoice.status() == 'for_payment'
resp = app.get(url + '?NameID=local')
assert resp.json['err'] == 0
assert resp.json['data']['display_id'] == '18'
assert resp.json['data']['pay_limit_date'] == ''
assert resp.json['data']['online_payment'] is False
assert resp.json['data']['no_online_payment_reason'] == 'Transation de payement en cours'
# basket invoice is no more returned since cancellation order sent to maelis
con.cancel_basket_invoices()
invoice = con.invoice_set.get(regie_id=109, invoice_id=18)
assert invoice.status() == 'cancelled'
resp = app.get(url + '?NameID=local')
assert resp.json['err'] == 1
@ -9674,7 +9790,7 @@ def test_pay_not_yet_cancelled_basket_invoice(activity_service, invoice_service,
# cancellation order was not sent to maelis
freezer.move_to('2023-03-03 19:20:00')
invoice = con.invoice_set.get(regie_id=109, invoice_id=18)
assert invoice.status() == 'cancelled'
assert invoice.status() == 'cancelling'
assert invoice.maelis_cancel_notification_date is None
resp = app.post_json(url + '?NameID=ignored', params=data)
assert resp.json['err'] == 0