diff --git a/api/src/pcapi/core/educational/api/offer.py b/api/src/pcapi/core/educational/api/offer.py index 58ac820f5ec..5aedc17c055 100644 --- a/api/src/pcapi/core/educational/api/offer.py +++ b/api/src/pcapi/core/educational/api/offer.py @@ -22,6 +22,7 @@ from pcapi.core.educational.models import CollectiveOffer from pcapi.core.educational.models import HasImageMixin from pcapi.core.educational.utils import get_image_from_url +from pcapi.core.external.attributes.api import update_external_pro from pcapi.core.mails import transactional as transactional_mails from pcapi.core.object_storage import store_public_object from pcapi.core.offerers import api as offerers_api @@ -268,7 +269,13 @@ def create_collective_offer( formats=offer_data.formats, # type: ignore[arg-type] author=user, ) - collective_offer.bookingEmails = offer_data.booking_emails + + emails = offer_data.booking_emails + collective_offer.bookingEmails = emails + + # we update pro email data in sendinblue + for email in emails: + update_external_pro(email) db.session.add(collective_offer) db.session.commit() diff --git a/api/src/pcapi/core/educational/models.py b/api/src/pcapi/core/educational/models.py index bcac6566420..ea873415e5c 100644 --- a/api/src/pcapi/core/educational/models.py +++ b/api/src/pcapi/core/educational/models.py @@ -1265,6 +1265,9 @@ class EducationalInstitution(PcObject, Base, Model): def full_name(self) -> str: return f"{self.institutionType} {self.name}".strip() + def with_program(self, program_name: str) -> bool: + return any(program.name == program_name for program in self.programs) + class EducationalYear(PcObject, Base, Model): __tablename__ = "educational_year" diff --git a/api/src/pcapi/core/external/attributes/api.py b/api/src/pcapi/core/external/attributes/api.py index a9efd1934b6..f5f697fd795 100644 --- a/api/src/pcapi/core/external/attributes/api.py +++ b/api/src/pcapi/core/external/attributes/api.py @@ -283,16 +283,29 @@ def get_pro_attributes(email: str) -> models.ProAttributes: .load_only(offerers_models.OffererTag.label), joinedload(offerers_models.Venue.bankInformation).load_only(finance_models.BankInformation.status), joinedload(offerers_models.Venue.venueLabel).load_only(offerers_models.VenueLabel.label), + joinedload(offerers_models.Venue.collectiveOffers) + .subqueryload(educational_models.CollectiveOffer.institution) + .subqueryload(educational_models.EducationalInstitution.programs) + .load_only(educational_models.EducationalInstitutionProgram.name), ) .all() ) if venues: + has_collective_offers_marseille_en_grand = False + all_venues += venues for venue in venues: offerers_names.add(venue.managingOfferer.name) offerers_tags.update(tag.label for tag in venue.managingOfferer.tags) + + for collective_offer in venue.collectiveOffers: + institution = collective_offer.institution + if institution and institution.with_program("marseille_en_grand"): + has_collective_offers_marseille_en_grand = True + has_individual_offers = offerers_repository.venues_have_offers(*venues) + attributes.update( { "dms_application_submitted": any(venue.hasPendingBankInformationApplication for venue in venues), @@ -303,6 +316,7 @@ def get_pro_attributes(email: str) -> models.ProAttributes: "has_individual_offers": has_individual_offers, "has_bookings": bookings_repository.venues_have_bookings(*venues), "has_banner_url": all(venue._bannerUrl for venue in venues if venue.isPermanent), + "eac_meg": has_collective_offers_marseille_en_grand, } ) diff --git a/api/src/pcapi/core/external/attributes/models.py b/api/src/pcapi/core/external/attributes/models.py index 5f25e7fbcc3..cd8812029e8 100644 --- a/api/src/pcapi/core/external/attributes/models.py +++ b/api/src/pcapi/core/external/attributes/models.py @@ -91,6 +91,7 @@ class ProAttributes: has_banner_url: bool | None = ( None # Set to False when at least one permanent venue doesn't have a banner URL, True otherwise ) + eac_meg: bool | None = None # At least one collective offer with 'Marseille en Grand' @dataclass diff --git a/api/src/pcapi/core/external/sendinblue.py b/api/src/pcapi/core/external/sendinblue.py index d39a65085ff..dfbd0df204b 100644 --- a/api/src/pcapi/core/external/sendinblue.py +++ b/api/src/pcapi/core/external/sendinblue.py @@ -89,6 +89,7 @@ class SendinblueAttributes(Enum): VENUE_NAME = "VENUE_NAME" VENUE_TYPE = "VENUE_TYPE" IS_EAC = "IS_EAC" + EAC_MEG = "EAC_MEG" @classmethod def list(cls) -> list[str]: @@ -244,6 +245,7 @@ def format_user_attributes(attributes: attributes_models.UserAttributes | attrib SendinblueAttributes.VENUE_NAME.value: _get_attr(attributes, "venues_names", format_list), SendinblueAttributes.VENUE_TYPE.value: _get_attr(attributes, "venues_types", format_list), SendinblueAttributes.IS_EAC.value: _get_attr(attributes, "is_eac"), + SendinblueAttributes.EAC_MEG.value: _get_attr(attributes, "eac_meg"), } diff --git a/api/tests/core/educational/test_models.py b/api/tests/core/educational/test_models.py index 1d562815814..e3a9148af04 100644 --- a/api/tests/core/educational/test_models.py +++ b/api/tests/core/educational/test_models.py @@ -603,6 +603,15 @@ def test_unique_program_for_an_educational_institution(self): institution.programs = [program1, program2] db.session.commit() + def test_with_program(self): + program1 = factories.EducationalInstitutionProgramFactory(name="under_test") + _program2 = factories.EducationalInstitutionProgramFactory(name="not_to_be_found") + + institution = factories.EducationalInstitutionFactory(programs=[program1]) + + assert institution.with_program("under_test") + assert not institution.with_program("not_to_be_found") + class CollectiveOfferDisplayedStatusTest: @pytest.mark.parametrize("status", set(CollectiveOfferDisplayedStatus)) diff --git a/api/tests/core/external/__init__.py b/api/tests/core/external/__init__.py index ce97c109fd1..d706b831b38 100644 --- a/api/tests/core/external/__init__.py +++ b/api/tests/core/external/__init__.py @@ -83,4 +83,5 @@ has_bookings=True, is_eac=False, has_banner_url=True, + eac_meg=False, ) diff --git a/api/tests/core/external/external_pro_test.py b/api/tests/core/external/external_pro_test.py index 697258210cc..b20e5104bad 100644 --- a/api/tests/core/external/external_pro_test.py +++ b/api/tests/core/external/external_pro_test.py @@ -23,6 +23,7 @@ # 1 query on user table with joinedload # 1 query on venue table with joinedload # 2 extra SQL queries: select exists on offer and booking tables +# 1 query on educational_institution for program (meg) table with joinedload EXPECTED_PRO_ATTR_NUM_QUERIES = 5 @@ -222,7 +223,11 @@ def test_update_external_pro_user_attributes( venueTypeCode=VenueTypeCode.CONCERT_HALL, # different from others ) - with assert_num_queries(EXPECTED_PRO_ATTR_NUM_QUERIES): + num_queries = EXPECTED_PRO_ATTR_NUM_QUERIES + if create_permanent: + num_queries += 1 + + with assert_num_queries(num_queries): attributes = get_pro_attributes(email) assert attributes.is_pro is True diff --git a/api/tests/core/external/sendinblue_test.py b/api/tests/core/external/sendinblue_test.py index 3faa90fa196..5c5130419d5 100644 --- a/api/tests/core/external/sendinblue_test.py +++ b/api/tests/core/external/sendinblue_test.py @@ -93,6 +93,7 @@ def test_format_attributes(self): "VENUE_NAME": None, "VENUE_TYPE": None, "IS_EAC": None, + "EAC_MEG": None, } def test_format_pro_attributes(self): @@ -156,6 +157,7 @@ def test_format_pro_attributes(self): "VENUE_NAME": "Venue Name 1,Venue Name 2", "VENUE_TYPE": "BOOKSTORE,MOVIE", "IS_EAC": False, + "EAC_MEG": False, } @@ -216,10 +218,10 @@ def setup_method(self): ), ] - self.expected_header = "BOOKED_OFFER_CATEGORIES;BOOKED_OFFER_CATEGORIES_COUNT;BOOKED_OFFER_SUBCATEGORIES;BOOKING_COUNT;BOOKING_VENUES_COUNT;CREDIT;DATE_CREATED;DATE_OF_BIRTH;DEPARTMENT_CODE;DEPOSITS_COUNT;DEPOSIT_ACTIVATION_DATE;DEPOSIT_EXPIRATION_DATE;DMS_APPLICATION_APPROVED;DMS_APPLICATION_SUBMITTED;ELIGIBILITY;FIRSTNAME;HAS_BANNER_URL;HAS_BOOKINGS;HAS_COLLECTIVE_OFFERS;HAS_COMPLETED_ID_CHECK;HAS_OFFERS;INITIAL_CREDIT;IS_ACTIVE_PRO;IS_BENEFICIARY;IS_BENEFICIARY_18;IS_BOOKING_EMAIL;IS_CURRENT_BENEFICIARY;IS_EAC;IS_ELIGIBLE;IS_EMAIL_VALIDATED;IS_FORMER_BENEFICIARY;IS_PERMANENT;IS_PRO;IS_TAGGED_COLLECTIVITE;IS_UNDERAGE_BENEFICIARY;IS_USER_EMAIL;IS_VIRTUAL;LASTNAME;LAST_BOOKING_DATE;LAST_FAVORITE_CREATION_DATE;LAST_VISIT_DATE;MARKETING_EMAIL_SUBSCRIPTION;MOST_BOOKED_MOVIE_GENRE;MOST_BOOKED_MUSIC_TYPE;MOST_BOOKED_OFFER_SUBCATEGORY;MOST_FAVORITE_OFFER_SUBCATEGORIES;OFFERER_NAME;PERMANENT_THEME_PREFERENCE;POSTAL_CODE;PRODUCT_BRUT_X_USE_DATE;USER_ID;USER_IS_ATTACHED;USER_IS_CREATOR;VENUE_COUNT;VENUE_LABEL;VENUE_NAME;VENUE_TYPE;EMAIL" - self.eren_expected_file_body = "CINEMA,LIVRE;2;ABO_LIVRE_NUMERIQUE,CARTE_CINE_ILLIMITE,CINE_PLEIN_AIR;4;3;480.00;06-02-2021;06-05-2003;12;1;;;;;age-18;First name;;;;Yes;;500;;Yes;Yes;;Yes;;Yes;Yes;No;;No;;No;;;Last name;06-05-2021;;;Yes;COMEDY;900;CINE_PLEIN_AIR;CINE_PLEIN_AIR,SUPPORT_PHYSIQUE_FILM;;cinema;;06-05-2021;1;;;;;;;eren.yeager@shinganshina.paradis" - self.mikasa_expected_file_body = "CINEMA,LIVRE;2;ABO_LIVRE_NUMERIQUE,CARTE_CINE_ILLIMITE,CINE_PLEIN_AIR;4;3;480.00;06-02-2021;06-05-2003;12;1;;;;;age-18;First name;;;Yes;Yes;;500;;Yes;Yes;;Yes;;Yes;Yes;No;;Yes;;No;;;Last name;06-05-2021;;;Yes;COMEDY;900;CINE_PLEIN_AIR;CINE_PLEIN_AIR,SUPPORT_PHYSIQUE_FILM;;cinema;;06-05-2021;2;;;;;;;mikasa.ackerman@shinganshina.paradis" - self.armin_expected_file_body = "CINEMA,LIVRE;2;ABO_LIVRE_NUMERIQUE,CARTE_CINE_ILLIMITE,CINE_PLEIN_AIR;4;3;480.00;06-02-2021;06-05-2003;12;1;;;;;age-18;First name;;;;Yes;;500;;Yes;Yes;;Yes;;Yes;Yes;No;;No;;No;;;Last name;06-05-2021;;;Yes;COMEDY;900;CINE_PLEIN_AIR;CINE_PLEIN_AIR,SUPPORT_PHYSIQUE_FILM;;cinema;;06-05-2021;3;;;;;;;armin.arlert@shinganshina.paradis" + self.expected_header = "BOOKED_OFFER_CATEGORIES;BOOKED_OFFER_CATEGORIES_COUNT;BOOKED_OFFER_SUBCATEGORIES;BOOKING_COUNT;BOOKING_VENUES_COUNT;CREDIT;DATE_CREATED;DATE_OF_BIRTH;DEPARTMENT_CODE;DEPOSITS_COUNT;DEPOSIT_ACTIVATION_DATE;DEPOSIT_EXPIRATION_DATE;DMS_APPLICATION_APPROVED;DMS_APPLICATION_SUBMITTED;EAC_MEG;ELIGIBILITY;FIRSTNAME;HAS_BANNER_URL;HAS_BOOKINGS;HAS_COLLECTIVE_OFFERS;HAS_COMPLETED_ID_CHECK;HAS_OFFERS;INITIAL_CREDIT;IS_ACTIVE_PRO;IS_BENEFICIARY;IS_BENEFICIARY_18;IS_BOOKING_EMAIL;IS_CURRENT_BENEFICIARY;IS_EAC;IS_ELIGIBLE;IS_EMAIL_VALIDATED;IS_FORMER_BENEFICIARY;IS_PERMANENT;IS_PRO;IS_TAGGED_COLLECTIVITE;IS_UNDERAGE_BENEFICIARY;IS_USER_EMAIL;IS_VIRTUAL;LASTNAME;LAST_BOOKING_DATE;LAST_FAVORITE_CREATION_DATE;LAST_VISIT_DATE;MARKETING_EMAIL_SUBSCRIPTION;MOST_BOOKED_MOVIE_GENRE;MOST_BOOKED_MUSIC_TYPE;MOST_BOOKED_OFFER_SUBCATEGORY;MOST_FAVORITE_OFFER_SUBCATEGORIES;OFFERER_NAME;PERMANENT_THEME_PREFERENCE;POSTAL_CODE;PRODUCT_BRUT_X_USE_DATE;USER_ID;USER_IS_ATTACHED;USER_IS_CREATOR;VENUE_COUNT;VENUE_LABEL;VENUE_NAME;VENUE_TYPE;EMAIL" + self.eren_expected_file_body = "CINEMA,LIVRE;2;ABO_LIVRE_NUMERIQUE,CARTE_CINE_ILLIMITE,CINE_PLEIN_AIR;4;3;480.00;06-02-2021;06-05-2003;12;1;;;;;;age-18;First name;;;;Yes;;500;;Yes;Yes;;Yes;;Yes;Yes;No;;No;;No;;;Last name;06-05-2021;;;Yes;COMEDY;900;CINE_PLEIN_AIR;CINE_PLEIN_AIR,SUPPORT_PHYSIQUE_FILM;;cinema;;06-05-2021;1;;;;;;;eren.yeager@shinganshina.paradis" + self.mikasa_expected_file_body = "CINEMA,LIVRE;2;ABO_LIVRE_NUMERIQUE,CARTE_CINE_ILLIMITE,CINE_PLEIN_AIR;4;3;480.00;06-02-2021;06-05-2003;12;1;;;;;;age-18;First name;;;Yes;Yes;;500;;Yes;Yes;;Yes;;Yes;Yes;No;;Yes;;No;;;Last name;06-05-2021;;;Yes;COMEDY;900;CINE_PLEIN_AIR;CINE_PLEIN_AIR,SUPPORT_PHYSIQUE_FILM;;cinema;;06-05-2021;2;;;;;;;mikasa.ackerman@shinganshina.paradis" + self.armin_expected_file_body = "CINEMA,LIVRE;2;ABO_LIVRE_NUMERIQUE,CARTE_CINE_ILLIMITE,CINE_PLEIN_AIR;4;3;480.00;06-02-2021;06-05-2003;12;1;;;;;;age-18;First name;;;;Yes;;500;;Yes;Yes;;Yes;;Yes;Yes;No;;No;;No;;;Last name;06-05-2021;;;Yes;COMEDY;900;CINE_PLEIN_AIR;CINE_PLEIN_AIR,SUPPORT_PHYSIQUE_FILM;;cinema;;06-05-2021;3;;;;;;;armin.arlert@shinganshina.paradis" def test_build_file_body(self): expected = ( diff --git a/api/tests/routes/pro/post_collective_offers_test.py b/api/tests/routes/pro/post_collective_offers_test.py index 5fea0cd9e0f..240b475cdeb 100644 --- a/api/tests/routes/pro/post_collective_offers_test.py +++ b/api/tests/routes/pro/post_collective_offers_test.py @@ -9,6 +9,7 @@ from pcapi.core.educational.models import CollectiveOffer import pcapi.core.offerers.factories as offerers_factories from pcapi.core.testing import override_features +from pcapi.core.users import testing as sendinblue_testing import pcapi.core.users.factories as users_factories @@ -107,6 +108,8 @@ def test_create_collective_offer(self, client): assert_offer_values(offer, data, user, offerer) + assert len(sendinblue_testing.sendinblue_requests) == 3 + def test_create_collective_offer_college_6(self, client): # Given venue = offerers_factories.VenueFactory() diff --git a/api/tests/scripts/external_users/batch_update_users_attributes_test.py b/api/tests/scripts/external_users/batch_update_users_attributes_test.py index c1e7b869f45..da9765059e4 100644 --- a/api/tests/scripts/external_users/batch_update_users_attributes_test.py +++ b/api/tests/scripts/external_users/batch_update_users_attributes_test.py @@ -138,6 +138,7 @@ def test_format_sendinblue_user(): "DMS_APPLICATION_APPROVED": None, "DMS_APPLICATION_SUBMITTED": None, "ELIGIBILITY": user.eligibility, + "EAC_MEG": None, "FIRSTNAME": "Jeanne", "HAS_BANNER_URL": None, "HAS_BOOKINGS": None,