From 0276de78c2bdc83d74c0bc431fe8f01d73b58312 Mon Sep 17 00:00:00 2001 From: Nicolas ROCHE Date: Wed, 26 Apr 2023 21:48:49 +0200 Subject: [PATCH] toulouse-maelis: trigger wcs when basket subscription is removed (#76398) --- passerelle/contrib/toulouse_maelis/models.py | 101 +++++++- tests/test_toulouse_maelis.py | 239 +++++++++++++++++++ 2 files changed, 338 insertions(+), 2 deletions(-) diff --git a/passerelle/contrib/toulouse_maelis/models.py b/passerelle/contrib/toulouse_maelis/models.py index 77911578..c421cec6 100644 --- a/passerelle/contrib/toulouse_maelis/models.py +++ b/passerelle/contrib/toulouse_maelis/models.py @@ -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']] diff --git a/tests/test_toulouse_maelis.py b/tests/test_toulouse_maelis.py index 75a01166..920802f9 100644 --- a/tests/test_toulouse_maelis.py +++ b/tests/test_toulouse_maelis.py @@ -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