Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(PC-33496)[PRO] feat: api pro modale collectif #15677

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api/src/pcapi/core/educational/api/dms.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
]


def import_dms_applications_for_all_eac_procedures(ignore_previous: bool = False) -> None:
"""import dms applications for all eac procedures"""
procedures = [proc for proc in EAC_DS_PROCEDURES if proc > 0]
for procedure_number in procedures:
import_dms_applications(procedure_number=procedure_number, ignore_previous=ignore_previous)


def import_dms_applications(procedure_number: int, ignore_previous: bool = False) -> None:
"""import dms applications for eac status"""
previous_import = _get_previous_import(procedure_number)
Expand Down
7 changes: 2 additions & 5 deletions api/src/pcapi/core/educational/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
from pcapi.core.educational import repository as educational_repository
from pcapi.core.educational.api import booking as educational_api_booking
import pcapi.core.educational.api.adage as adage_api
from pcapi.core.educational.api.dms import EAC_DS_PROCEDURES
from pcapi.core.educational.api.dms import import_dms_applications
from pcapi.core.educational.api.dms import import_dms_applications_for_all_eac_procedures
import pcapi.core.educational.api.institution as institution_api
import pcapi.core.educational.api.playlists as playlists_api
import pcapi.core.educational.models as educational_models
Expand Down Expand Up @@ -153,9 +152,7 @@ def handle_pending_collective_booking_j3() -> None:
@log_cron_with_transaction
def import_eac_dms_application(ignore_previous: bool = False) -> None:
"""Import procedures from dms."""
procedures = [proc for proc in EAC_DS_PROCEDURES if proc > 0]
for procedure_number in procedures:
import_dms_applications(procedure_number=procedure_number, ignore_previous=ignore_previous)
import_dms_applications_for_all_eac_procedures(ignore_previous=ignore_previous)


@blueprint.cli.command("notify_reimburse_collective_booking")
Expand Down
19 changes: 19 additions & 0 deletions api/src/pcapi/core/offerers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
from pcapi.core.educational import exceptions as educational_exceptions
from pcapi.core.educational import models as educational_models
from pcapi.core.educational import repository as educational_repository
from pcapi.core.educational.api import dms as dms_api
import pcapi.core.educational.api.adage as adage_api
import pcapi.core.educational.api.address as educational_address_api
from pcapi.core.external import zendesk_sell
from pcapi.core.external.attributes import api as external_attributes_api
Expand Down Expand Up @@ -3084,3 +3086,20 @@ def update_offerer_address(offerer_address_id: int, address_id: int, label: str
# We shouldn't enp up here, but if so log the exception so we can investigate
db.session.rollback()
logger.exception(exc)


def synchronize_from_adage_and_check_registration(offerer_id: int) -> bool:
# FIXME: to be modified when adage sync cron frequency is updated
since_date = datetime.utcnow() - timedelta(days=1)
adage_api.synchronize_adage_ids_on_venues(debug=False, since_date=since_date)
return offerers_repository.offerer_has_venue_with_adage_id(offerer_id)


def synchronize_from_ds_and_check_application(offerer_id: int) -> bool:
dms_api.import_dms_applications_for_all_eac_procedures(ignore_previous=False)
query = (
db.session.query(offerers_models.Venue)
.filter(offerers_models.Venue.managingOffererId == offerer_id)
.filter(offerers_models.Venue.collectiveDmsApplications.any())
)
return db.session.query(query.exists()).scalar()
57 changes: 57 additions & 0 deletions api/src/pcapi/routes/pro/offerers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pcapi.connectors.big_query.queries.offerer_stats import DAILY_CONSULT_PER_OFFERER_LAST_180_DAYS_TABLE
from pcapi.connectors.big_query.queries.offerer_stats import TOP_3_MOST_CONSULTED_OFFERS_LAST_30_DAYS_TABLE
from pcapi.connectors.entreprise import sirene
import pcapi.core.educational.exceptions as educational_exceptions
import pcapi.core.finance.api as finance_api
import pcapi.core.finance.exceptions as finance_exceptions
import pcapi.core.finance.repository as finance_repository
Expand All @@ -25,6 +26,7 @@
from pcapi.routes.apis import private_api
from pcapi.routes.serialization import offerers_serialize
from pcapi.serialization.decorator import spectree_serialize
from pcapi.utils import requests
from pcapi.utils.rest import check_user_has_access_to_offerer

from . import blueprint
Expand Down Expand Up @@ -326,3 +328,58 @@ def get_offerer_headline_offer(
if not offerer_headline_offer:
raise ResourceNotFoundError()
return offerers_serialize.OffererHeadLineOfferResponseModel.from_orm(offerer_headline_offer)


@private_api.route("/offerers/<int:offerer_id>/eligibility", methods=["GET"])
@login_required
@atomic()
@spectree_serialize(
response_model=offerers_serialize.OffererEligibilityResponseModel,
api=blueprint.pro_private_schema,
on_success_status=200,
)
def get_offerer_eligibility(
offerer_id: int,
) -> offerers_serialize.OffererEligibilityResponseModel:
check_user_has_access_to_offerer(current_user, offerer_id)

try:
has_adage_id = api.synchronize_from_adage_and_check_registration(offerer_id)
if has_adage_id:
return offerers_serialize.OffererEligibilityResponseModel(
offerer_id=offerer_id,
has_adage_id=has_adage_id,
has_ds_application=None,
is_onboarded=True,
)
except (educational_exceptions.AdageException, requests.exceptions.RequestException) as exception:
has_adage_id = None
logger.error(
"Error while checking Adage status",
extra={
"offerer_id": offerer_id,
"error": str(exception),
},
)

try:
has_ds_application = api.synchronize_from_ds_and_check_application(offerer_id)
except Exception as exception: # pylint: disable=broad-exception-caught
has_ds_application = None
logger.error(
"Error while checking Adage status",
extra={
"offerer_id": offerer_id,
"error": str(exception),
},
)

if has_adage_id is None and has_ds_application is None:
raise ApiErrors(errors={"eligibility": ["Le statut de la structure n'a pas pu être vérifié"]}, status_code=400)

return offerers_serialize.OffererEligibilityResponseModel(
offerer_id=offerer_id,
has_adage_id=has_adage_id,
has_ds_application=has_ds_application,
is_onboarded=has_adage_id or has_ds_application,
)
12 changes: 12 additions & 0 deletions api/src/pcapi/routes/serialization/offerers_serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from pcapi.routes.serialization.address_serialize import AddressResponseModel
from pcapi.routes.serialization.venues_serialize import BannerMetaModel
from pcapi.routes.serialization.venues_serialize import DMSApplicationForEAC
from pcapi.serialization.utils import to_camel
import pcapi.utils.date as date_utils
from pcapi.utils.email import sanitize_email

Expand Down Expand Up @@ -431,3 +432,14 @@ class OffererHeadLineOfferResponseModel(BaseModel):

class Config:
orm_mode = True


class OffererEligibilityResponseModel(BaseModel):
offerer_id: int
has_adage_id: bool | None
has_ds_application: bool | None
is_onboarded: bool | None

class Config:
allow_population_by_field_name = True
alias_generator = to_camel
54 changes: 54 additions & 0 deletions api/tests/routes/pro/get_offerer_eligibility_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import pytest

from pcapi.core import testing
import pcapi.core.educational.factories as collective_factories
from pcapi.core.offerers import factories as offerers_factories
from pcapi.core.testing import assert_num_queries
from pcapi.core.users import factories as users_factories


pytestmark = pytest.mark.usefixtures("db_session")


class Return200Test:

@pytest.mark.parametrize(
"adage_id,collective_ds_application,is_onboarded",
[
(None, None, False),
("1", None, True),
(None, "1", True),
("1", "1", True),
],
)
def test_get_offerer_eligibility_success(self, client, adage_id, collective_ds_application, is_onboarded):
pro = users_factories.ProFactory()
offerer = offerers_factories.OffererFactory()
offerers_factories.UserOffererFactory(user=pro, offerer=offerer)

venue = offerers_factories.VenueFactory(managingOfferer=offerer, adageId=adage_id)
if collective_ds_application is not None:
collective_factories.CollectiveDmsApplicationFactory(venue=venue, procedure=collective_ds_application)

offerer_id = offerer.id
client = client.with_session_auth(pro.email)
response = client.get(f"/offerers/{offerer_id}/eligibility")
assert response.status_code == 200
assert response.json.get("isOnboarded") is is_onboarded
assert adage_id is None or response.json.get("hasAdageId")
if adage_id is None:
assert collective_ds_application is None or response.json.get("hasDsApplication")


class Return400Test:
num_queries = testing.AUTHENTICATION_QUERIES
num_queries += 1 # check user_offerer
num_queries += 1 # rollback (atomic)

def test_access_by_unauthorized_pro_user(self, client):
pro = users_factories.ProFactory()
client = client.with_session_auth(email=pro.email)
offerer_id = 0
with assert_num_queries(self.num_queries):
response = client.get(f"/offerers/{offerer_id}/eligibility")
assert response.status_code == 403
1 change: 1 addition & 0 deletions pro/src/apiClient/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export { OfferAddressType } from './models/OfferAddressType';
export { OfferContactFormEnum } from './models/OfferContactFormEnum';
export type { OfferDomain } from './models/OfferDomain';
export type { OffererApiKey } from './models/OffererApiKey';
export type { OffererEligibilityResponseModel } from './models/OffererEligibilityResponseModel';
export type { OffererHeadLineOfferResponseModel } from './models/OffererHeadLineOfferResponseModel';
export { OffererMemberStatus } from './models/OffererMemberStatus';
export type { OffererStatsDataModel } from './models/OffererStatsDataModel';
Expand Down
11 changes: 11 additions & 0 deletions pro/src/apiClient/v1/models/OffererEligibilityResponseModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type OffererEligibilityResponseModel = {
hasAdageId?: boolean | null;
hasDsApplication?: boolean | null;
isOnboarded?: boolean | null;
offererId: number;
};

24 changes: 23 additions & 1 deletion pro/src/apiClient/v1/services/DefaultService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import type { ListVenueProviderResponse } from '../models/ListVenueProviderRespo
import type { LoginUserBodyModel } from '../models/LoginUserBodyModel';
import type { NewPasswordBodyModel } from '../models/NewPasswordBodyModel';
import type { OffererHeadLineOfferResponseModel } from '../models/OffererHeadLineOfferResponseModel';
import type { OffererEligibilityResponseModel } from '../models/OffererEligibilityResponseModel';
import type { OffererStatsResponseModel } from '../models/OffererStatsResponseModel';
import type { OfferStatus } from '../models/OfferStatus';
import type { PatchAllOffersActiveStatusBodyModel } from '../models/PatchAllOffersActiveStatusBodyModel';
Expand Down Expand Up @@ -1449,7 +1450,28 @@ export class DefaultService {
403: `Forbidden`,
422: `Unprocessable Entity`,
},
});
})
}
/**
* get_offerer_eligibility <GET>
* @param offererId
* @returns OffererEligibilityResponseModel OK
* @throws ApiError
*/
public getOffererEligibility(
offererId: number
): CancelablePromise<OffererEligibilityResponseModel> {
return this.httpRequest.request({
method: 'GET',
url: '/offerers/{offerer_id}/eligibility',
path: {
offerer_id: offererId,
},
errors: {
403: `Forbidden`,
422: `Unprocessable Entity`,
},
})
}
/**
* get_offerer_headline_offer <GET>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { axe } from 'vitest-axe'

import { renderWithProviders } from 'commons/utils/renderWithProviders'
Expand All @@ -7,7 +8,11 @@ import { OnboardingOffersChoice } from './OnboardingOffersChoice'

describe('OnboardingOffersChoice Component', () => {
beforeEach(() => {
renderWithProviders(<OnboardingOffersChoice />)
renderWithProviders(<OnboardingOffersChoice />, {
storeOverrides: {
offerer: { selectedOffererId: 1, offererNames: [], isOnboarded: false, },
},
})
})

it('should pass axe accessibility tests', async () => {
Expand Down Expand Up @@ -39,4 +44,14 @@ describe('OnboardingOffersChoice Component', () => {
const secondCardButton = screen.getAllByText('Commencer')[1]
expect(secondCardButton).toBeInTheDocument()
})

it('displays the onboarding collective modal when the second button is clicked', async () => {
await userEvent.click(
screen.getByTitle('Commencer la création d’offre sur ADAGE')
)

expect(
await screen.findByTestId('onboarding-collective-modal')
).toBeInTheDocument()
})
})
Loading
Loading