toulouse-maelis: trigger wcs when basket subscription is removed (#76398)

This commit is contained in:
Nicolas Roche 2023-04-26 21:48:49 +02:00 committed by Nicolas Roche
parent 80fc536e2b
commit 0276de78c2
2 changed files with 338 additions and 2 deletions

View File

@ -21,10 +21,11 @@ import json
import re
from decimal import Decimal
from operator import itemgetter
from urllib.parse import urljoin
from urllib.parse import urljoin, urlparse
import zeep
from dateutil import rrule
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models, transaction
from django.db.models import JSONField
@ -43,6 +44,7 @@ from passerelle.utils.conversion import simplify
from passerelle.utils.jsonresponse import APIError
from passerelle.utils.soap import SOAPFault, SOAPServiceUnreachable
from passerelle.utils.templates import render_to_string
from passerelle.utils.wcs import WcsApi, WcsApiError
from . import activity_schemas, family_schemas, invoice_schemas, schemas, utils
@ -310,9 +312,24 @@ class ToulouseMaelis(BaseResource, HTTPResource):
):
invoice.cancel()
def trigger_subscriptions_cron(self):
# find subscriptions removed from baskets
pending_basket_subscriptions = self.subscription_set.filter(invoice__isnull=True)
family_ids = {x.family_id for x in pending_basket_subscriptions}
for family_id in family_ids:
self.get_baskets_raw(family_id)
subscriptions = self.subscription_set.filter(
wcs_trigger_date__isnull=True,
)
for subscription in subscriptions:
if subscription.trigger_status() == 'triggering':
subscription.trigger()
def hourly(self):
self.notify_invoices_paid()
self.cancel_basket_invoices()
self.trigger_subscriptions_cron()
def get_referential(self, referential_name, id=None, q=None, limit=None, distinct=True):
if id is not None:
@ -4158,6 +4175,13 @@ class ToulouseMaelis(BaseResource, HTTPResource):
return
invoice.notify()
def trigger_subscription_job(self, pk):
try:
subscription = self.subscription_set.get(pk=pk)
except Invoice.DoesNotExist:
return
subscription.trigger()
@endpoint(
display_category='Facture',
name='regie',
@ -4392,13 +4416,86 @@ class Subscription(models.Model):
return 'pending_basket'
def trigger_status(self):
if self.status() in ['paid', 'cancelled', 'removed']:
if self.wcs_trigger_date is not None:
# wcs demand was triggered
return 'triggered'
if self.status() in ['removed']:
# wcs demand can be triggered
return 'triggering'
else:
# waiting for a definive subscription status
return 'pending'
def set_trigger(self):
if self.trigger_status() != 'triggering':
return
if self.wcs_trigger_payload:
return
self.wcs_trigger_payload = {
'err': 1 if self.status() in ['removed'] else 0,
'data': {
'regie_id': self.regie_id,
'regie_text': self.resource.get_referential_value('Regie', self.regie_id),
'invoice_id': self.invoice.invoice_id if self.invoice else None,
'invoice_status': self.invoice.status() if self.invoice else None,
'invoice_data': self.invoice.maelis_data if self.invoice else None,
'subscription_id': self.pk,
'subscription_status': self.status(),
'subscription_data': self.maelis_data,
},
}
if self.status() == 'removed':
self.wcs_trigger_payload['err_desc'] = "Le panier n'a pas été validé"
self.save()
self.resource.add_job(
'trigger_subscription_job',
pk=self.pk,
natural_id='%s/%s' % (self.wcs_form_number, self.pk),
)
def get_wcs_api(self, base_url):
scheme, netloc, dummy, dummy, dummy, dummy = urlparse(base_url)
services = settings.KNOWN_SERVICES.get('wcs', {})
service = None
for service in services.values():
remote_url = service.get('url')
r_scheme, r_netloc, dummy, dummy, dummy, dummy = urlparse(remote_url)
if r_scheme == scheme and r_netloc == netloc:
return WcsApi(
base_url,
orig=service.get('orig'),
key=service.get('secret'),
session=self.resource.requests,
)
@transaction.atomic
def trigger(self):
obj = Subscription.objects.select_for_update().get(pk=self.pk)
if obj.trigger_status() != 'triggering':
return
base_url = '%shooks/update_subscription/' % (obj.wcs_form_api_url)
wcs_api = obj.get_wcs_api(base_url)
if not wcs_api:
err_desc = 'Cannot find wcs service for %s' % obj.wcs_form_api_url
self.resource.logger.warning(err_desc)
result = err_desc
else:
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
self.resource.logger.info(
'trigger wcs: %s -> %s' % (base_url, self.wcs_trigger_payload['data']['subscription_status'])
)
try:
result = wcs_api.post_json(obj.wcs_trigger_payload, [], headers=headers)
except WcsApiError as e:
self.resource.logger.warning(e)
return
obj.wcs_trigger_date = now()
obj.wcs_trigger_response = result
obj.save()
class Meta:
ordering = ('resource', 'wcs_form_number')
unique_together = [['resource', 'wcs_form_number']]

View File

@ -15,6 +15,7 @@
import base64
import datetime
import json
import logging
import os
from unittest import mock
@ -138,6 +139,21 @@ def ape_service():
yield mock
@pytest.fixture
def wcs_service(settings, requests_mock):
wcs_service = {
'default': {
'title': 'test',
'url': 'https://wcs.example.com',
'secret': 'xxx',
'orig': 'passerelle',
},
}
settings.KNOWN_SERVICES = {'wcs': wcs_service}
with requests_mock as mock:
yield mock
@pytest.fixture(scope='module')
def django_db_setup(django_db_setup, django_db_blocker):
with django_db_blocker.unblock():
@ -10131,3 +10147,226 @@ def test_invoice_pdf_error(invoice_service, con, app):
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'django.http.response.Http404'
assert resp.json['err_desc'] == 'Fichier PDF non trouvé'
def test_trigger_wcs_on_removed_subscriptions_cron(
family_service, activity_service, wcs_service, con, app, freezer, caplog
):
family_service.add_soap_response('readFamily', get_xml_file('R_read_family_for_subscription.xml'))
activity_service.add_soap_response('getPersonUnitInfo', get_xml_file('R_get_person_unit_info.xml'))
activity_service.add_soap_response('addPersonUnitBasket', get_xml_file('R_add_person_unit_basket.xml'))
activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket_empty.xml'))
wcs_service.add(
responses.POST,
'https://wcs.example.com/api/forms/exemple-inscription-loisirs-1/12/hooks/update_subscription/',
json={'err': 0},
status=200,
)
Link.objects.create(resource=con, family_id='1312', name_id='local')
# subscribe providing a wcs demand
freezer.move_to('2023-03-03 18:20:00')
resp = app.post_json(
get_endpoint('add-person-basket-subscription') + '?NameID=local',
params={
'person_id': '266145',
'activity_id': 'A10053179798',
'unit_id': 'A10053179809',
'place_id': 'A10053179757',
'start_date': '2023-02-01',
'end_date': '2023-06-30',
'form_api_url': 'https://wcs.example.com/api/forms/exemple-inscription-loisirs-1/12/',
'form_number': '13-12',
},
)
assert resp.json['err'] == 0
subscription = con.subscription_set.get(wcs_form_number='13-12')
assert subscription.status() == 'pending_basket'
assert [x['idIns'] for x in subscription.maelis_data['basket']['lignes']] == ['S10055641658']
# basket was removed, send trigger to wcs
con.hourly()
assert (
'https://wcs.example.com/api/forms/exemple-inscription-loisirs-1/12/hooks/update_subscription/'
in wcs_service.calls[-1].request.url
)
trigger_body = json.loads(wcs_service.calls[-1].request.body)
assert trigger_body['err'] == 1
assert trigger_body['err_desc'] == "Le panier n'a pas été validé"
assert trigger_body['data']['subscription_status'] == 'removed'
assert trigger_body['data']['regie_text'] == 'DSBL'
assert any(['trigger wcs' in x.message for x in caplog.records])
subscription = con.subscription_set.get(wcs_form_number='13-12')
assert subscription.trigger_status() == 'triggered'
def test_trigger_wcs_on_removed_subscriptions_job(
family_service, activity_service, wcs_service, con, app, freezer, caplog
):
family_service.add_soap_response('readFamily', get_xml_file('R_read_family_for_subscription.xml'))
activity_service.add_soap_response('getPersonUnitInfo', get_xml_file('R_get_person_unit_info.xml'))
activity_service.add_soap_response('addPersonUnitBasket', get_xml_file('R_add_person_unit_basket.xml'))
activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket_empty.xml'))
wcs_service.add(
responses.POST,
'https://wcs.example.com/api/forms/exemple-inscription-loisirs-1/12/hooks/update_subscription/',
json={'err': 0},
status=200,
)
Link.objects.create(resource=con, family_id='1312', name_id='local')
# subscribe providing a wcs demand
freezer.move_to('2023-03-03 18:20:00')
resp = app.post_json(
get_endpoint('add-person-basket-subscription') + '?NameID=local',
params={
'person_id': '266145',
'activity_id': 'A10053179798',
'unit_id': 'A10053179809',
'place_id': 'A10053179757',
'start_date': '2023-02-01',
'end_date': '2023-06-30',
'form_api_url': 'https://wcs.example.com/api/forms/exemple-inscription-loisirs-1/12/',
'form_number': '13-12',
},
)
assert resp.json['err'] == 0
subscription = con.subscription_set.get(wcs_form_number='13-12')
assert subscription.status() == 'pending_basket'
assert [x['idIns'] for x in subscription.maelis_data['basket']['lignes']] == ['S10055641658']
# get basket having subscription removed
resp = app.get(get_endpoint('get-baskets') + '?NameID=local')
assert resp.json['err'] == 0
subscription = con.subscription_set.get(wcs_form_number='13-12')
assert subscription.status() == 'removed'
assert subscription.trigger_status() == 'triggering'
# send trigger to wcs
con.jobs()
assert (
'https://wcs.example.com/api/forms/exemple-inscription-loisirs-1/12/hooks/update_subscription/'
in wcs_service.calls[-1].request.url
)
trigger_body = json.loads(wcs_service.calls[-1].request.body)
assert trigger_body['err'] == 1
assert trigger_body['err_desc'] == "Le panier n'a pas été validé"
assert trigger_body['data']['subscription_status'] == 'removed'
assert trigger_body['data']['regie_text'] == 'DSBL'
assert any(['trigger wcs' in x.message for x in caplog.records])
subscription = con.subscription_set.get(wcs_form_number='13-12')
assert subscription.trigger_status() == 'triggered'
def test_trigger_wcs_service_error(family_service, activity_service, con, app, freezer, caplog):
family_service.add_soap_response('readFamily', get_xml_file('R_read_family_for_subscription.xml'))
activity_service.add_soap_response('getPersonUnitInfo', get_xml_file('R_get_person_unit_info.xml'))
activity_service.add_soap_response('addPersonUnitBasket', get_xml_file('R_add_person_unit_basket.xml'))
activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket_empty.xml'))
Link.objects.create(resource=con, family_id='1312', name_id='local')
# subscribe providing a wcs demand
freezer.move_to('2023-03-03 18:20:00')
resp = app.post_json(
get_endpoint('add-person-basket-subscription') + '?NameID=local',
params={
'person_id': '266145',
'activity_id': 'A10053179798',
'unit_id': 'A10053179809',
'place_id': 'A10053179757',
'start_date': '2023-02-01',
'end_date': '2023-06-30',
'form_api_url': 'https://wcs.example.com/api/forms/exemple-inscription-loisirs-1/12/',
'form_number': '13-12',
},
)
assert resp.json['err'] == 0
subscription = con.subscription_set.get(wcs_form_number='13-12')
assert subscription.status() == 'pending_basket'
# basket was removed, send trigger to wcs
con.hourly()
assert caplog.records[-1].levelno == logging.WARNING
assert (
caplog.records[-1].message
== 'Cannot find wcs service for https://wcs.example.com/api/forms/exemple-inscription-loisirs-1/12/'
)
subscription = con.subscription_set.get(wcs_form_number='13-12')
assert subscription.trigger_status() == 'triggered'
assert (
subscription.wcs_trigger_response
== 'Cannot find wcs service for https://wcs.example.com/api/forms/exemple-inscription-loisirs-1/12/'
)
def test_trigger_wcs_api_error(family_service, activity_service, wcs_service, con, app, freezer):
family_service.add_soap_response('readFamily', get_xml_file('R_read_family_for_subscription.xml'))
activity_service.add_soap_response('getPersonUnitInfo', get_xml_file('R_get_person_unit_info.xml'))
activity_service.add_soap_response('addPersonUnitBasket', get_xml_file('R_add_person_unit_basket.xml'))
activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket_empty.xml'))
Link.objects.create(resource=con, family_id='1312', name_id='local')
# subscribe providing a wcs demand
freezer.move_to('2023-03-03 18:20:00')
resp = app.post_json(
get_endpoint('add-person-basket-subscription') + '?NameID=local',
params={
'person_id': '266145',
'activity_id': 'A10053179798',
'unit_id': 'A10053179809',
'place_id': 'A10053179757',
'start_date': '2023-02-01',
'end_date': '2023-06-30',
'form_api_url': 'https://wcs.example.com/api/forms/exemple-inscription-loisirs-1/12/',
'form_number': '13-12',
},
)
assert resp.json['err'] == 0
subscription = con.subscription_set.get(wcs_form_number='13-12')
assert subscription.status() == 'pending_basket'
assert len([x for x in wcs_service.calls if '/hooks/' in x.request.url]) == 0
# basket was removed, send trigger to wcs
wcs_service.add(
responses.POST,
'https://wcs.example.com/api/forms/exemple-inscription-loisirs-1/12/hooks/update_subscription/',
json={'err': 1, 'err_class': 'Access denied', 'err_desc': None},
status=403,
)
con.hourly()
assert len([x for x in wcs_service.calls if '/hooks/' in x.request.url]) == 1
subscription = con.subscription_set.get(wcs_form_number='13-12')
assert subscription.trigger_status() == 'triggering'
# retry
wcs_service.add(
responses.POST,
'https://wcs.example.com/api/forms/exemple-inscription-loisirs-1/12/hooks/update_subscription/',
body=CONNECTION_ERROR,
)
con.hourly()
assert len([x for x in wcs_service.calls if '/hooks/' in x.request.url]) == 2
subscription = con.subscription_set.get(wcs_form_number='13-12')
assert subscription.trigger_status() == 'triggering'
# retry again
wcs_service.add(
responses.POST,
'https://wcs.example.com/api/forms/exemple-inscription-loisirs-1/12/hooks/update_subscription/',
body='plop',
status=500,
)
con.hourly()
assert len([x for x in wcs_service.calls if '/hooks/' in x.request.url]) == 3
subscription = con.subscription_set.get(wcs_form_number='13-12')
assert subscription.trigger_status() == 'triggering'
assert subscription.wcs_trigger_response is None