Skip to content

Commit

Permalink
(PC-34341)[BO] fix: avoid crash at collective offer validation when A…
Browse files Browse the repository at this point in the history
…DAGE notification timeouts
  • Loading branch information
prouzet-pass committed Jan 31, 2025
1 parent bc9b1cf commit 375e825
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 7 deletions.
2 changes: 2 additions & 0 deletions api/src/pcapi/models/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class FeatureToggle(enum.Enum):
API_SIRENE_AVAILABLE = "Active les fonctionnalitées liées à l'API Sirene"
APP_ENABLE_AUTOCOMPLETE = "Active l'autocomplete sur la barre de recherche relative au rework de la homepage"
BENEFICIARY_VALIDATION_AFTER_FRAUD_CHECKS = "Active la validation d'un bénéficiaire via les contrôles de sécurité"
DISABLE_ADAGE_INSTITUTION_NOTIFICATION = "Désactiver la notification de l'établissement à la validation des offres collectives (à utiliser lorsqu'ADAGE est KO)"
DISABLE_ENTERPRISE_API = "Désactiver les appels à l'API entreprise"
DISABLE_BOOST_EXTERNAL_BOOKINGS = "Désactiver les réservations externes Boost"
DISABLE_CDS_EXTERNAL_BOOKINGS = "Désactiver les réservations externes CDS"
Expand Down Expand Up @@ -170,6 +171,7 @@ def nameKey(self) -> str:


FEATURES_DISABLED_BY_DEFAULT: tuple[FeatureToggle, ...] = (
FeatureToggle.DISABLE_ADAGE_INSTITUTION_NOTIFICATION,
FeatureToggle.DISABLE_BOOST_EXTERNAL_BOOKINGS,
FeatureToggle.DISABLE_CDS_EXTERNAL_BOOKINGS,
FeatureToggle.DISABLE_CGR_EXTERNAL_BOOKINGS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from pcapi.core.users import models as users_models
from pcapi.models import db
from pcapi.models import offer_mixin
from pcapi.models.feature import FeatureToggle
from pcapi.repository import atomic
from pcapi.repository import mark_transaction_as_invalid
from pcapi.repository import on_commit
Expand Down Expand Up @@ -366,21 +367,27 @@ def _batch_validate_or_reject_collective_offers(
)
)

if validation is offer_mixin.OfferValidationStatus.APPROVED and collective_offer.institutionId is not None:
if (
validation is offer_mixin.OfferValidationStatus.APPROVED
and collective_offer.institutionId is not None
and not FeatureToggle.DISABLE_ADAGE_INSTITUTION_NOTIFICATION.is_active()
):
try:
adage_client.notify_institution_association(serialize_collective_offer(collective_offer))
except educational_exceptions.AdageInvalidEmailException:
# in the case of an invalid institution email, adage is not notified but we still want to validate of reject the offer
flash(
Markup("Email invalide pour l'offre {offer_id}, Adage n'a pas été notifié").format(
Markup("Email invalide pour l'offre <b>{offer_id}</b>, ADAGE n'a pas été notifié").format(
offer_id=collective_offer.id
)
),
"warning",
)
except educational_exceptions.AdageException as exp:
flash(
Markup("Erreur Adage pour l'offre {offer_id}: {message}").format(
offer_id=collective_offer.id, message=exp.message
)
Markup(
"Erreur lors de la notification à ADAGE pour l'offre <b>{offer_id}</b> : {message}"
).format(offer_id=collective_offer.id, message=exp.message),
"warning",
)

mark_transaction_as_invalid()
Expand All @@ -389,6 +396,19 @@ def _batch_validate_or_reject_collective_offers(
collective_offer_update_succeed_ids.remove(collective_offer.id)

collective_offer_update_failed_ids.append(collective_offer.id)
except Exception as exc: # pylint: disable=broad-exception-caught
# ConnectionError, ReadTimeout...
# When ADAGE is unavailable, we still need to validate collective offers, so the notification
# will not be sent to educational institution.
flash(
Markup(
"Erreur lors de la notification à ADAGE pour l'offre <b>{offer_id}</b> : {message}"
).format(
offer_id=collective_offer.id,
message=getattr(exc, "message", None) or str(exc) or type(exc).__name__,
),
"warning",
)

if len(collective_offer_update_succeed_ids) == 1:
flash(
Expand Down
53 changes: 52 additions & 1 deletion api/tests/routes/backoffice/collective_offers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from flask import url_for
import pytest
import requests.exceptions

from pcapi.core.categories import subcategories_v2 as subcategories
from pcapi.core.educational import exceptions as educational_exceptions
Expand Down Expand Up @@ -849,6 +850,56 @@ def test_validate_collective_offer_with_institution_invalid_email(
assert collective_offer_to_validate.validation == OfferValidationStatus.APPROVED
assert collective_offer_to_validate.lastValidationType == OfferValidationType.MANUAL

@pytest.mark.settings(
ADAGE_API_URL="https://adage_base_url",
ADAGE_BACKEND="pcapi.core.educational.adage_backends.adage.AdageHttpClient",
)
def test_validate_collective_offer_adage_timeout(self, legit_user, authenticated_client, requests_mock):
collective_offer = educational_factories.PendingCollectiveOfferFactory()

endpoint = requests_mock.post("https://adage_base_url/v1/offre-assoc", exc=requests.exceptions.ReadTimeout)

response = self.post_to_endpoint(
authenticated_client, collective_offer_id=collective_offer.id, follow_redirects=True
)
assert response.status_code == 200

assert (
html_parser.extract_alert(response.data)
== f"Erreur lors de la notification à ADAGE pour l'offre {collective_offer.id} : ReadTimeout"
)

assert endpoint.called

db.session.refresh(collective_offer)
assert collective_offer.isActive is True
assert collective_offer.validation == OfferValidationStatus.APPROVED
assert collective_offer.lastValidationType == OfferValidationType.MANUAL

@pytest.mark.settings(
ADAGE_API_URL="https://adage_base_url",
ADAGE_BACKEND="pcapi.core.educational.adage_backends.adage.AdageHttpClient",
)
@pytest.mark.features(DISABLE_ADAGE_INSTITUTION_NOTIFICATION=True)
def test_validate_collective_offer_adage_notification_disabled(
self, legit_user, authenticated_client, requests_mock
):
collective_offer = educational_factories.PendingCollectiveOfferFactory()

endpoint = requests_mock.post("https://adage_base_url/v1/offre-assoc", exc=requests.exceptions.ReadTimeout)

response = self.post_to_endpoint(
authenticated_client, collective_offer_id=collective_offer.id, follow_redirects=True
)
assert response.status_code == 200

assert not endpoint.called

db.session.refresh(collective_offer)
assert collective_offer.isActive is True
assert collective_offer.validation == OfferValidationStatus.APPROVED
assert collective_offer.lastValidationType == OfferValidationType.MANUAL

def test_cant_validate_non_pending_offer(self, legit_user, authenticated_client):
collective_offer_to_validate = educational_factories.CollectiveOfferFactory(
validation=OfferValidationStatus.REJECTED
Expand Down Expand Up @@ -1042,7 +1093,7 @@ def test_batch_validate_collective_offers_adage_exception(self, legit_user, auth
assert response.status_code == 200
assert (
html_parser.extract_alert(response.data)
== f"Erreur Adage pour l'offre {collective_offer.id}: An error occured on adage side"
== f"Erreur lors de la notification à ADAGE pour l'offre {collective_offer.id} : An error occured on adage side"
)


Expand Down

0 comments on commit 375e825

Please sign in to comment.