diff --git a/app/commands.py b/app/commands.py index 4c66b3d5..41d4a596 100644 --- a/app/commands.py +++ b/app/commands.py @@ -323,3 +323,36 @@ def load_company_stats(): def temp_command_generate_xm_control(id): control = ControllerControl.query.get(id) temp_write_greco_xml(control) + + +@app.cli.command("sync_brevo", with_appcontext=True) +@click.argument("pipeline_names", nargs=-1) +@click.option( + "--verbose", + is_flag=True, + help="Enable verbose mode for more detailed output", +) +def sync_brevo_command(pipeline_names, verbose): + """ + Command to sync companies between the database and Brevo. + You can specify one or more pipeline names as arguments. + Example usage: flask sync_brevo "Test Dev churn" + """ + from app.services.sync_companies_with_brevo import ( + sync_companies_with_brevo, + ) + from app.helpers.brevo import BrevoApiClient + + if not pipeline_names: + print("Please provide at least one pipeline name.") + return + + brevo = BrevoApiClient(app.config["BREVO_API_KEY"]) + + app.logger.info( + f"Process sync companies with Brevo began for pipelines: {pipeline_names}" + ) + + sync_companies_with_brevo(brevo, list(pipeline_names), verbose=verbose) + + app.logger.info("Process sync companies with Brevo done") diff --git a/app/controllers/mission.py b/app/controllers/mission.py index da1984ef..94ff4308 100644 --- a/app/controllers/mission.py +++ b/app/controllers/mission.py @@ -13,7 +13,10 @@ ) from app.controllers.utils import atomic_transaction from app.data_access.mission import MissionOutput -from app.domain.mission import get_start_end_time_at_employee_validation +from app.domain.mission import ( + get_start_end_time_at_employee_validation, + get_mission_start_and_end_from_activities, +) from app.domain.notifications import ( warn_if_mission_changes_since_latest_user_action, ) @@ -24,6 +27,8 @@ company_admin, check_actor_can_write_on_mission_for_user, ) +from app.domain.regulations import compute_regulations +from app.domain.user import get_current_employment_in_company from app.domain.validation import ( validate_mission, pre_check_validate_mission_by_admin, @@ -41,6 +46,7 @@ UnavailableSwitchModeError, ) from app.helpers.graphene_types import TimeStamp +from app.helpers.submitter_type import SubmitterType from app.models import Company, User, Activity from app.models.mission import Mission from app.models.mission_end import MissionEnd @@ -480,8 +486,37 @@ def mutate(cls, _, info, mission_id, user_id): mission = Mission.query.get(mission_id) user = User.query.get(user_id) activities_to_update = mission.activities_for(user) + is_current_user_admin = company_admin( + current_user, mission.company_id + ) + should_recompute_regulations = ( + not mission.is_holiday() + and is_current_user_admin + and len(activities_to_update) > 0 + ) + for activity in activities_to_update: activity.dismiss() + + if should_recompute_regulations: + employment = get_current_employment_in_company( + user=user, company=mission.company + ) + business = employment.business if employment else None + ( + mission_start, + mission_end, + ) = get_mission_start_and_end_from_activities( + activities=activities_to_update, user=user + ) + compute_regulations( + user=user, + period_start=mission_start, + period_end=mission_end, + submitter_type=SubmitterType.ADMIN, + business=business, + ) + return mission diff --git a/app/helpers/brevo.py b/app/helpers/brevo.py index a1228fc2..42519896 100644 --- a/app/helpers/brevo.py +++ b/app/helpers/brevo.py @@ -8,7 +8,6 @@ from app import app from app.helpers.errors import MobilicError -from config import BREVO_API_KEY_ENV class BrevoRequestError(MobilicError): @@ -39,11 +38,29 @@ class LinkCompanyContactData: contact_id: int +@dataclass +class GetDealData: + deal_id: str + + +@dataclass +class UpdateDealStageData: + deal_id: str + pipeline_id: str + stage_id: str + + +@dataclass +class GetDealsByPipelineData: + pipeline_id: str + limit: Optional[int] = None + + def check_api_key(func): @wraps(func) def wrapper(self, *args, **kwargs): if self.api_key is None: - app.logger.warning(f"{BREVO_API_KEY_ENV} not found.") + app.logger.warning("BREVO_API_KEY not found.") return None else: return func(self, *args, **kwargs) @@ -130,6 +147,101 @@ def link_company_and_contact(self, data: LinkCompanyContactData): except ApiException as e: raise BrevoRequestError(f"Request to Brevo API failed: {e}") + @check_api_key + def get_deal(self, data: GetDealData): + try: + url = f"{self.BASE_URL}crm/deals/{data.deal_id}" + response = self._session.get(url) + response.raise_for_status() + return response.json() + except ApiException as e: + raise BrevoRequestError(f"Request to Brevo API failed: {e}") + + @check_api_key + def update_deal_stage(self, data: UpdateDealStageData): + try: + url = f"{self.BASE_URL}crm/deals/{data.deal_id}" + payload = { + "attributes": { + "pipeline": data.pipeline_id, + "deal_stage": data.stage_id, + } + } + response = self._session.patch(url, json=payload) + response.raise_for_status() + + if response.status_code == 204: + return {"message": "Deal stage updated successfully"} + + return response.json() + except ApiException as e: + raise BrevoRequestError(f"Request to Brevo API failed: {e}") + + @check_api_key + def get_deals_by_pipeline(self, data: GetDealsByPipelineData): + try: + url = f"{self.BASE_URL}crm/deals" + params = { + "filters[attributes.pipeline]": data.pipeline_id, + "limit": data.limit, + } + response = self._session.get(url, params=params) + response.raise_for_status() + return response.json() + except ApiException as e: + raise BrevoRequestError(f"Request to Brevo API failed: {e}") + + @check_api_key + def get_all_pipelines(self): + try: + url = f"{self.BASE_URL}crm/pipeline/details/all" + response = self._session.get(url) + response.raise_for_status() + return response.json() + except ApiException as e: + raise BrevoRequestError(f"Request to Brevo API failed: {e}") + + @check_api_key + def get_pipeline_details(self, pipeline_id: str): + try: + url = f"{self.BASE_URL}crm/pipeline/details/{pipeline_id}" + response = self._session.get(url) + response.raise_for_status() + return response.json() + except ApiException as e: + raise BrevoRequestError(f"Request to Brevo API failed: {e}") + + @check_api_key + def get_stage_name(self, pipeline_id: str, stage_id: str): + pipeline_details = self.get_pipeline_details(pipeline_id) + + if isinstance(pipeline_details, list): + pipeline = next( + ( + item + for item in pipeline_details + if item.get("pipeline") == pipeline_id + ), + None, + ) + if not pipeline: + app.logger.warning( + f"Pipeline with ID {pipeline_id} not found." + ) + return None + else: + pipeline = pipeline_details + + stages = pipeline.get("stages", []) + for stage in stages: + if stage["id"] == stage_id: + return stage["name"] + + app.logger.warning( + f"Stage with ID {stage_id} not found in pipeline {pipeline_id}." + ) + return None + @staticmethod def remove_plus_sign(phone_number): if phone_number.startswith("+"): @@ -146,4 +258,4 @@ def remove_plus_sign(phone_number): except (ValueError, TypeError): raise ValueError("BREVO_COMPANY_SUBSCRIBE_LIST must be an integer") -brevo = BrevoApiClient(app.config[BREVO_API_KEY_ENV]) +brevo = BrevoApiClient(app.config["BREVO_API_KEY"]) diff --git a/app/models/business.py b/app/models/business.py index 2df815b6..237a6273 100644 --- a/app/models/business.py +++ b/app/models/business.py @@ -10,11 +10,17 @@ class TransportType(str, Enum): class BusinessType(str, Enum): + # TRM LONG_DISTANCE = "Longue distance" SHORT_DISTANCE = "Courte distance" SHIPPING = "Messagerie, Fonds et valeur" + # TRV FREQUENT = "Lignes régulières" INFREQUENT = "Occasionnels" + TAXI_GENERAL = "Taxi général" + TAXI_REGULATED = "Taxi conventionné" + VTC = "VTC" + LOTI = "LOTI" class Business(BaseModel): diff --git a/app/seed/__init__.py b/app/seed/__init__.py index c9fa3b5f..9e915523 100644 --- a/app/seed/__init__.py +++ b/app/seed/__init__.py @@ -88,7 +88,7 @@ def clean(): DELETE FROM team_vehicle; """ ) - + Email.query.delete() CompanyStats.query.delete() CompanyCertification.query.delete() CompanyKnownAddress.query.delete() @@ -99,7 +99,6 @@ def clean(): RefreshToken.query.delete() UserReadToken.query.delete() - Email.query.delete() RegulatoryAlert.query.delete() RegulationComputation.query.delete() diff --git a/app/seed/scenarios/__init__.py b/app/seed/scenarios/__init__.py index c8082e7b..7a80b893 100644 --- a/app/seed/scenarios/__init__.py +++ b/app/seed/scenarios/__init__.py @@ -12,6 +12,7 @@ run_scenario_busy_admin, ) from app.seed.scenarios.certificated_company import run_scenario_certificated +from app.seed.scenarios.run_certificate import scenario_run_certificate from app.seed.scenarios.temps_de_liaison import ( ADMIN_EMAIL as ADMIN_TEMPS_DE_LIAISON, EMPLOYEE_EMAIL as EMPLOYEE_TEMPS_DE_LIAISON, @@ -127,4 +128,10 @@ def run(self): [], run_scenario_controls, ), + SeedScenario( + "Certificate computation", + "Run certificate computation", + [], + scenario_run_certificate, + ), ] diff --git a/app/seed/scenarios/run_certificate.py b/app/seed/scenarios/run_certificate.py new file mode 100644 index 00000000..0aaaa19c --- /dev/null +++ b/app/seed/scenarios/run_certificate.py @@ -0,0 +1,7 @@ +from datetime import date + +from app.domain.certificate_criteria import compute_company_certifications + + +def scenario_run_certificate(): + compute_company_certifications(date.today()) diff --git a/app/services/sync_companies_with_brevo.py b/app/services/sync_companies_with_brevo.py new file mode 100644 index 00000000..3593e255 --- /dev/null +++ b/app/services/sync_companies_with_brevo.py @@ -0,0 +1,161 @@ +import logging +from app import db +from app.helpers.brevo import ( + BrevoApiClient, + UpdateDealStageData, + GetDealsByPipelineData, +) + +logger = logging.getLogger(__name__) + + +def sync_companies_with_brevo( + brevo: BrevoApiClient, pipeline_names: list, verbose=False +): + if verbose: + logger.setLevel(logging.DEBUG) + + for pipeline_name in pipeline_names: + try: + logger.debug(f"Syncing pipeline: {pipeline_name}") + + pipeline_id = get_pipeline_id_by_name(brevo, pipeline_name) + logger.debug(f"Pipeline ID: {pipeline_id}") + + stage_mapping = create_stage_mapping(brevo, pipeline_id) + logger.debug(f"Stage mapping: {stage_mapping}") + + db_companies = get_companies_from_db_via_function() + logger.debug(f"Companies from DB: {db_companies}") + + brevo_companies = get_companies_from_brevo(brevo, pipeline_id) + logger.debug(f"Companies from Brevo: {brevo_companies}") + + updates_needed = compare_companies( + db_companies, brevo_companies, stage_mapping + ) + logger.debug(f"Updates needed: {updates_needed}") + + update_companies_in_brevo( + brevo, updates_needed, stage_mapping, pipeline_id + ) + logger.debug(f"Update complete for pipeline: {pipeline_name}") + + except ValueError as e: + logger.error(f"Error syncing pipeline '{pipeline_name}': {e}") + + +def get_pipeline_id_by_name(brevo: BrevoApiClient, pipeline_name: str): + pipelines = brevo.get_all_pipelines() + for pipeline in pipelines: + if pipeline["pipeline_name"] == pipeline_name: + return pipeline["pipeline"] + raise ValueError(f"Pipeline '{pipeline_name}' not found.") + + +def create_stage_mapping(brevo: BrevoApiClient, pipeline_id: str) -> dict: + pipeline_details = brevo.get_pipeline_details(pipeline_id) + + if isinstance(pipeline_details, list): + pipeline_details = next( + (p for p in pipeline_details if p["pipeline"] == pipeline_id), None + ) + + if not pipeline_details: + raise ValueError(f"Pipeline with ID {pipeline_id} not found.") + + stage_mapping = { + stage["name"]: stage["id"] + for stage in pipeline_details.get("stages", []) + } + return stage_mapping + + +def get_companies_from_db_via_function(): + result = db.session.execute( + "SELECT * FROM get_companies_status()" + ).fetchall() + + companies = [ + { + "id": row[0], + "name": row[1], + "status": row[6], + } + for row in result + ] + return companies + + +def get_companies_from_brevo(brevo: BrevoApiClient, pipeline_id: str): + """ + Retrieve deals (companies) from Brevo using the pipeline ID. + """ + deals_data = brevo.get_deals_by_pipeline( + GetDealsByPipelineData(pipeline_id=pipeline_id, limit=100) + ) + companies_in_brevo = [ + { + "id": deal["id"], + "name": deal["attributes"].get("deal_name"), + "status": brevo.get_stage_name( + pipeline_id, deal["attributes"].get("deal_stage") + ), + } + for deal in deals_data.get("items", []) + ] + return companies_in_brevo + + +def normalize_status(status): + return status.strip().lower() + + +def compare_companies(db_companies, brevo_companies, stage_mapping): + updates_needed = [] + brevo_dict = {company["name"]: company for company in brevo_companies} + + for db_company in db_companies: + brevo_company = brevo_dict.get(db_company["name"]) + db_status = normalize_status(db_company["status"]) + brevo_status = normalize_status(brevo_company["status"]) + db_status_id = stage_mapping.get(db_status) + brevo_status_id = stage_mapping.get(brevo_status) + if db_status_id and db_status_id != brevo_status_id: + updates_needed.append( + { + "db_company_id": db_company["id"], + "brevo_deal_id": brevo_company["id"], + "new_status": db_company["status"], + "name": db_company["name"], + } + ) + return updates_needed + + +def update_companies_in_brevo( + brevo: BrevoApiClient, updates_needed, stage_mapping, pipeline_id: str +): + for update in updates_needed: + stage_id = stage_mapping.get(update["new_status"].lower()) + if stage_id: + update_data = UpdateDealStageData( + deal_id=update["brevo_deal_id"], + pipeline_id=pipeline_id, + stage_id=stage_id, + ) + try: + brevo.update_deal_stage(update_data) + company_name = update.get("name", "Unknown Company") + print( + f"Updated deal '{company_name}' (ID: {update['brevo_deal_id']}) " + f"to stage '{update['new_status']}' in Brevo." + ) + except Exception as e: + print( + f"Failed to update deal {update['brevo_deal_id']} to stage '{update['new_status']}' in Brevo. Error: {e}" + ) + else: + print( + f"Stage ID for status '{update['new_status']}' not found. Skipping update for deal {update['brevo_deal_id']}." + ) diff --git a/app/tests/regulations/test_deleting_mission_triggers_computation.py b/app/tests/regulations/test_deleting_mission_triggers_computation.py new file mode 100644 index 00000000..ae1246c9 --- /dev/null +++ b/app/tests/regulations/test_deleting_mission_triggers_computation.py @@ -0,0 +1,62 @@ +from datetime import datetime + +from app.helpers.submitter_type import SubmitterType +from app.models import RegulationComputation +from app.seed.helpers import get_datetime_tz +from app.tests.helpers import make_authenticated_request, ApiRequests +from app.tests.regulations import RegulationsTest + + +class TestDeletingMissionTriggersComputation(RegulationsTest): + def _get_computations_nb_for_date(self, date): + employee_computations = RegulationComputation.query.filter( + RegulationComputation.user == self.employee, + RegulationComputation.day == date, + RegulationComputation.submitter_type == SubmitterType.EMPLOYEE, + ).all() + admin_computations = RegulationComputation.query.filter( + RegulationComputation.user == self.employee, + RegulationComputation.day == date, + RegulationComputation.submitter_type == SubmitterType.ADMIN, + ).all() + return len(employee_computations), len(admin_computations) + + def test_admin_deletes_mission_triggers_computations(self): + # Employee logs a mission with too many work hours + long_mission = self._log_and_validate_mission( + mission_name="Long mission", + submitter=self.employee, + work_periods=[ + [ + get_datetime_tz(2024, 10, 7, 3, 0), # lundi + get_datetime_tz(2024, 10, 7, 22, 0), + ], + ], + ) + + ( + nb_employee_computations, + nb_admin_computations, + ) = self._get_computations_nb_for_date("2024-10-7") + self.assertEqual(nb_employee_computations, 1) + self.assertEqual(nb_admin_computations, 0) + + # Admin deletes this mission + make_authenticated_request( + time=datetime.now(), + submitter_id=self.admin.id, + query=ApiRequests.cancel_mission, + unexposed_query=False, + variables={ + "missionId": long_mission.id, + "userId": self.employee.id, + }, + ) + + # There should be one computation employee and admin + ( + nb_employee_computations, + nb_admin_computations, + ) = self._get_computations_nb_for_date("2024-10-7") + self.assertEqual(nb_employee_computations, 1) + self.assertEqual(nb_admin_computations, 1) diff --git a/app/tests/regulations/test_end_of_week.py b/app/tests/regulations/test_end_of_week.py new file mode 100644 index 00000000..e223a8f2 --- /dev/null +++ b/app/tests/regulations/test_end_of_week.py @@ -0,0 +1,28 @@ +from app.seed.helpers import ( + get_datetime_tz, +) +from app.tests.regulations import RegulationsTest + + +class TestEndOfWeek(RegulationsTest): + def test_logging_on_last_day_of_week_no_error(self): + self._log_and_validate_mission( + mission_name=f"Temps la semaine suivante", + submitter=self.employee, + work_periods=[ + [ + get_datetime_tz(2024, 9, 30, 18), # lundi + get_datetime_tz(2024, 9, 30, 20), + ], + ], + ) + self._log_and_validate_mission( + mission_name=f"Log fin de semaine", + submitter=self.employee, + work_periods=[ + [ + get_datetime_tz(2024, 9, 29, 18), # dimanche + get_datetime_tz(2024, 9, 29, 20), + ], + ], + ) diff --git a/config.py b/config.py index 035802e3..74242276 100644 --- a/config.py +++ b/config.py @@ -8,8 +8,6 @@ MOBILIC_ENV = os.environ.get("MOBILIC_ENV", "dev") -BREVO_API_KEY_ENV = "BREVO_API_KEY" - CGU_INITIAL_RELASE_DATE = datetime(2022, 1, 1) CGU_INITIAL_VERSION = "v1.0" @@ -31,7 +29,7 @@ class Config: FRONTEND_URL = os.environ.get("FRONTEND_URL") MAILJET_API_KEY = os.environ.get("MAILJET_API_KEY") MAILJET_API_SECRET = os.environ.get("MAILJET_API_SECRET") - BREVO_API_KEY = os.environ.get(BREVO_API_KEY_ENV) + BREVO_API_KEY = os.environ.get("BREVO_API_KEY") FC_CLIENT_ID = os.environ.get("FC_CLIENT_ID") FC_CLIENT_SECRET = os.environ.get("FC_CLIENT_SECRET") FC_URL = os.environ.get( @@ -79,6 +77,7 @@ class Config: "ENABLE_NEWSLETTER_SUBSCRIPTION", False ) APISPEC_FORMAT_RESPONSE = lambda x: x + LIVESTORM_API_TOKEN = os.environ.get("LIVESTORM_API_TOKEN", None) DISABLE_EMAIL = os.environ.get("DISABLE_EMAIL", False) CONTROL_SIGNING_KEY = os.environ.get("CONTROL_SIGNING_KEY") @@ -125,6 +124,7 @@ class DevConfig(Config): CELERY_BROKER_URL = os.environ.get( "CELERY_BROKER_URL", "redis://localhost:6379/0" ) + BREVO_API_KEY = os.environ.get("BREVO_API_KEY") class StagingConfig(Config): @@ -140,6 +140,9 @@ class TestConfig(Config): DISABLE_EMAIL = True CONTROL_SIGNING_KEY = "abc" CERTIFICATION_API_KEY = "1234" + BREVO_COMPANY_SUBSCRIBE_LIST = os.environ.get( + "BREVO_COMPANY_SUBSCRIBE_LIST", 22 + ) class ProdConfig(Config): diff --git a/cron.json b/cron.json index 27b2ef25..f7a729d3 100644 --- a/cron.json +++ b/cron.json @@ -9,6 +9,10 @@ }, { "command": "0 5 * * * flask load_company_stats" + }, + { + "command": "0 2 * * * flask sync_companies_with_brevo 'Test Dev churn'", + "size": "2XL" } ] } \ No newline at end of file diff --git a/migrations/versions/0d842a23583f_.py b/migrations/versions/0d842a23583f_.py new file mode 100644 index 00000000..0aa5e5c3 --- /dev/null +++ b/migrations/versions/0d842a23583f_.py @@ -0,0 +1,159 @@ +"""Add get_companies_status for sync_brevo + +Revision ID: 0d842a23583f +Revises: 83a7fd4629f0 +Create Date: 2024-10-15 11:36:03.770348 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "0d842a23583f" +down_revision = "83a7fd4629f0" +branch_labels = None +depends_on = None + + +def upgrade(): + + conn = op.get_bind() + conn.execute( + """ + BEGIN; + CREATE OR REPLACE FUNCTION get_companies_status() + RETURNS TABLE ( + company_id INT, + usual_name TEXT, + siren TEXT, + phone_number TEXT, + min_effective_range INT, + naf_code TEXT, + status TEXT, + status_date TEXT, + company_creation_date TEXT, + last_mission_reception_date TEXT, + mission_count_last_month INT, + admin_email TEXT, + admin_first_name TEXT, + admin_last_name TEXT + ) AS $$ + BEGIN + RETURN QUERY + WITH ranked_missions AS ( + SELECT + c.id AS company_id, + m.id AS mission_id, + m.reception_time AS mission_reception_time, + ROW_NUMBER() OVER (PARTITION BY c.id ORDER BY m.reception_time DESC) AS row_num + FROM + company_stats cs + JOIN company c ON c.id = cs.company_id + LEFT JOIN mission m ON c.id = m.company_id + ), + counts AS ( + SELECT + ranked_missions.company_id, + COUNT(*) AS mission_count_last_month + FROM + ranked_missions + WHERE + ranked_missions.mission_reception_time >= NOW() - INTERVAL '1 months' + GROUP BY + ranked_missions.company_id + ), + closest_admin_users AS ( + SELECT + rm.company_id, + u.id AS user_id, + u.email AS admin_email, + INITCAP(u.first_name) AS admin_first_name, + INITCAP(u.last_name) AS admin_last_name, + ABS(EXTRACT(epoch FROM ea.creation_time - c.creation_time)) AS difference_seconds, + ROW_NUMBER() OVER (PARTITION BY rm.company_id ORDER BY ABS(EXTRACT(epoch FROM ea.creation_time - c.creation_time))) AS row_num, + ea.has_admin_rights + FROM + "user" u + JOIN + employment ea ON u.id = ea.user_id + AND ea.has_admin_rights IS TRUE + AND ea.validation_status = 'approved' + AND ea.end_date IS NULL + AND ea.dismissed_at IS NULL + JOIN + ranked_missions rm ON ea.company_id = rm.company_id + JOIN + company c ON c.id = rm.company_id + ) + SELECT + c.id AS company_id, + c.usual_name::TEXT AS usual_name, + (CASE WHEN c.siren IS NULL THEN '0' ELSE c.siren END)::TEXT AS siren, + c.phone_number::TEXT AS phone_number, + CASE + WHEN (c.siren_api_info#>> array['uniteLegale', 'trancheEffectifsUniteLegale']::text[])::text = '00' THEN 0 + WHEN (c.siren_api_info#>> array['uniteLegale', 'trancheEffectifsUniteLegale']::text[])::text = '01' THEN 1 + WHEN (c.siren_api_info#>> array['uniteLegale', 'trancheEffectifsUniteLegale']::text[])::text = '02' THEN 3 + WHEN (c.siren_api_info#>> array['uniteLegale', 'trancheEffectifsUniteLegale']::text[])::text = '03' THEN 6 + WHEN (c.siren_api_info#>> array['uniteLegale', 'trancheEffectifsUniteLegale']::text[])::text = '11' THEN 10 + WHEN (c.siren_api_info#>> array['uniteLegale', 'trancheEffectifsUniteLegale']::text[])::text = '12' THEN 20 + WHEN (c.siren_api_info#>> array['uniteLegale', 'trancheEffectifsUniteLegale']::text[])::text = '21' THEN 50 + WHEN (c.siren_api_info#>> array['uniteLegale', 'trancheEffectifsUniteLegale']::text[])::text = '22' THEN 100 + WHEN (c.siren_api_info#>> array['uniteLegale', 'trancheEffectifsUniteLegale']::text[])::text = '31' THEN 200 + WHEN (c.siren_api_info#>> array['uniteLegale', 'trancheEffectifsUniteLegale']::text[])::text = '32' THEN 250 + WHEN (c.siren_api_info#>> array['uniteLegale', 'trancheEffectifsUniteLegale']::text[])::text = '41' THEN 500 + WHEN (c.siren_api_info#>> array['uniteLegale', 'trancheEffectifsUniteLegale']::text[])::text = '42' THEN 1000 + WHEN (c.siren_api_info#>> array['uniteLegale', 'trancheEffectifsUniteLegale']::text[])::text = '51' THEN 2000 + WHEN (c.siren_api_info#>> array['uniteLegale', 'trancheEffectifsUniteLegale']::text[])::text = '52' THEN 5000 + WHEN (c.siren_api_info#>> array['uniteLegale', 'trancheEffectifsUniteLegale']::text[])::text = '53' THEN 10000 + ELSE 0 + END AS min_effective_range, + COALESCE(c.siren_api_info#>> array['uniteLegale', 'activitePrincipaleUniteLegale']::text[], 'NA')::TEXT AS naf_code, + CASE + WHEN cs.first_certification_date IS NOT NULL THEN 'Certifiée' + WHEN cs.first_active_criteria_date IS NOT NULL THEN 'Activé' + WHEN cs.first_mission_validation_by_admin_date IS NOT NULL AND counts.mission_count_last_month >= 1 THEN + 'Onboardée et actif' + WHEN cs.first_mission_validation_by_admin_date IS NOT NULL AND counts.mission_count_last_month = 0 THEN + 'Onboardée et inactif' + WHEN cs.first_employee_invitation_date IS NOT NULL THEN 'Salarié invité' + ELSE 'Inscrite' + END AS status, + CASE + WHEN cs.first_certification_date IS NOT NULL THEN TO_CHAR(cs.first_certification_date, 'DD/MM/YYYY') + WHEN cs.first_active_criteria_date IS NOT NULL THEN TO_CHAR(cs.first_active_criteria_date, 'DD/MM/YYYY') + WHEN cs.first_mission_validation_by_admin_date IS NOT NULL THEN TO_CHAR(cs.first_mission_validation_by_admin_date, 'DD/MM/YYYY') + WHEN cs.first_employee_invitation_date IS NOT NULL THEN TO_CHAR(cs.first_employee_invitation_date, 'DD/MM/YYYY') + ELSE TO_CHAR(cs.creation_time, 'DD/MM/YYYY') + END AS status_date, + TO_CHAR(c.creation_time, 'DD/MM/YYYY') AS company_creation_date, + COALESCE(TO_CHAR(rm.mission_reception_time, 'DD/MM/YYYY'), 'NA') AS last_mission_reception_date, + COALESCE(counts.mission_count_last_month, 0)::int AS mission_count_last_month, + MAX(cau.admin_email)::TEXT AS admin_email, + MAX(cau.admin_first_name)::TEXT AS admin_first_name, + MAX(cau.admin_last_name)::TEXT AS admin_last_name + FROM + company c + LEFT JOIN company_stats cs ON c.id = cs.company_id + LEFT JOIN ranked_missions rm ON c.id = rm.company_id + LEFT JOIN counts ON c.id = counts.company_id + LEFT JOIN closest_admin_users cau ON rm.company_id = cau.company_id AND cau.row_num = 1 + WHERE + rm.row_num = 1 + GROUP BY + c.id, c.usual_name, counts.mission_count_last_month, cs.first_certification_date, cs.first_active_criteria_date, cs.first_employee_invitation_date, cs.creation_time, rm.mission_reception_time, cs.first_mission_validation_by_admin_date + ORDER BY + c.id; + END; + $$ LANGUAGE plpgsql STABLE; + COMMIT; + """ + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.execute("DROP FUNCTION get_companies_status()") + # ### end Alembic commands ### diff --git a/migrations/versions/bd643a8d5269_add_taxi_businesses.py b/migrations/versions/bd643a8d5269_add_taxi_businesses.py new file mode 100644 index 00000000..31459940 --- /dev/null +++ b/migrations/versions/bd643a8d5269_add_taxi_businesses.py @@ -0,0 +1,87 @@ +"""add taxi businesses + +Revision ID: bd643a8d5269 +Revises: 0d842a23583f +Create Date: 2024-10-22 12:03:34.022199 + +""" +from alembic import op +import sqlalchemy as sa + +from app.models.business import TransportType, BusinessType + +# revision identifiers, used by Alembic. +revision = "bd643a8d5269" +down_revision = "0d842a23583f" +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute("ALTER TABLE business DROP CONSTRAINT IF EXISTS businesstype") + op.alter_column( + "business", + "business_type", + type_=sa.Enum( + "Longue distance", + "Courte distance", + "Messagerie, Fonds et valeur", + "Lignes régulières", + "Occasionnels", + "Taxi général", + "Taxi conventionné", + "VTC", + "LOTI", + name="businesstype", + native_enum=False, + ), + nullable=False, + ) + + data = [ + { + "id": 6, + "transport_type": TransportType.TRV, + "business_type": BusinessType.TAXI_GENERAL, + }, + { + "id": 7, + "transport_type": TransportType.TRV, + "business_type": BusinessType.TAXI_REGULATED, + }, + { + "id": 8, + "transport_type": TransportType.TRV, + "business_type": BusinessType.VTC, + }, + { + "id": 9, + "transport_type": TransportType.TRV, + "business_type": BusinessType.LOTI, + }, + ] + + connection = op.get_bind() + connection.execute( + sa.insert( + sa.Table("business", sa.MetaData(), autoload_with=connection) + ).values(data) + ) + + +def downgrade(): + op.execute("ALTER TABLE business DROP CONSTRAINT IF EXISTS businesstype") + op.alter_column( + "business", + "business_type", + type_=sa.Enum( + "Longue distance", + "Courte distance", + "Messagerie, Fonds et valeur", + "Lignes régulières", + "Occasionnels", + name="businesstype", + native_enum=False, + ), + nullable=False, + )