Skip to content

Api reference

components.clinic.public.business_logic

gdpr_compliance

CLINIC_COUNTRY_APP_NAMES module-attribute

CLINIC_COUNTRY_APP_NAMES = [ALAN_FR]

CLINIC_RETENTION_PERIOD_IN_YEARS module-attribute

CLINIC_RETENTION_PERIOD_IN_YEARS = 2

delete_member_data

delete_member_data(global_profile_id)

Delete all clinic data related to a specific member.

Source code in components/clinic/public/business_logic/gdpr_compliance.py
def delete_member_data(global_profile_id: UUID) -> None:
    """Delete all clinic data related to a specific member."""
    from components.clinic.internal.business_logic.clinic_user import (
        delete_all_clinic_data_for_user,
    )

    is_deletion_enabled = bool_feature_flag(
        feature_flag_key="clinic-gdpr-member-data-deletion-killswitch",
        default_value=False,
    )

    current_logger.info(
        "Starting clinic member data deletion",
        global_profile_id=global_profile_id,
        is_deletion_enabled=is_deletion_enabled,
    )

    for app_name in CLINIC_COUNTRY_APP_NAMES:
        feature_user_to_delete = _get_app_feature_user_from_profile_id(
            global_profile_id, app_name
        )

        if not feature_user_to_delete:
            current_logger.info(
                "No feature user found, skipping",
                global_profile_id=global_profile_id,
                app_name=app_name.value,
            )
            continue

        try:
            clinic_user = get_existing_clinic_user_for_feature_user(
                app_user=feature_user_to_delete
            )
        except BaseErrorCode:
            current_logger.info(
                "No clinic user found, skipping",
                global_profile_id=global_profile_id,
                app_id=feature_user_to_delete.app_id,
                app_user_id=feature_user_to_delete.app_user_id,
            )
            continue

        action = "Deleting" if is_deletion_enabled else "Would have deleted"
        current_logger.info(
            f"{action} clinic data for user",
            global_profile_id=global_profile_id,
            clinic_user_id=clinic_user.id,
            is_deletion_enabled=is_deletion_enabled,
        )

        if is_deletion_enabled:
            delete_all_clinic_data_for_user(
                clinic_user_id=clinic_user.id, dry_run=False
            )

    current_logger.info(
        "Completed clinic member data deletion",
        global_profile_id=global_profile_id,
        is_deletion_enabled=is_deletion_enabled,
    )

get_profiles_ready_for_deletion

get_profiles_ready_for_deletion()

Return a list of inactive global profile IDs that are ready for deletion. We use global profile IDs, because user IDs are not unique between apps (FR/ES/BE)

in the clinic, we define a member as "ready for deletion" in https://github.com/alan-eu/Topics/discussions/31845?sort=old#discussioncomment-15755603 ⧉

The summary of the properties we are interested in: +----------------------------+-----------------------------------------------+-----------------------------------------------+ | Entity Name | Properties Checked | Rationale for Properties Checked | +============================+===============================================+===============================================+ | Medical Conversations | created_at (time of last medical conversation | The user sending a message clearly denotes an | | | part sent by user) | intentional interaction with our health | | | | services. | +----------------------------+-----------------------------------------------+-----------------------------------------------+ | Therapist Booking Sessions | ends_at (time of most recent therapist booking | A session is a clear intention of interaction | | | session where the user was the attending member | with our health services; the time it ends is | | | (clinic_user field)) | when that interaction ended. Sometimes followed| | | | by a medical conversation (covered above). | +----------------------------+-----------------------------------------------+-----------------------------------------------+ | Health Programs | last_updated_at (time of most recent health | This property is updated with any interaction | | | program progress created by the user; denoted by| with a health program. Some interactions may | | | app_id and user_id) | not truly be “program” interactions (e.g., a | | | | rating), but we don’t have a better way to | | | | differentiate between actions, and this catches| | | | them all. | +----------------------------+-----------------------------------------------+-----------------------------------------------+

Returns:

Type Description
list[UUID]

List[uuid.UUID]: List of global profile IDs ready for deletion

Source code in components/clinic/public/business_logic/gdpr_compliance.py
@tracer.wrap(service="clinic")
def get_profiles_ready_for_deletion() -> list[UUID]:
    """
    Return a list of inactive global profile IDs that are ready for deletion.
    We use global profile IDs, because user IDs are not unique between apps (FR/ES/BE)

    in the clinic, we define a member as "ready for deletion" in https://github.com/alan-eu/Topics/discussions/31845?sort=old#discussioncomment-15755603

    The summary of the properties we are interested in:
    +----------------------------+-----------------------------------------------+-----------------------------------------------+
    | Entity Name                | Properties Checked                              | Rationale for Properties Checked               |
    +============================+===============================================+===============================================+
    | Medical Conversations      | created_at (time of last medical conversation   | The user sending a message clearly denotes an  |
    |                            | part sent by user)                              | intentional interaction with our health        |
    |                            |                                               | services.                                     |
    +----------------------------+-----------------------------------------------+-----------------------------------------------+
    | Therapist Booking Sessions | ends_at (time of most recent therapist booking  | A session is a clear intention of interaction  |
    |                            | session where the user was the attending member | with our health services; the time it ends is  |
    |                            | (clinic_user field))                            | when that interaction ended. Sometimes followed|
    |                            |                                               | by a medical conversation (covered above).     |
    +----------------------------+-----------------------------------------------+-----------------------------------------------+
    | Health Programs            | last_updated_at (time of most recent health     | This property is updated with any interaction  |
    |                            | program progress created by the user; denoted by| with a health program. Some interactions may   |
    |                            | app_id and user_id)                             | not truly be “program” interactions (e.g., a   |
    |                            |                                               | rating), but we don’t have a better way to     |
    |                            |                                               | differentiate between actions, and this catches|
    |                            |                                               | them all.                                      |
    +----------------------------+-----------------------------------------------+-----------------------------------------------+

    Returns:
        List[uuid.UUID]: List of global profile IDs ready for deletion
    """
    from components.clinic.public.dependencies import get_app_dependency

    all_candidate_profile_ids: list[UUID] = []
    profile_to_feature_user: dict[UUID, FeatureUser] = {}
    candidate_inactive_feature_users: list[FeatureUser] = []

    for app_name in CLINIC_COUNTRY_APP_NAMES:
        # collect candidate profiles and their feature user per country
        # each profile has at most 1 feature user per country
        adapter = get_app_dependency(app_name.value)
        current_logger.info("fetching inactive profile IDs", app_name=app_name.value)
        inactive_profile_ids = adapter.get_inactive_profile_ids_since(
            period_in_years=CLINIC_RETENTION_PERIOD_IN_YEARS
        )
        current_logger.info(
            "fetched inactive profile IDs",
            app_name=app_name.value,
            count=len(inactive_profile_ids),
        )
        all_candidate_profile_ids.extend(inactive_profile_ids)

        current_logger.info(
            "resolving profile IDs to feature users",
            app_name=app_name.value,
            count=len(inactive_profile_ids),
        )
        for inactive_profile_id in inactive_profile_ids:
            inactive_feature_user = _get_app_feature_user_from_profile_id(
                inactive_profile_id, app_name
            )

            if inactive_feature_user:
                profile_to_feature_user[inactive_profile_id] = inactive_feature_user
                candidate_inactive_feature_users.append(inactive_feature_user)

        current_logger.info(
            "resolved feature users",
            app_name=app_name.value,
            resolved=len(profile_to_feature_user),
        )

    current_logger.info(
        "total candidate profiles", count=len(all_candidate_profile_ids)
    )

    # batch get existing clinic users (no creation)
    current_logger.info("fetching existing clinic users")
    existing_clinic_users = get_existing_clinic_users_for_feature_users(
        candidate_inactive_feature_users
    )
    current_logger.info(
        "fetched existing clinic users", count=len(existing_clinic_users)
    )

    # resolve profile_id -> clinic_user_id directly
    feature_user_key_to_clinic_user_id = {
        (clinic_user.app_id, clinic_user.app_user_id): clinic_user.id
        for clinic_user in existing_clinic_users
    }
    inactive_profile_to_clinic_user_id = {
        profile_id: feature_user_key_to_clinic_user_id.get(
            (feature_user.app_id, feature_user.app_user_id)
        )
        for profile_id, feature_user in profile_to_feature_user.items()
    }

    candidates_without_clinic_user = len(all_candidate_profile_ids) - len(
        feature_user_key_to_clinic_user_id
    )
    current_logger.info(
        "candidates without clinic user", count=candidates_without_clinic_user
    )

    # batch check for recent interactions
    potentially_inactive_clinic_user_ids = [
        clinic_user_id
        for clinic_user_id in inactive_profile_to_clinic_user_id.values()
        if clinic_user_id is not None
    ]

    current_logger.info(
        "checking recent interactions",
        clinic_user_count=len(potentially_inactive_clinic_user_ids),
    )

    # union of all recent interaction sources
    active_via_conversations = (
        get_subset_of_clinic_user_ids_with_recent_medical_conversation_interactions(
            potentially_inactive_clinic_user_ids
        )
    )
    current_logger.info("active via conversations", count=len(active_via_conversations))

    active_via_sessions = (
        get_subset_of_clinic_user_ids_with_recent_session_interactions(
            potentially_inactive_clinic_user_ids
        )
    )
    current_logger.info("active via sessions", count=len(active_via_sessions))

    active_via_health_programs = (
        get_subset_of_clinic_user_ids_with_recent_health_program_interactions(
            candidate_inactive_feature_users, feature_user_key_to_clinic_user_id
        )
    )
    current_logger.info(
        "active via health programs", count=len(active_via_health_programs)
    )

    active_clinic_user_ids = (
        active_via_conversations | active_via_sessions | active_via_health_programs
    )
    current_logger.info(
        "total active clinic users (union)", count=len(active_clinic_user_ids)
    )

    # disqualify profiles with any recent interaction
    result = [
        profile_id
        for profile_id in all_candidate_profile_ids
        if inactive_profile_to_clinic_user_id.get(profile_id)
        not in active_clinic_user_ids
    ]

    current_logger.info("profiles ready for deletion", count=len(result))
    return result

get_subset_of_clinic_user_ids_with_recent_health_program_interactions

get_subset_of_clinic_user_ids_with_recent_health_program_interactions(
    feature_users, feature_user_key_to_clinic_user_id
)

Return the subset of clinic user IDs whose corresponding feature users have had health program interactions in the last CLINIC_RETENTION_PERIOD_IN_YEARS years.

Checks the last_updated_at time of the most recent health program progress for each feature user, then maps active feature users back to clinic user IDs.

Parameters:

Name Type Description Default
feature_users list[FeatureUser]

List of feature users to check

required
feature_user_key_to_clinic_user_id dict[tuple[str, str], UUID]

Mapping of (app_id, app_user_id) to clinic user ID

required
Source code in components/clinic/public/business_logic/gdpr_compliance.py
@tracer.wrap(service="clinic")
def get_subset_of_clinic_user_ids_with_recent_health_program_interactions(
    feature_users: list[FeatureUser],
    feature_user_key_to_clinic_user_id: dict[tuple[str, str], UUID],
) -> set[UUID]:
    """
    Return the subset of clinic user IDs whose corresponding feature users have had
    health program interactions in the last CLINIC_RETENTION_PERIOD_IN_YEARS years.

    Checks the last_updated_at time of the most recent health program progress for each feature user,
    then maps active feature users back to clinic user IDs.

    Args:
        feature_users: List of feature users to check
        feature_user_key_to_clinic_user_id: Mapping of (app_id, app_user_id) to clinic user ID
    """
    if not feature_users:
        return set()

    cutoff = datetime.now(tz=UTC) - relativedelta(
        years=CLINIC_RETENTION_PERIOD_IN_YEARS
    )
    active_health_programs_feature_users = (
        get_feature_users_with_health_program_interaction_after(feature_users, cutoff)
    )

    active_clinic_user_ids: set[UUID] = set()
    for feature_user in active_health_programs_feature_users:
        clinic_user_id = feature_user_key_to_clinic_user_id.get(
            (feature_user.app_id, feature_user.app_user_id)
        )
        if clinic_user_id is not None:
            active_clinic_user_ids.add(clinic_user_id)

    return active_clinic_user_ids

get_subset_of_clinic_user_ids_with_recent_medical_conversation_interactions

get_subset_of_clinic_user_ids_with_recent_medical_conversation_interactions(
    clinic_user_ids,
)

Return clinic user IDs that had medical conversation interactions within retention period.

Uses batched IN clauses to avoid overwhelming PostgreSQL with large queries.

Parameters:

Name Type Description Default
clinic_user_ids list[UUID]

List of clinic user IDs to check

required
Source code in components/clinic/public/business_logic/gdpr_compliance.py
@tracer.wrap(service="clinic")
def get_subset_of_clinic_user_ids_with_recent_medical_conversation_interactions(
    clinic_user_ids: list[UUID],
) -> set[UUID]:
    """Return clinic user IDs that had medical conversation interactions within retention period.

    Uses batched IN clauses to avoid overwhelming PostgreSQL with large queries.

    Args:
        clinic_user_ids: List of clinic user IDs to check
    """
    if not clinic_user_ids:
        return set()

    cutoff = datetime.now() - relativedelta(years=CLINIC_RETENTION_PERIOD_IN_YEARS)
    total_batches = math.ceil(len(clinic_user_ids) / _BATCH_SIZE)
    current_logger.info(
        "checking recent conversations in batches",
        total=len(clinic_user_ids),
        total_batches=total_batches,
    )

    result: set[UUID] = set()
    for batch_idx in range(total_batches):
        start = batch_idx * _BATCH_SIZE
        batch = clinic_user_ids[start : start + _BATCH_SIZE]

        rows = (
            current_session.execute(
                select(MedicalConversationPart.clinic_user_author_id)
                .where(
                    MedicalConversationPart.clinic_user_author_id.in_(batch),
                    MedicalConversationPart.created_at > cutoff,
                )
                .distinct()
            )
            .scalars()
            .all()
        )

        batch_result = {uid for uid in rows if uid is not None}
        result.update(batch_result)
        current_logger.info(
            "checked conversations batch",
            batch=batch_idx + 1,
            total_batches=total_batches,
            batch_results=len(batch_result),
        )

    return result

get_subset_of_clinic_user_ids_with_recent_session_interactions

get_subset_of_clinic_user_ids_with_recent_session_interactions(
    clinic_user_ids,
)

Return clinic user IDs that had therapist booking sessions within retention period.

Uses batched IN clauses to avoid overwhelming PostgreSQL with large queries.

Parameters:

Name Type Description Default
clinic_user_ids list[UUID]

List of clinic user IDs to check

required
Source code in components/clinic/public/business_logic/gdpr_compliance.py
@tracer.wrap(service="clinic")
def get_subset_of_clinic_user_ids_with_recent_session_interactions(
    clinic_user_ids: list[UUID],
) -> set[UUID]:
    """Return clinic user IDs that had therapist booking sessions within retention period.

    Uses batched IN clauses to avoid overwhelming PostgreSQL with large queries.

    Args:
        clinic_user_ids: List of clinic user IDs to check
    """
    if not clinic_user_ids:
        return set()

    cutoff = datetime.now() - relativedelta(years=CLINIC_RETENTION_PERIOD_IN_YEARS)
    total_batches = math.ceil(len(clinic_user_ids) / _BATCH_SIZE)
    current_logger.info(
        "checking recent sessions in batches",
        total=len(clinic_user_ids),
        total_batches=total_batches,
    )

    result: set[UUID] = set()
    for batch_idx in range(total_batches):
        start = batch_idx * _BATCH_SIZE
        batch = clinic_user_ids[start : start + _BATCH_SIZE]

        rows = (
            current_session.execute(
                select(TherapistBookingSession.clinic_user_id)
                .where(
                    TherapistBookingSession.clinic_user_id.isnot(None),
                    TherapistBookingSession.clinic_user_id.in_(batch),
                    TherapistBookingSession.ends_at > cutoff,
                )
                .distinct()
            )
            .scalars()
            .all()
        )

        result.update(rows)
        current_logger.info(
            "checked sessions batch",
            batch=batch_idx + 1,
            total_batches=total_batches,
            batch_results=len(rows),
        )

    return result

get_user_ids_ready_for_deletion

get_user_ids_ready_for_deletion()

Return inactive feature users ready for deletion, bypassing global profiles.

Same retention logic as get_profiles_ready_for_deletion but operates entirely with app user IDs, avoiding the N+1 profile→feature_user resolution.

Returns:

Type Description
list[FeatureUser]

Feature users whose clinic data is ready for deletion.

Source code in components/clinic/public/business_logic/gdpr_compliance.py
@tracer.wrap(service="clinic")
def get_user_ids_ready_for_deletion() -> list[FeatureUser]:
    """Return inactive feature users ready for deletion, bypassing global profiles.

    Same retention logic as get_profiles_ready_for_deletion but operates entirely
    with app user IDs, avoiding the N+1 profile→feature_user resolution.

    Returns:
        Feature users whose clinic data is ready for deletion.
    """
    from components.clinic.public.dependencies import get_app_dependency

    all_candidates: list[FeatureUser] = []

    for app_name in CLINIC_COUNTRY_APP_NAMES:
        adapter = get_app_dependency(app_name.value)
        current_logger.info("fetching inactive user IDs", app_name=app_name.value)
        inactive_user_ids = adapter.get_inactive_user_ids_since(
            period_in_years=CLINIC_RETENTION_PERIOD_IN_YEARS
        )
        current_logger.info(
            "fetched inactive user IDs",
            app_name=app_name.value,
            count=len(inactive_user_ids),
        )
        all_candidates.extend(
            FeatureUser(app_id=app_name.value, app_user_id=uid)
            for uid in inactive_user_ids
        )

    current_logger.info("total candidate feature users", count=len(all_candidates))

    # batch get existing clinic users (no creation)
    current_logger.info("fetching existing clinic users")
    existing_clinic_users = get_existing_clinic_users_for_feature_users(all_candidates)
    current_logger.info(
        "fetched existing clinic users", count=len(existing_clinic_users)
    )

    feature_user_key_to_clinic_user_id: dict[tuple[str, str], UUID] = {
        (clinic_user.app_id, clinic_user.app_user_id): clinic_user.id
        for clinic_user in existing_clinic_users
    }

    candidates_without_clinic_user = len(all_candidates) - len(
        feature_user_key_to_clinic_user_id
    )
    current_logger.info(
        "candidates without clinic user", count=candidates_without_clinic_user
    )

    potentially_inactive_clinic_user_ids = [
        clinic_user_id
        for clinic_user_id in feature_user_key_to_clinic_user_id.values()
        if clinic_user_id is not None
    ]

    current_logger.info(
        "checking recent interactions",
        clinic_user_count=len(potentially_inactive_clinic_user_ids),
    )

    # union of all recent interaction sources
    active_via_conversations = (
        get_subset_of_clinic_user_ids_with_recent_medical_conversation_interactions(
            potentially_inactive_clinic_user_ids
        )
    )
    current_logger.info("active via conversations", count=len(active_via_conversations))

    active_via_sessions = (
        get_subset_of_clinic_user_ids_with_recent_session_interactions(
            potentially_inactive_clinic_user_ids
        )
    )
    current_logger.info("active via sessions", count=len(active_via_sessions))

    active_via_health_programs = (
        get_subset_of_clinic_user_ids_with_recent_health_program_interactions(
            all_candidates, feature_user_key_to_clinic_user_id
        )
    )
    current_logger.info(
        "active via health programs", count=len(active_via_health_programs)
    )

    active_clinic_user_ids = (
        active_via_conversations | active_via_sessions | active_via_health_programs
    )
    current_logger.info(
        "total active clinic users (union)", count=len(active_clinic_user_ids)
    )

    # disqualify feature users with any recent interaction
    result = [
        feature_user
        for feature_user in all_candidates
        if feature_user_key_to_clinic_user_id.get(
            (feature_user.app_id, feature_user.app_user_id)
        )
        not in active_clinic_user_ids
    ]

    current_logger.info("feature users ready for deletion", count=len(result))
    return result

insi_identity

is_identity_verified_for_user

is_identity_verified_for_user(app_user)

Check if the user's identity is verified.

Parameters:

Name Type Description Default
app_user FeatureUser

The application user whose identity is to be checked.

required

Returns: bool: True if the user's identity is verified, False otherwise.

Source code in components/clinic/public/business_logic/insi_identity.py
def is_identity_verified_for_user(app_user: FeatureUser) -> bool:
    """
    Check if the user's identity is verified.

    Args:
        app_user (FeatureUser): The application user whose identity is to be checked.
    Returns:
        bool: True if the user's identity is verified, False otherwise.
    """
    clinic_user = get_or_create_clinic_user(app_user=app_user)

    return is_identity_validated(clinic_user_id=clinic_user.id)

medical_conversation

get_first_medical_conversation_for_member

get_first_medical_conversation_for_member(app_user)

Retrieves the first medical conversation for a given member.

This function acts as a proxy to the internal business logic function get_first_medical_conversation_for_member to fetch the first medical conversation associated with the provided app_user.

Parameters:

Name Type Description Default
app_user FeatureUser

The user for whom the first medical conversation is to be retrieved.

required

Returns:

Type Description
Optional[MedicalConversation]

Optional[MedicalConversation]: The first medical conversation for the given member, or None if no conversation is found.

Source code in components/clinic/public/business_logic/medical_conversation.py
def get_first_medical_conversation_for_member(
    app_user: FeatureUser,
) -> Optional[MedicalConversation]:
    """
    Retrieves the first medical conversation for a given member.

    This function acts as a proxy to the internal business logic function
    `get_first_medical_conversation_for_member` to fetch the first medical
    conversation associated with the provided `app_user`.

    Args:
        app_user (FeatureUser): The user for whom the first medical conversation
                                is to be retrieved.

    Returns:
        Optional[MedicalConversation]: The first medical conversation for the
                                       given member, or None if no conversation
                                       is found.
    """
    from components.clinic.internal.business_logic.medical_conversation import (
        get_first_medical_conversation_for_member,
    )

    return get_first_medical_conversation_for_member(app_user=app_user)

mark_conversation_as_deleted

mark_conversation_as_deleted(medical_conversation_id)
Source code in components/clinic/public/business_logic/medical_conversation.py
def mark_conversation_as_deleted(medical_conversation_id: UUID) -> None:  # noqa: D103
    from components.clinic.internal.business_logic.medical_chat_bot import (
        get_medical_chat_bot_clinic_user_id,
    )
    from components.clinic.internal.business_logic.medical_conversation import (
        mark_as_deleted,
    )

    mark_as_deleted(
        medical_conversation_id=medical_conversation_id,
        actor_clinic_user_id=get_medical_chat_bot_clinic_user_id(),
    )

therapist_booking_session

get_past_sessions_that_need_invoice_generation_count_for_user

get_past_sessions_that_need_invoice_generation_count_for_user(
    feature_user, session_type
)

Counts the number of past reimbursed therapist booking sessions waiting for invoicing for a specific user and session type. sessions that are waiting for invoicing are those that are not refunded, are confirmed, and have a valid credit. They ended between now and 2 days ago. (default_delay_in_hours is 48)

Arguments: feature_user (FeatureUser): The user for whom upcoming sessions need to be counted. session_type (TherapistBookingSessionType): The type of therapist session to filter by.

Returns: int: The count of done reimbursed therapist booking sessions matching the provided user and session type.

Source code in components/clinic/public/business_logic/therapist_booking_session.py
def get_past_sessions_that_need_invoice_generation_count_for_user(
    feature_user: FeatureUser, session_type: TherapistBookingSessionType
) -> int:
    """
    Counts the number of past reimbursed therapist booking sessions waiting for
    invoicing for a specific user and session type.
    sessions that are waiting for invoicing are those that are not refunded, are
    confirmed, and have a valid credit. They ended between now and 2 days ago. (default_delay_in_hours is 48)

    Arguments:
    feature_user (FeatureUser): The user for whom upcoming sessions need
    to be counted.
    session_type (TherapistBookingSessionType): The type of therapist session
    to filter by.

    Returns:
    int: The count of done reimbursed therapist booking sessions matching the
    provided user and session type.
    """
    clinic_user = get_or_create_clinic_user(app_user=feature_user)

    return int(
        get_past_sessions_that_need_invoices_query(
            clinic_user_id=clinic_user.id,
            session_type=session_type,
            min_date=(datetime.now() - timedelta(hours=DEFAULT_DELAY_IN_HOURS)),
            delay_in_hours=0,
        ).count()
    )

get_upcoming_reimbursed_therapist_booking_session_count_for_user

get_upcoming_reimbursed_therapist_booking_session_count_for_user(
    feature_user, session_type
)

Counts the number of upcoming reimbursed therapist booking sessions for a specific user and session type. Only counts sessions that are not refunded, are confirmed, have a valid credit, and are scheduled for a future date.

Arguments: feature_user (FeatureUser): The user for whom upcoming sessions need to be counted. session_type (TherapistBookingSessionType): The type of therapist session to filter by.

Returns: int: The count of upcoming reimbursed therapist booking sessions matching the provided user and session type.

Source code in components/clinic/public/business_logic/therapist_booking_session.py
def get_upcoming_reimbursed_therapist_booking_session_count_for_user(
    feature_user: FeatureUser, session_type: TherapistBookingSessionType
) -> int:
    """
    Counts the number of upcoming reimbursed therapist booking sessions for a
    specific user and session type. Only counts sessions that are not refunded,
    are confirmed, have a valid credit, and are scheduled for a future date.

    Arguments:
    feature_user (FeatureUser): The user for whom upcoming sessions need
    to be counted.
    session_type (TherapistBookingSessionType): The type of therapist session
    to filter by.

    Returns:
    int: The count of upcoming reimbursed therapist booking sessions matching
    the provided user and session type.
    """
    from components.clinic.internal.booking.models.therapist_booking_session import (
        TherapistBookingSession,
    )

    clinic_user = get_or_create_clinic_user(app_user=feature_user)

    return int(
        current_session.query(TherapistBookingSession)  # noqa: ALN085
        .filter(
            ~TherapistBookingSession.is_refunded,
            TherapistBookingSession.confirmed_at.isnot(None),
            TherapistBookingSession.clinic_user_id == clinic_user.id,
            TherapistBookingSession.session_type == session_type,
            TherapistBookingSession.starts_at > datetime.now(),
            TherapistBookingSession.therapist_booking_user_credit_id.isnot(None),
        )
        .count()
    )

user

app_user_has_upcoming_availability_for_alan_consultations

app_user_has_upcoming_availability_for_alan_consultations(
    member_app_user,
    locale=None,
    availability_threshold_in_hours=24,
    medical_admins_ids=None,
)

Returns true if the given member is able to book an Alan consultation within the following upcoming parameters: - next availability is within the next availability threshold in hours (24 hours by default)

Source code in components/clinic/public/business_logic/user.py
def app_user_has_upcoming_availability_for_alan_consultations(
    member_app_user: FeatureUser,
    locale: Optional[str] = None,
    availability_threshold_in_hours: Optional[int] = 24,
    medical_admins_ids: Optional[
        list[str]
    ] = None,  # if this argument is passed, we filter the medical admins to only choose those with an id in this list
) -> bool:
    """
    Returns true if the given member is able to book an Alan consultation within the following upcoming parameters:
    - next availability is within the next availability threshold in hours (24 hours by default)
    """
    from components.clinic.internal.business_logic.medical_admin import (
        get_medical_admins_for_member,
    )

    # general practitioners that are bookable online are the only ones that do Alan consultations
    available_medical_admins = get_medical_admins_for_member(
        app_user=member_app_user,
        is_bookable_online=True,
        locale=locale,
        medical_specialties=[MedicalAdminSpecialty.GENERAL_PRACTITIONER],
        medical_admins_ids=medical_admins_ids,
        availability_threshold_in_hours=availability_threshold_in_hours,
    )

    # since we applied the availability threshold filter, if there are no available medical admins, it means that the member is not able to book an Alan consultation within the given upcoming availability
    return len(available_medical_admins) > 0

get_clinic_user_for_french_user

get_clinic_user_for_french_user(user_id)
Source code in components/clinic/public/business_logic/user.py
def get_clinic_user_for_french_user(user_id: int) -> ClinicUser | None:  # noqa: D103
    from components.clinic.internal.business_logic.clinic_user import (
        get_clinic_user_for_french_user,
    )

    return get_clinic_user_for_french_user(user_id)

get_medical_admin_for_user

get_medical_admin_for_user(user_id, not_deleted=True)
Source code in components/clinic/public/business_logic/user.py
def get_medical_admin_for_user(  # noqa: D103
    user_id: int, not_deleted: Optional[bool] = True
) -> MedicalAdmin | None:
    from components.clinic.internal.business_logic.medical_admin import (
        get_medical_admin_by_app_user,
    )

    return get_medical_admin_by_app_user(
        medical_admin_app_user=FeatureUser(
            app_user_id=str(user_id),
            app_id=get_current_app_name(),
        ),
        not_deleted=not_deleted,
    )

has_app_user_access_to_orientation

has_app_user_access_to_orientation(app_user)
Source code in components/clinic/public/business_logic/user.py
def has_app_user_access_to_orientation(app_user: FeatureUser) -> bool:  # noqa: D103
    from components.clinic.internal.booking.business_logic.sessions_queries import (
        has_app_user_access_to_orientation,
    )

    return has_app_user_access_to_orientation(app_user=app_user)
update_clinic_consent_for_user(
    actor_user, has_given_consent, delete_existing_data=True
)

Update the clinic user consent

Parameters:

Name Type Description Default
actor_user FeatureUser

The actor feature user

required
has_given_consent bool

The user has given his consent

required
delete_existing_data Optional[bool]

Has to delete existing data status

True
Source code in components/clinic/public/business_logic/user.py
def update_clinic_consent_for_user(
    actor_user: FeatureUser,
    has_given_consent: bool,
    delete_existing_data: Optional[bool] = True,
) -> None:
    """
    Update the clinic user consent

    Args:
        actor_user: The actor feature user
        has_given_consent: The user has given his consent
        delete_existing_data: Has to delete existing data status
    """
    from components.clinic.internal.business_logic.clinic_user import (
        update_clinic_consent,
    )

    update_clinic_consent(
        actor_app_user=actor_user,
        has_given_consent=has_given_consent,
        delete_existing_data=delete_existing_data,
    )

validate_child_dependent

validate_child_dependent_and_get_feature_user

validate_child_dependent_and_get_feature_user(
    parent_user, child_app_user_id
)

Validate that a child is a dependent of the current user and return a FeatureUser for the child.

Parameters:

Name Type Description Default
parent_user FeatureUser

The current user (parent)

required
child_app_user_id str

The ID of the child to validate

required
Source code in components/clinic/public/business_logic/validate_child_dependent.py
def validate_child_dependent_and_get_feature_user(
    parent_user: FeatureUser, child_app_user_id: str
) -> FeatureUser:
    """
    Validate that a child is a dependent of the current user and return a FeatureUser for the child.

    Args:
        parent_user: The current user (parent)
        child_app_user_id: The ID of the child to validate

    Raises:
        BaseErrorCode.forbidden if user has no dependents or child is not a dependent of current user
    """
    # Get the app user data of the current user
    user_data = get_app_dependency(parent_user.app_id).get_app_user_data(
        app_user_id=parent_user.app_user_id
    )

    if not user_data.dependents:
        raise BaseErrorCode.forbidden(message="User has no dependents")

    # Get the list of dependent app_user_ids
    dependent_ids = [
        dependent.app_user_id
        for dependent in user_data.dependents
        if dependent.app_user_id
    ]

    if child_app_user_id not in dependent_ids:
        raise BaseErrorCode.forbidden(
            message="Child is not a dependent of the current user"
        )

    # Create and return feature user for the child
    return FeatureUser(
        app_user_id=child_app_user_id,
        app_id=parent_user.app_id,
    )

components.clinic.public.commands

app_group

clinic_commands module-attribute

clinic_commands = AppGroup('clinic')

register_clinic_commands

register_clinic_commands()
Source code in components/clinic/public/commands/app_group.py
def register_clinic_commands() -> None:  # noqa: D103
    from components.clinic.public.commands import (  # noqa: F401
        clinic_invoices,
        end_to_end_test,
        gdpr_compliance,
        medical_admins,
        medical_conversation,
        unread_message_reminders,
        user,
    )

booking

app_group

booking_commands module-attribute
booking_commands = AppGroup('booking')
register_booking_commands
register_booking_commands()
Source code in components/clinic/public/commands/booking/app_group.py
def register_booking_commands() -> None:  # noqa: D103
    from components.clinic.public.commands.booking import (  # noqa: F401
        notifications,
        sessions,
        therapists,
    )

notifications

notify_all_scheduled_messages
notify_all_scheduled_messages()
Source code in components/clinic/public/commands/booking/notifications.py
@booking_commands.command()
@command_with_dry_run
def notify_all_scheduled_messages() -> None:  # noqa: D103
    from components.clinic.internal.booking.business_logic.notifications import (
        notify_all_scheduled_messages,
    )

    notify_all_scheduled_messages()

sessions

cancel_all_future_sessions_for_medical_admin_as_medical_admin
cancel_all_future_sessions_for_medical_admin_as_medical_admin(
    dry_run, medical_admin_id
)

Cancels all future sessions for a given medical admin as if the same medical admin is cancelling them themselves. This is useful when a medical admin is departing Alan, and we want all the members with booked sessions to receive the appropriate notifications for the cancellation.

Source code in components/clinic/public/commands/booking/sessions.py
@booking_commands.command()
@click.option(
    "--medical-admin-id",
    type=str,
    required=True,
)
@command_with_dry_run
def cancel_all_future_sessions_for_medical_admin_as_medical_admin(
    dry_run: bool,
    medical_admin_id: str,
) -> None:
    """Cancels all future sessions for a given medical admin as if the same medical admin is cancelling them themselves.
    This is useful when a medical admin is departing Alan, and we want all the members with booked sessions to receive the appropriate notifications for the cancellation.
    """
    from components.clinic.internal.booking.business_logic.sessions_actions import (
        cancel_session_for_expert,
    )
    from components.clinic.internal.booking.business_logic.sessions_queries import (
        get_therapist_upcoming_sessions,
    )
    from components.clinic.internal.models.medical_admin import MedicalAdmin
    from shared.helpers.get_or_else import get_or_raise_missing_resource
    from shared.helpers.logging.logger import current_logger

    # get the feature user for the medical admin
    medical_admin = get_or_raise_missing_resource(MedicalAdmin, medical_admin_id)
    medical_admin_feature_user = medical_admin.clinic_user.feature_user

    # get all the sessions for the medical admin
    sessions = get_therapist_upcoming_sessions(
        actor_app_user=medical_admin_feature_user,
    )

    current_logger.info(
        f"Found {len(sessions)} upcoming sessions for medical admin {medical_admin_id}"
    )

    # cancel each session as the medical admin
    for num_cancelled, session in enumerate(sessions, 1):
        if dry_run:
            current_logger.info(f"DRY RUN: Would have cancelled session {session.id}")
        else:
            current_logger.info(f"Cancelling session {session.id}")

        cancel_session_for_expert(
            session_id=session.id,
            actor_app_user=medical_admin_feature_user,
            dry_run=dry_run,
        )
        if num_cancelled % 5 == 0:
            current_logger.info(f"Cancelled {num_cancelled}/{len(sessions)} sessions")

    current_logger.info(f"Completed. Cancelled {len(sessions)} sessions")
delete_test_sessions
delete_test_sessions()
Source code in components/clinic/public/commands/booking/sessions.py
@booking_commands.command()
@do_not_run_in_prod
def delete_test_sessions() -> None:  # noqa: D103
    from components.clinic.internal.booking.business_logic.end_to_end_test import (
        delete_test_sessions,
    )

    delete_test_sessions()
expire_booking_invites
expire_booking_invites(dry_run)
Source code in components/clinic/public/commands/booking/sessions.py
@booking_commands.command()
@command_with_dry_run
def expire_booking_invites(dry_run: bool) -> None:  # noqa: D103
    from components.clinic.internal.booking.business_logic.sessions_actions import (
        cancel_all_expired_invites,
    )

    cancel_all_expired_invites(dry_run=dry_run)
generate_past_sessions_invoice
generate_past_sessions_invoice(dry_run=False)
Source code in components/clinic/public/commands/booking/sessions.py
@booking_commands.command()
@command_with_dry_run
def generate_past_sessions_invoice(dry_run: bool = False) -> None:  # noqa: D103
    from components.clinic.internal.booking.business_logic.sessions_invoicing import (
        generate_past_sessions_invoice,
    )

    generate_past_sessions_invoice(dry_run=dry_run)
generate_sessions_invoices
generate_sessions_invoices(dry_run)
Source code in components/clinic/public/commands/booking/sessions.py
@booking_commands.command()
@command_with_dry_run
def generate_sessions_invoices(dry_run: bool) -> None:  # noqa: D103
    from components.clinic.internal.booking.business_logic.sessions_invoicing import (
        generate_sessions_invoices,
    )

    generate_sessions_invoices(dry_run=dry_run)
push_metadata_to_doctor_ai_for_recent_ended_sessions_booked_from_mo
push_metadata_to_doctor_ai_for_recent_ended_sessions_booked_from_mo(
    dry_run,
)

Push the metadata of recent ended sessions (<48 hours ago) booked from Mo to Doctor AI.

Source code in components/clinic/public/commands/booking/sessions.py
@booking_commands.command()
@command_with_dry_run
def push_metadata_to_doctor_ai_for_recent_ended_sessions_booked_from_mo(
    dry_run: bool,
) -> None:
    """Push the metadata of recent ended sessions (<48 hours ago) booked from Mo to Doctor AI."""
    from components.clinic.internal.booking.business_logic.events_subscribers import (
        track_lifecycle_event_for_session_from_mo_redirection,
    )
    from components.clinic.internal.booking.business_logic.sessions_queries import (
        get_recent_ended_sessions_booked_from_mo,
    )
    from components.clinic.internal.booking.entities.events import (
        SessionRecommendedByMoWasUpdated,
    )
    from shared.helpers.logging.logger import current_logger

    sessions = get_recent_ended_sessions_booked_from_mo()

    for session in sessions:
        if dry_run:
            current_logger.info(
                f"DRY RUN: Would have pushed metadata to Doctor AI for session {session.id} with prescription types {session.prescription_types}"
            )
            continue

        # We can afford running the event synchronously here as we are not a user facing process
        # and operate on a small batch (max 10^2)
        track_lifecycle_event_for_session_from_mo_redirection(
            event=SessionRecommendedByMoWasUpdated(
                session_id=session.id,
                metadata={
                    "status": "did_happen",
                    "prescription_types": session.prescription_types,
                },
            )
        )
refund_payment_not_linked_to_session
refund_payment_not_linked_to_session(dry_run)
Source code in components/clinic/public/commands/booking/sessions.py
@booking_commands.command()
@command_with_dry_run
def refund_payment_not_linked_to_session(dry_run: bool) -> None:  # noqa: D103
    from components.clinic.internal.booking.business_logic.credits import (
        refund_payment_not_linked_to_session,
    )

    refund_payment_not_linked_to_session(dry_run=dry_run)

therapists

update_cached_next_availability
update_cached_next_availability(dry_run)
Source code in components/clinic/public/commands/booking/therapists.py
@booking_commands.command()
@command_with_dry_run
def update_cached_next_availability(dry_run: bool) -> None:  # noqa: D103
    from components.clinic.internal.booking.business_logic.therapists import (
        update_cached_next_availability_of_all_therapists,
    )

    update_cached_next_availability_of_all_therapists(commit=not dry_run)

clinic_invoices

generate_fake_clinic_invoices

generate_fake_clinic_invoices(dry_run)
Source code in components/clinic/public/commands/clinic_invoices.py
@clinic_commands.command()
@command_with_dry_run
@do_not_run_in_prod
def generate_fake_clinic_invoices(dry_run: bool) -> None:  # noqa: D103
    from components.clinic.internal.business_logic.clinic_invoice import (
        generate_fake_clinic_invoices,
    )

    generate_fake_clinic_invoices(dry_run=dry_run)

sync_clinic_monthly_invoice_lines_from_turing

sync_clinic_monthly_invoice_lines_from_turing(
    dry_run, date, previous_month, mapped_ids
)
Source code in components/clinic/public/commands/clinic_invoices.py
@clinic_commands.command()
@command_with_dry_run
@click.option(
    "--date",
    type=click.DateTime(formats=["%Y-%m-%d"]),
    required=False,
    help="Date in YYYY-MM-DD format (e.g., 2021-09-01)",
)
@click.option(
    "--previous-month",
    is_flag=True,
    default=False,
    help="Run invoice generation for the previous month",
)
@click.option(
    "--mapped_ids",
    type=str,
    required=False,
    help="Optional mapping of origin IDs to target IDs in the format 'originid1:targetid1,originid2:targetid2'. If provided, only the specified origin IDs will be queried from Turing, and the corresponding target IDs will be used when inserting into the backend database.",
)
def sync_clinic_monthly_invoice_lines_from_turing(  # noqa: D103
    dry_run: bool,
    date: Optional[datetime.date],
    previous_month: bool,
    mapped_ids: Optional[str],
) -> None:
    from components.clinic.internal.business_logic.clinic_invoice import (
        sync_clinic_monthly_invoice_lines_from_turing,
    )

    if date and previous_month:
        raise click.BadParameter(
            "You can't specify both --date and --previous-month options"
        )

    if previous_month:
        date = (
            datetime.date.today().replace(day=1) - datetime.timedelta(days=1)
        ).replace(day=1)
        click.echo(f"Invoicing date: {date}")

    result = sync_clinic_monthly_invoice_lines_from_turing(
        dry_run=dry_run,
        date=date,
        mapped_ids=mapped_ids,
    )

    if not dry_run:
        current_app.slack_web_client.chat_postMessage(  # type: ignore[attr-defined]
            channel=(
                SlackChannel.alan_clinic_ops_channel
                if is_production_mode()
                else SlackChannel.test
            ),
            username="Clinic Bot",
            icon_emoji=":moneybag:",
            text=(
                f"👋 I just finished generating {result.created_invoices_count} invoices (for the {result.invoicing_date} date)."
                "\nHead over <https://alan.com/marmot-v2/alan-clinic/invoices|the billing page> in Marmot and send the invoices for validation 🍔"
                + (
                    f"\nnote: {result.removed_invoices_count} draft invoice(s) were first removed when generating today's invoices."
                    if result.removed_invoices_count
                    else ""
                )
            ),
        )

end_to_end_test

delete_test_conversations

delete_test_conversations()
Source code in components/clinic/public/commands/end_to_end_test.py
@clinic_commands.command()
@do_not_run_in_prod
def delete_test_conversations() -> None:  # noqa: D103
    from components.clinic.internal.business_logic.end_to_end_test import (
        delete_test_conversations,
    )

    delete_test_conversations()

keep_last_part_from_test_conversations

keep_last_part_from_test_conversations()
Source code in components/clinic/public/commands/end_to_end_test.py
@clinic_commands.command()
@do_not_run_in_prod
def keep_last_part_from_test_conversations() -> None:  # noqa: D103
    from components.clinic.internal.business_logic.end_to_end_test import (
        keep_last_part_from_test_conversations,
    )

    keep_last_part_from_test_conversations()

gdpr_compliance

get_user_ids_ready_for_deletion

get_user_ids_ready_for_deletion()

Return feature users whose clinic data is ready for GDPR deletion.

Source code in components/clinic/public/commands/gdpr_compliance.py
@clinic_commands.command()
def get_user_ids_ready_for_deletion() -> None:
    """Return feature users whose clinic data is ready for GDPR deletion."""
    from components.clinic.public.business_logic.gdpr_compliance import (
        get_user_ids_ready_for_deletion as _get_user_ids_ready_for_deletion,
    )

    feature_users = _get_user_ids_ready_for_deletion()

    current_logger.info(
        "user IDs ready for deletion",
        count=len(feature_users),
    )

medical_admins

backfill_medical_admin_synced_ans_data

backfill_medical_admin_synced_ans_data(dry_run)

This temporary command creates ANS data for medical admins that have a medical_identifier but don't have associated ANS data yet.

TODO: @sarah.louahem Remove this command once run in prod - we now create ANS data automatically.

Source code in components/clinic/public/commands/medical_admins.py
@clinic_commands.command()
@command_with_dry_run
def backfill_medical_admin_synced_ans_data(dry_run: bool) -> None:
    """
    This temporary command creates ANS data for medical admins that have
    a medical_identifier but don't have associated ANS data yet.

    TODO: @sarah.louahem Remove this command once run in prod - we now create ANS data automatically.
    """
    from components.clinic.internal.business_logic.medical_admin_ans import (
        backfill_medical_admin_synced_ans_data as backfill_ans_data,
    )

    click.echo(f"Starting backfill of medical admin ANS data with dry_run={dry_run}")

    try:
        backfill_ans_data(dry_run)
        click.echo(f"Backfill completed successfully with dry_run={dry_run}")
    except Exception as error:
        click.echo(
            f"Error during backfill: {error}",
            err=True,
        )
        raise

create_medical_admins

create_medical_admins(password, dry_run)
Source code in components/clinic/public/commands/medical_admins.py
@clinic_commands.command()
@click.option(
    "--password", help="Password for all the accounts", type=str, default="azerty"
)
@command_with_dry_run
def create_medical_admins(password: str, dry_run: bool) -> None:  # noqa: D103
    _create_medical_admins(password=password, dry_run=dry_run)

create_medical_chat_bot

create_medical_chat_bot()
Source code in components/clinic/public/commands/medical_admins.py
@clinic_commands.command()
def create_medical_chat_bot() -> None:  # noqa: D103
    from components.clinic.internal.business_logic.medical_chat_bot import (
        create_medical_chat_bot_clinic_user,
        medical_chat_bot_clinic_user_exists,
    )

    if not is_development_mode():
        click.echo("This command is only available in development mode!")
        return

    if medical_chat_bot_clinic_user_exists():
        click.echo("The Medical Chat Bot already exists.")
    else:
        create_medical_chat_bot_clinic_user()

set_offline_inactive_medical_admin

set_offline_inactive_medical_admin(minutes, dry_run)
Source code in components/clinic/public/commands/medical_admins.py
@clinic_commands.command()
@click.option(
    "--minutes",
    type=int,
    default=30,
    help="Number of minutes before medical admin is inactive",
)
@command_with_dry_run
def set_offline_inactive_medical_admin(minutes: int, dry_run: bool) -> None:  # noqa: D103
    from components.clinic.internal.business_logic.medical_admin import (
        set_offline_inactive_medical_admins,
    )

    set_offline_inactive_medical_admins(
        minutes=minutes,
        dry_run=dry_run,
    )

sync_medical_admin_ans_data

sync_medical_admin_ans_data(dry_run)

Sync medical admin ANS data with the ANS FHIR API.

This command checks all stored medical admin ANS data and updates records that have older versions than what's available in the ANS FHIR API.

Source code in components/clinic/public/commands/medical_admins.py
@clinic_commands.command()
@command_with_dry_run
def sync_medical_admin_ans_data(dry_run: bool) -> None:
    """
    Sync medical admin ANS data with the ANS FHIR API.

    This command checks all stored medical admin ANS data and updates records
    that have older versions than what's available in the ANS FHIR API.
    """
    from components.clinic.internal.business_logic.medical_admin_ans import (
        sync_outdated_medical_admin_ans_data,
    )

    click.echo(f"Starting sync of medical admin ANS data with dry_run={dry_run}")

    try:
        sync_outdated_medical_admin_ans_data(dry_run)
        click.echo(f"Sync completed successfully with dry_run={dry_run}")
    except Exception as error:
        click.echo(
            f"Error during sync: {error}",
            err=True,
        )
        raise

medical_admins_dataset

MedicalAdminData dataclass

MedicalAdminData(
    first_name,
    last_name,
    onboarding_status,
    accessible_conversation_specialties,
    apps_displayed_in,
    has_access_to_app_ids,
    access_types,
    specialty,
    country,
    languages,
    description=None,
    experiences=None,
    answers_to_proactive_conversation_topics=None,
    prod_id=None,
    dato_id=None,
    first_message_body=None,
    avatar=None,
    clinic_role=None,
)
access_types instance-attribute
access_types
accessible_conversation_specialties instance-attribute
accessible_conversation_specialties
answers_to_proactive_conversation_topics class-attribute instance-attribute
answers_to_proactive_conversation_topics = None
apps_displayed_in instance-attribute
apps_displayed_in
avatar class-attribute instance-attribute
avatar = None
clinic_role class-attribute instance-attribute
clinic_role = None
country instance-attribute
country
dato_id class-attribute instance-attribute
dato_id = None
description class-attribute instance-attribute
description = None
experiences class-attribute instance-attribute
experiences = None
first_message_body class-attribute instance-attribute
first_message_body = None
first_name instance-attribute
first_name
has_access_to_app_ids instance-attribute
has_access_to_app_ids
languages instance-attribute
languages
last_name instance-attribute
last_name
onboarding_status instance-attribute
onboarding_status
prod_id class-attribute instance-attribute
prod_id = None
specialty instance-attribute
specialty

MedicalAdminExperienceData dataclass

MedicalAdminExperienceData(title, subtitle, index)
index instance-attribute
index
subtitle instance-attribute
subtitle
title instance-attribute
title

RAW_MEDICAL_ADMINS module-attribute

RAW_MEDICAL_ADMINS = [
    MedicalAdminData(
        first_name="Augustin",
        last_name="Beaucote",
        specialty=GENERAL_PRACTITIONER,
        avatar="http://eu.alan.uploads.s3.eu-central-1.amazonaws.com/manual_upload/9d775ac221a84812a3cc18bae63b7766_doctor_beaucote_headshot.jpg",
        description="Je suis médecin généraliste et immunologue et j’ai rejoint Alan début 2020 pour développer les services de santé.\nJ’ai fait mes études en région parisienne à la faculté du Kremlin Bicêtre avant de rejoindre la région de Montpellier en 2015, où j’ai été interne pendant 3 ans.\nJ’ai choisi de me spécialiser en immunologie et j’ai fait des remplacements en cabinet de médecine générale et aux urgences adultes et pédiatriques.",
        experiences=[
            MedicalAdminExperienceData(
                title="Immunothérapies ciblées des maladies",
                subtitle="Faculté de médecine de Montpellier",
                index=3,
            ),
            MedicalAdminExperienceData(
                title="Master 2, Génome et différenciation cellulaire - Hématopoïèse",
                subtitle="Université Paris Diderot et IGMM",
                index=2,
            ),
            MedicalAdminExperienceData(
                title="DESC 2 d'immunologie clinique",
                subtitle="Université de Montpellier",
                index=1,
            ),
            MedicalAdminExperienceData(
                title="Doctorat en Médecine Générale",
                subtitle="Faculté de médecine de Montpellier",
                index=0,
            ),
        ],
        apps_displayed_in=[alan_insurance],
        accessible_conversation_specialties=[
            GENERAL_MEDICINE,
            PEDIATRICS,
            CHILDCARE,
            DIETETICS,
            DERMATOLOGY,
            PHYSIOTHERAPY,
            GYNECOLOGY,
        ],
        clinic_role=CLINIC_ADMIN,
        has_access_to_app_ids=[ALAN_FR],
        access_types=[CHAT],
        answers_to_proactive_conversation_topics=[
            SLEEP,
            STRESS,
        ],
        country=france,
        languages=[french, english],
        onboarding_status=COMPLETED,
    ),
    MedicalAdminData(
        first_name="Clara",
        last_name="Poncelet",
        specialty=MIDWIFE,
        first_message_body="Je suis Clara, je suis sage-femme, j’exerce en Suisse dans une structure hospitalière et en France comme sage-femme libérale. Je suis aussi maman d’une petite fille depuis fin 2020 !",
        avatar="http://eu.alan.uploads.s3.eu-central-1.amazonaws.com/manual_upload/3fec273f3a484072b50029a7ee7a0440_clara_poncelet.jpg",
        description="Je suis sage-femme depuis 2015 et heureuse nouvelle arrivée chez Alan.\nCes cinq dernières années sont marquées par mes voyages régionaux car j’ai choisi d’exercer dans différents grands centres hospitaliers afin d’enrichir ma formation.\nDepuis 2019 je fais des remplacements en libéral pour un accompagnement semi-global et j’exerce en Suisse dans une structure hospitalière. Je me forme dans les domaines ouverts aux sages-femmes dès que j’en ai l’occasion pour adapter mes pratiques.\nJe suis maman d’une petite fille depuis fin 2020.",
        experiences=[
            MedicalAdminExperienceData(
                title="Diplôme de sage-femme en 2015",
                subtitle="Université de Bourgogne à Dijon",
                index=0,
            )
        ],
        accessible_conversation_specialties=[GYNECOLOGY],
        apps_displayed_in=[alan_insurance],
        has_access_to_app_ids=[ALAN_FR],
        access_types=[CHAT],
        country=france,
        languages=[french, english],
        onboarding_status=COMPLETED,
    ),
    MedicalAdminData(
        first_name="Barbara",
        last_name="Dezileaux",
        specialty=GENERAL_PRACTITIONER,
        avatar="http://eu.alan.uploads.s3.eu-central-1.amazonaws.com/manual_upload/8b28ef4ffbea476c93565ad2d3cd4eb7_doctor_dezileaux_headshot.jpg",
        description="Médecin généraliste, j’ai choisi de rejoindre les équipes d’Alan car j’apprécie leur démarche d’aide et de conseil envers leurs utilisateurs.\nJe suis diplômée depuis 2015, après avoir réalisé mes études à Bordeaux puis mon internat à Lille.\nJ’exerce depuis en cabinet libéral et en clinique dans différentes régions de France.\nJ’ai à cœur de me former en continu, en particulier sur les sujets de la maternité et de la périnatalité qui me passionnent.",
        experiences=[
            MedicalAdminExperienceData(
                title="Doctorat en Médecine Générale",
                subtitle="Faculté de médecine de Lille",
                index=0,
            )
        ],
        accessible_conversation_specialties=[
            GENERAL_MEDICINE,
            PEDIATRICS,
            CHILDCARE,
            DIETETICS,
            DERMATOLOGY,
            PHYSIOTHERAPY,
            GYNECOLOGY,
        ],
        apps_displayed_in=[alan_insurance],
        access_types=[CHAT],
        has_access_to_app_ids=[ALAN_FR],
        country=france,
        languages=[french, english],
        onboarding_status=COMPLETED,
    ),
    MedicalAdminData(
        first_name="Marion",
        last_name="Cosson",
        specialty=GENERAL_PRACTITIONER,
        clinic_role=CLINIC_ADMIN,
        apps_displayed_in=[],
        has_access_to_app_ids=[ALAN_FR],
        accessible_conversation_specialties=[
            GENERAL_MEDICINE,
            PEDIATRICS,
            CHILDCARE,
            DIETETICS,
            DERMATOLOGY,
            PHYSIOTHERAPY,
            GYNECOLOGY,
        ],
        access_types=[CHAT],
        country=france,
        languages=[french, english, spanish],
        onboarding_status=COMPLETED,
    ),
    MedicalAdminData(
        first_name="Pauline",
        last_name="Lotte",
        specialty=CHILDCARE_NURSE,
        avatar="http://eu.alan.uploads.s3.eu-central-1.amazonaws.com/manual_upload/9a0297eaf92f4f88a763c9fe3c954983_pauline-lotte.jpg",
        description="Je suis infirmière puéricultrice depuis 2007 et installée en libéral depuis 2018. J'interviens chez Alan en tant que spécialiste du sommeil.\nAprès 10 ans de pratique en hôpital pédiatrique et 1 an en direction de crèche, j'ai choisi de travailler en indépendante pour accompagner les parents au plus près de leur quotidien avec bébé.\nJe me suis spécialisée dans plusieurs domaines, et plus spécialement le sommeil du tout-petit.\nJe suis maman d'une petite fille depuis 2019.",
        experiences=[
            MedicalAdminExperienceData(
                title="2006 - Diplôme d'Etat d'Infirmière",
                subtitle="IFSI de Reims",
                index=10,
            ),
            MedicalAdminExperienceData(
                title="2007 - Diplôme d'Etat de Puéricultrice",
                subtitle="Ecole de puéricultrice de Reims",
                index=9,
            ),
            MedicalAdminExperienceData(
                title="2013 - Diplôme Universitaire de prise en charge de la douleur de l'enfant",
                subtitle=None,
                index=8,
            ),
            MedicalAdminExperienceData(
                title="2019 - Instructrice Dunstan Baby Langage",
                subtitle="Dunstan Baby Langage France",
                index=7,
            ),
            MedicalAdminExperienceData(
                title="2019 - Educateur Montessori",
                subtitle="Enfance Positive",
                index=6,
            ),
            MedicalAdminExperienceData(
                title="2020 - Animateur de Signes Associés à la Parole",
                subtitle="Eveil et Signes",
                index=5,
            ),
            MedicalAdminExperienceData(
                title="2020 - Formation Nutrition pédiatrique",
                subtitle="EPM nutrition",
                index=4,
            ),
            MedicalAdminExperienceData(
                title="2020 - Formation au sommeil du bébé de 0 à 5 ans",
                subtitle="M. Bilodeau",
                index=3,
            ),
            MedicalAdminExperienceData(
                title="2020 - Comprendre et accompagner le sommeil de l'enfant",
                subtitle="Mandy Roman",
                index=2,
            ),
            MedicalAdminExperienceData(
                title="2021 - Formation consultation du sommeil, alimentation et rythmes de 0 à 6 ans en avril 2021",
                subtitle="Prosom",
                index=1,
            ),
            MedicalAdminExperienceData(
                title="2021 - Formation sommeil du tout petit et accompagnement parental",
                subtitle="Ingrid Bayot",
                index=0,
            ),
        ],
        apps_displayed_in=[alan_insurance],
        accessible_conversation_specialties=[CHILDCARE],
        has_access_to_app_ids=[ALAN_FR],
        access_types=[CHAT],
        country=france,
        languages=[dutch, english],
        onboarding_status=COMPLETED,
    ),
    MedicalAdminData(
        first_name="Margaux",
        last_name="Degen",
        specialty=PSYCHOLOGIST,
        accessible_conversation_specialties=[
            PSYCHOLOGY,
            THERAPY,
        ],
        apps_displayed_in=[alan_insurance],
        has_access_to_app_ids=[ALAN_FR],
        access_types=[VIDEO],
        country=france,
        languages=[french, english],
        onboarding_status=COMPLETED,
        dato_id="148187763",
    ),
    MedicalAdminData(
        first_name="Emile",
        last_name="Montrois",
        specialty=PSYCHOLOGIST,
        accessible_conversation_specialties=[
            PSYCHOLOGY,
            THERAPY,
        ],
        access_types=[VIDEO],
        apps_displayed_in=[alan_insurance],
        has_access_to_app_ids=[ALAN_FR],
        country=france,
        languages=[french, english],
        onboarding_status=COMPLETED,
        dato_id="148184344",
    ),
    MedicalAdminData(
        first_name="Florian",
        last_name="Ghiazza",
        specialty=PHYSIOTHERAPIST,
        accessible_conversation_specialties=[
            PHYSIOTHERAPY,
            BACK_PAIN,
            EMERGENCY_PHYSIOTHERAPY,
        ],
        access_types=[CHAT, VIDEO],
        apps_displayed_in=[alan_insurance],
        has_access_to_app_ids=[ALAN_FR],
        country=france,
        languages=[french, english],
        onboarding_status=COMPLETED,
        dato_id="OTiPilFASx-0yBrP2HtLrQ",
        prod_id="b65af61b-c455-4c5f-a678-164d165d1f45",
    ),
    MedicalAdminData(
        first_name="Diego",
        last_name="Ferral-Toro",
        specialty=PHYSIOTHERAPIST,
        accessible_conversation_specialties=[
            PHYSIOTHERAPY,
            BACK_PAIN,
            EMERGENCY_PHYSIOTHERAPY,
        ],
        access_types=[CHAT, VIDEO],
        apps_displayed_in=[alan_insurance],
        has_access_to_app_ids=[ALAN_ES],
        country=spain,
        languages=[spanish, english],
        onboarding_status=COMPLETED,
        dato_id="XSdL4ehETM-owb_20xEmhg",
        prod_id="18eb92d9-3271-4286-91dc-a4e1a8ac5de2",
    ),
    MedicalAdminData(
        first_name="Diane",
        last_name="Coomans",
        specialty=PHYSIOTHERAPIST,
        accessible_conversation_specialties=[
            PHYSIOTHERAPY,
            BACK_PAIN,
            EMERGENCY_PHYSIOTHERAPY,
        ],
        access_types=[CHAT, VIDEO],
        apps_displayed_in=[alan_insurance],
        has_access_to_app_ids=[ALAN_BE],
        country=belgium,
        languages=[dutch, english],
        onboarding_status=COMPLETED,
        dato_id="OXi_GJK7TziPCWlkkFWQCw",
        prod_id="59ce8029-2aa1-4ff5-81b1-25156ebcf5f1",
    ),
    MedicalAdminData(
        first_name="Robin",
        last_name="Vervaeke",
        specialty=PHYSIOTHERAPIST,
        accessible_conversation_specialties=[
            PHYSIOTHERAPY,
            BACK_PAIN,
            EMERGENCY_PHYSIOTHERAPY,
        ],
        access_types=[CHAT, VIDEO],
        apps_displayed_in=[alan_insurance],
        has_access_to_app_ids=[ALAN_FR, ALAN_ES, ALAN_BE],
        country=france,
        languages=[french, english],
        onboarding_status=COMPLETED,
        dato_id="R03pRAdAR1-Hq7_B4A0pAw",
        prod_id="59584887-47a7-464d-86c7-4f1a15299eaa",
    ),
    MedicalAdminData(
        first_name="Olivier",
        last_name="Delarras",
        specialty=GENERAL_PRACTITIONER,
        accessible_conversation_specialties=[
            GENERAL_MEDICINE,
            CONSULTATION,
            PEDIATRICS,
            CHILDCARE,
            DIETETICS,
            DERMATOLOGY,
            PHYSIOTHERAPY,
            GYNECOLOGY,
        ],
        access_types=[CHAT, VIDEO],
        apps_displayed_in=[alan_insurance],
        has_access_to_app_ids=[ALAN_FR, ALAN_ES, ALAN_BE],
        country=france,
        languages=[french, english],
        onboarding_status=COMPLETED,
        dato_id="RunHY9MqRXOl4ti7fuhoTw",
        prod_id="3c7c88e9-0794-4d23-9a32-002e75ca1f60",
    ),
]

medical_conversation

close_old_answered_medical_conversations

close_old_answered_medical_conversations(
    age_for_gp, age_for_non_gp, dry_run
)
Source code in components/clinic/public/commands/medical_conversation.py
@clinic_commands.command()
@command_with_dry_run
@click.option(
    "--age-for-gp", type=int, help="Minimum age in days to close gp conversations"
)
@click.option(
    "--age-for-non-gp",
    type=int,
    help="Minimum age in days to close non gp conversations",
)
def close_old_answered_medical_conversations(  # noqa: D103
    age_for_gp: int,
    age_for_non_gp: int,
    dry_run: bool,
) -> None:
    from components.clinic.internal.business_logic.medical_conversation import (
        close_old_answered_medical_conversations,
    )

    close_old_answered_medical_conversations(
        age_for_gp=age_for_gp,
        age_for_non_gp=age_for_non_gp,
        dry_run=dry_run,
    )

create_fake_conversations

create_fake_conversations(count, specialty, app, is_closed)

Create fake conversations for testing purposes only - count: number of fake conversations to create - specialty: medical specialty of the fake conversations to create - app: app of the fake conversations to create - is_closed: whether the fake conversations should be closed or not

Source code in components/clinic/public/commands/medical_conversation.py
@clinic_commands.command()
@click.option(
    "-c", "--count", default=50, help="Number of fake conversations to create"
)
@click.option(
    "-s",
    "--specialty",
    default=None,
    help="Medical specialty of fake conversations to create",
)
@click.option(
    "-a",
    "--app",
    default=None,
    help="App of fake conversations to create",
)
@click.option(
    "--is-closed",
    type=bool,
    default=False,
    help="App of fake conversations to create",
)
@do_not_run_in_prod
def create_fake_conversations(
    count: int, specialty: str, app: str, is_closed: bool
) -> None:
    """
    Create fake conversations for testing purposes only
    - count: number of fake conversations to create
    - specialty: medical specialty of the fake conversations to create
    - app: app of the fake conversations to create
    - is_closed: whether the fake conversations should be closed or not
    """
    import random

    from sqlalchemy.sql.expression import func

    from components.clinic.internal.enums.medical_conversation_specialty import (
        MedicalConversationSpecialty,
    )
    from components.clinic.internal.models.clinic_user import ClinicUser
    from components.clinic.internal.models.medical_admin import MedicalAdmin
    from components.clinic.internal.models.medical_conversation import (
        MedicalConversation,
    )
    from components.clinic.internal.models.medical_conversation_attachment import (
        MedicalConversationAttachment,
    )
    from components.clinic.internal.models.medical_conversation_part import (
        MedicalConversationPart,
    )
    from components.fr.public.user.create_profile_with_user import (  # noqa: ALN043
        create_profile_with_user,
    )
    from shared.helpers.app_name import AppName

    # re-use existing encrypted body for new parts
    # they can be decrypted by medical admin
    parts_with_internal_encrypted_body = (
        current_session.query(MedicalConversationPart)  # noqa: ALN085
        .filter(
            MedicalConversationPart.internal_encrypted_body.is_not(None),
        )
        .all()
    )
    internal_encrypted_bodies = [
        p.internal_encrypted_body for p in parts_with_internal_encrypted_body
    ]

    # re-use existing attachments for new attachments
    # they can be decrypted by medical admin
    internally_encrypted_attachments = (
        current_session.query(MedicalConversationAttachment)  # noqa: ALN085
        .filter(MedicalConversationAttachment.is_internally_encrypted.is_(True))
        .all()
    )

    medical_specialty = MedicalConversationSpecialty.validate(specialty)
    app_id = AppName.validate(app)

    medical_admins = current_session.query(MedicalAdmin).filter(  # noqa: ALN085
        MedicalAdmin.clinic_role != MedicalAdminRole.CLINIC_OPS
    )

    for _ in range(count):
        medical_specialty_for_conversation = (
            medical_specialty
            if medical_specialty
            else random.choice(list(MedicalConversationSpecialty))  # noqa: S311
        )

        medical_admins_for_specialty = [
            medical_admin
            for medical_admin in medical_admins
            if medical_admin.default_conversation_specialty
            == medical_specialty_for_conversation
        ]
        if not medical_admins_for_specialty:
            click.echo(
                f"No medical admin found to handle specialty {medical_specialty_for_conversation}, skiping for this conversation"
            )
            continue

        app_id = (
            app_id if app_id else random.choice([AppName.ALAN_FR, AppName.ALAN_BE])  # noqa: S311
        )

        clinic_user = (
            current_session.query(ClinicUser)  # noqa: ALN085
            .filter(ClinicUser.app_id == app_id)
            .order_by(func.random())
            .first()
        )
        # 20% chance to create a new user
        has_to_create_user = (
            clinic_user is None or random.randint(0, 4) == 0  # noqa: S311
        )

        if has_to_create_user:
            gender = random.choice([UserGender.male, UserGender.female])  # noqa: S311
            first_name = (
                faker.first_name_male()
                if gender == UserGender.male
                else faker.first_name_female()
            )
            last_name = faker.last_name()
            birth_date = faker.date_of_birth(minimum_age=18, maximum_age=80)
            user = create_profile_with_user(
                first_name=first_name,
                last_name=last_name,
                gender=gender,
                birth_date=birth_date,
            )
            clinic_user = ClinicUser(
                app_user_id=str(user.id),
                app_id=app_id,
            )
            current_session.add(clinic_user)

        medical_admin = random.choice(medical_admins_for_specialty)  # noqa: S311

        conversation = MedicalConversation(
            creator_clinic_user=clinic_user,
            member_clinic_user=clinic_user,
            medical_specialty=medical_specialty_for_conversation,
            has_been_closed_by_medical_admin=is_closed,
        )
        current_session.add(conversation)
        current_session.flush()

        # create between 2 and 30 user parts
        nb_parts = random.randint(2, 30)  # noqa: S311
        for _ in range(nb_parts):
            internal_encrypted_body = (
                random.choice(internal_encrypted_bodies)  # noqa: S311
                if len(internal_encrypted_bodies) > 0
                else None
            )
            member_part = MedicalConversationPart(
                medical_conversation=conversation,
                author=clinic_user,
                body=faker.text(),
                internal_encrypted_body=internal_encrypted_body,
            )
            current_session.add(member_part)
            # 25% chance to have an attachment
            if random.randint(0, 3) == 0:  # noqa: S311
                internally_encrypted_attachment = (
                    random.choice(internally_encrypted_attachments)  # noqa: S311
                    if len(internally_encrypted_attachments) > 0
                    else None
                )
                if internally_encrypted_attachment:
                    member_attachment = MedicalConversationAttachment(
                        medical_conversation_part=member_part,
                        filename=internally_encrypted_attachment.filename,
                        mime_type=internally_encrypted_attachment.mime_type,
                        s3_key=internally_encrypted_attachment.s3_key,  # gitleaks:allow
                        is_internally_encrypted=True,
                    )
                    current_session.add(member_attachment)
            # for each user part, create between 2 and 5 medical admin parts
            nb_admin_parts = random.randint(2, 5)  # noqa: S311
            for _ in range(nb_admin_parts):
                admin_part = MedicalConversationPart(
                    author=medical_admin,
                    medical_conversation=conversation,
                    body=faker.text(),
                    internal_encrypted_body=random.choice(  # noqa: S311
                        internal_encrypted_bodies
                    )
                    if len(internal_encrypted_bodies) > 0
                    else None,
                )
                current_session.add(admin_part)
                # 25% chance to have an attachment
                if random.randint(0, 3) == 0:  # noqa: S311
                    internally_encrypted_attachment = (
                        random.choice(internally_encrypted_attachments)  # noqa: S311
                        if len(internally_encrypted_attachments) > 0
                        else None
                    )
                    if internally_encrypted_attachment:
                        admin_attachment = MedicalConversationAttachment(
                            medical_conversation_part=admin_part,
                            filename=internally_encrypted_attachment.filename,
                            mime_type=internally_encrypted_attachment.mime_type,
                            s3_key=internally_encrypted_attachment.s3_key,  # gitleaks:allow
                            is_internally_encrypted=True,
                        )
                        current_session.add(admin_attachment)
        # 50% chance to add 1 last user part to the conversation
        # have similar proportion of inactive and active conversations
        if random.randint(0, 1) == 0:  # noqa: S311
            internal_encrypted_body = (
                random.choice(internal_encrypted_bodies)  # noqa: S311
                if len(internal_encrypted_bodies) > 0
                else None
            )
            last_member_part = MedicalConversationPart(
                medical_conversation=conversation,
                author=clinic_user,
                body=faker.text(),
                internal_encrypted_body=internal_encrypted_body,
            )
            current_session.add(last_member_part)
        current_logger.info(
            f"Created fake conversation {conversation.id} with specialty {medical_specialty_for_conversation} for assigned medical admin {medical_admin.id}"
        )

    current_logger.info(f"Created {count} fake conversations")
    current_session.commit()

erase_all_conversation_data

erase_all_conversation_data()
Source code in components/clinic/public/commands/medical_conversation.py
@clinic_commands.command()
@do_not_run_in_prod
def erase_all_conversation_data() -> None:  # noqa: D103
    from components.clinic.internal.models.medical_conversation import (
        MedicalConversation,
    )
    from components.clinic.internal.models.medical_conversation_ai_suggestions_result import (
        MedicalConversationAiSuggestionsResult,
    )
    from components.clinic.internal.models.medical_conversation_attachment import (
        MedicalConversationAttachment,
    )
    from components.clinic.internal.models.medical_conversation_bookmark import (
        MedicalConversationBookmark,
    )
    from components.clinic.internal.models.medical_conversation_follow_up import (
        MedicalConversationFollowUp,
    )
    from components.clinic.internal.models.medical_conversation_part import (
        MedicalConversationPart,
    )
    from components.clinic.internal.models.medical_conversation_rating_v2 import (
        MedicalConversationRatingV2,
    )
    from components.clinic.internal.models.medical_conversation_redirection import (
        MedicalConversationRedirection,
    )
    from components.clinic.internal.models.medical_conversation_redirection_medical_conversation_part import (
        MedicalConversationRedirectionMedicalConversationPart,
    )
    from components.clinic.internal.models.medical_conversation_review_from_medical_admin import (
        MedicalConversationReviewFromMedicalAdmin,
    )
    from components.clinic.internal.models.medical_conversation_tag_mapping import (
        MedicalConversationTagMapping,
    )

    if not click.confirm("This will erase all conversation data, are you sure?"):
        return

    current_session.query(MedicalConversationAiSuggestionsResult).delete()  # noqa: ALN085
    current_session.query(MedicalConversationReviewFromMedicalAdmin).delete()  # noqa: ALN085
    current_session.query(  # noqa: ALN085
        MedicalConversationRedirectionMedicalConversationPart
    ).delete()
    current_session.query(MedicalConversationRedirection).delete()  # noqa: ALN085
    current_session.query(MedicalConversationBookmark).delete()  # noqa: ALN085
    current_session.query(MedicalConversationFollowUp).delete()  # noqa: ALN085
    current_session.query(MedicalConversationTagMapping).delete()  # noqa: ALN085
    current_session.query(MedicalConversationRatingV2).delete()  # noqa: ALN085
    current_session.query(MedicalConversationAttachment).delete()  # noqa: ALN085
    current_session.query(MedicalConversationPart).delete()  # noqa: ALN085
    current_session.query(MedicalConversation).delete()  # noqa: ALN085

    current_session.commit()

faker module-attribute

faker = Faker('fr_FR')

resolve_closable_conversations

resolve_closable_conversations()
Source code in components/clinic/public/commands/medical_conversation.py
@clinic_commands.command()
def resolve_closable_conversations() -> None:  # noqa: D103
    from components.clinic.internal.business_logic.medical_conversation import (
        resolve_closable_conversations as _resolve_closable_conversations,
    )

    _resolve_closable_conversations()

send_pending_proactive_conversations

send_pending_proactive_conversations(dry_run)
Source code in components/clinic/public/commands/medical_conversation.py
@clinic_commands.command()
@command_with_dry_run
def send_pending_proactive_conversations(dry_run: bool) -> None:  # noqa: D103
    from components.clinic.internal.business_logic.medical_conversation_proactive import (
        send_pending_proactive_conversations,
    )

    send_pending_proactive_conversations(commit=not dry_run)

update_active_after_for_conversations

update_active_after_for_conversations(
    dry_run, recently_modified_only
)

This command will update the active_after field on conversations. It's helpful for: - computing it the first time - resetting it from time to time, if we have a bug and we want to fix the values

Source code in components/clinic/public/commands/medical_conversation.py
@clinic_commands.command()
@command_with_dry_run
@click.option(
    "--recently-modified-only",
    type=bool,
    required=False,
    default=False,
    is_flag=True,
    help="Only look at recently modified conversations",
)
def update_active_after_for_conversations(
    dry_run: bool, recently_modified_only: bool
) -> None:
    """
    This command will update the active_after field on conversations.
    It's helpful for:
    - computing it the first time
    - resetting it from time to time, if we have a bug and we want to fix the values
    """
    from components.clinic.internal.business_logic.medical_conversation import (
        update_active_after_for_conversations as _update_active_after_for_conversations,
    )

    _update_active_after_for_conversations(
        commit=not dry_run, recently_modified_only=recently_modified_only
    )
    if dry_run:
        # Just being safe
        current_session.rollback()

unread_message_reminders

send_medical_conversations_unread_message_reminders

send_medical_conversations_unread_message_reminders(
    dry_run,
)
Source code in components/clinic/public/commands/unread_message_reminders.py
@clinic_commands.command()
@command_with_dry_run
def send_medical_conversations_unread_message_reminders(dry_run: bool) -> None:  # noqa: D103
    from components.clinic.internal.business_logic.medical_conversation import (
        send_medical_conversations_unread_message_reminders,
    )

    send_medical_conversations_unread_message_reminders(dry_run=dry_run)

user

clear_stripe_customer_ids

clear_stripe_customer_ids(dry_run)

Clears all stripe_customer_id values from clinic_user table. Only runs in non-production environments. Usage: flask clinic clear_stripe_customer_ids flask clinic clear_stripe_customer_ids --execute

Source code in components/clinic/public/commands/user.py
@clinic_commands.command()
@command_with_dry_run
def clear_stripe_customer_ids(dry_run: bool) -> None:
    """Clears all stripe_customer_id values from clinic_user table.
    Only runs in non-production environments.
    Usage:
        flask clinic clear_stripe_customer_ids
        flask clinic clear_stripe_customer_ids --execute
    """
    from sqlalchemy import func, select, update

    from components.clinic.internal.models.clinic_user import ClinicUser
    from shared.helpers.db import current_session
    from shared.helpers.env import is_production_mode

    if is_production_mode():
        current_logger.error("Cannot run in production")
        return

    if dry_run:
        count = current_session.execute(
            select(func.count())
            .select_from(ClinicUser)
            .where(ClinicUser.stripe_customer_id.is_not(None))
        ).scalar_one()
        current_logger.info(
            f"DRY RUN - would clear stripe_customer_id for {count} clinic users"
        )
    else:
        result = current_session.execute(
            update(ClinicUser)
            .where(ClinicUser.stripe_customer_id.is_not(None))
            .values(stripe_customer_id=None)
        )
        current_session.commit()
        current_logger.info(
            f"Cleared stripe_customer_id for {result.rowcount} clinic users"
        )

delete_clinic_data_for_user

delete_clinic_data_for_user(clinic_user_id, dry_run)
Source code in components/clinic/public/commands/user.py
@clinic_commands.command()
@click.argument("clinic_user_id", type=str)
@command_with_dry_run
def delete_clinic_data_for_user(clinic_user_id: UUID, dry_run: bool) -> None:  # noqa: D103
    from components.clinic.internal.business_logic.clinic_user import (
        delete_all_clinic_data_for_user,
    )

    current_logger.warn(
        "⚠️ This command will delete all clinic user data (conversations, messages, ratings, etc.)"
    )
    delete_all_clinic_data_for_user(clinic_user_id=clinic_user_id, dry_run=dry_run)

components.clinic.public.dependencies

BaseUserData dataclass

BaseUserData(first_name, last_name)

Base user data used by the clinic

This is a subset of the UserData class, used when only the basic user data is needed.

Attributes:

Name Type Description
first_name str | None

The user's first name

last_name str | None

The user's last name

first_name instance-attribute

first_name

full_name property

full_name

Return the full name of the user

last_name instance-attribute

last_name

BookingSessionPackage dataclass

BookingSessionPackage(
    price_in_cents, included=None, reimbursed=None
)

Package available for a booking session type. This includes session's pricing and number of sessions included or reimbursed.

Attributes:

Name Type Description
price_in_cents int

The price of a session in cents

reimbursed Optional[BookingSessionPackageCount]

The reimbursed sessions information (reimbursed by Alan e.g alternative medicine allowance)

included Optional[BookingSessionPackageCount]

The included sessions information (covered with Alan)

included class-attribute instance-attribute

included = None

price_in_cents instance-attribute

price_in_cents

reimbursed class-attribute instance-attribute

reimbursed = None

BookingSessionPackageCount dataclass

BookingSessionPackageCount(
    count_limit, count_remaining=None
)

Count of sessions depends on the type

Attributes:

Name Type Description
count_limit int

The limit of the session count

count_remaining int | None

The remaining session count

count_limit instance-attribute

count_limit

count_remaining class-attribute instance-attribute

count_remaining = None

ClinicAdapter

Bases: ABC

Adapter for the clinic

alan_company_address class-attribute instance-attribute

alan_company_address = (
    "117 Quai de Valmy, 75010 Paris, France"
)

alan_company_display_name class-attribute instance-attribute

alan_company_display_name = 'Alan Services'

alan_company_registration_label class-attribute instance-attribute

alan_company_registration_label = 'SIRET'

alan_company_registration_number class-attribute instance-attribute

alan_company_registration_number = '908361314 00010'
clinic_consent_ai_publish_date = None

The release date of the chat x therapy feature - when conversations are created for therapy sessions between member and therapist automatically

create_external_user abstractmethod

create_external_user(onboarding_data, profile_service)

Create an external teleconsultation user profile with onboarding data. This feature is used to create an external user profile to access teleconsultation without being an Alan member.

Parameters:

Name Type Description Default
onboarding_data ExternalOnboardingUserData

The onboarding user data

required
profile_service ProfileService

The profile service to update birth information

required

Returns:

Type Description
tuple[Any, RefreshTokenType]

A tuple containing the user object and refresh token type

Raises:

Type Description
BaseErrorCode

If gender validation fails

SSNValidationError

If SSN validation fails

Source code in components/clinic/public/dependencies.py
@abstractmethod
def create_external_user(
    self,
    onboarding_data: ExternalOnboardingUserData,
    profile_service: ProfileService,
) -> tuple[Any, "RefreshTokenType"]:
    """Create an external teleconsultation user profile with onboarding data.
    This feature is used to create an external user profile to access teleconsultation without being an Alan member.

    Args:
        onboarding_data: The onboarding user data
        profile_service: The profile service to update birth information

    Returns:
        A tuple containing the user object and refresh token type

    Raises:
        BaseErrorCode: If gender validation fails
        SSNValidationError: If SSN validation fails
    """

currency class-attribute instance-attribute

currency = EUR

get_app_base_user_data abstractmethod

get_app_base_user_data(app_user_id)

Get the user data base for the clinic. This is a subset of the user data, used when only the basic user data is needed. It prevents the need to load all the user data when only the basic user data is needed.

Parameters:

Name Type Description Default
app_user_id str

The user ID

required
compute_key_account_info

Whether to compute key account info for the user

required
Source code in components/clinic/public/dependencies.py
@abstractmethod
def get_app_base_user_data(self, app_user_id: str) -> BaseUserData:
    """Get the user data base for the clinic.
    This is a subset of the user data, used when only the basic user data is needed.
    It prevents the need to load all the user data when only the basic user data is needed.

    Args:
        app_user_id: The user ID
        compute_key_account_info: Whether to compute key account info for the user
    """

get_app_user_available_health_services abstractmethod

get_app_user_available_health_services(app_user_id)

Get the available health services for the user

Parameters:

Name Type Description Default
app_user_id str

The user ID

required
Source code in components/clinic/public/dependencies.py
@abstractmethod
def get_app_user_available_health_services(
    self,
    app_user_id: str,
) -> list[AvailableHealthService]:
    """Get the available health services for the user

    Args:
        app_user_id: The user ID
    """

get_app_user_data abstractmethod

get_app_user_data(
    app_user_id, compute_key_account_info=False
)

Get the user data for the clinic

Parameters:

Name Type Description Default
app_user_id str

The user ID

required
compute_key_account_info bool

Whether to compute key account info for the user

False
Source code in components/clinic/public/dependencies.py
@abstractmethod
def get_app_user_data(
    self,
    app_user_id: str,
    compute_key_account_info: bool = False,
) -> UserData:
    """Get the user data for the clinic

    Args:
        app_user_id: The user ID
        compute_key_account_info: Whether to compute key account info for the user
    """

get_booking_session_package abstractmethod

get_booking_session_package(app_user_id, session_type)

Get the pricing for the booking session for the user

Parameters:

Name Type Description Default
app_user_id str

The user ID

required
session_type TherapistBookingSessionType

The session type

required

Returns:

Type Description
BookingSessionPackage | None

The booking session package

BookingSessionPackage | None

If None is returned, the session is free

Source code in components/clinic/public/dependencies.py
@abstractmethod
def get_booking_session_package(
    self,
    app_user_id: str,
    session_type: TherapistBookingSessionType,
) -> BookingSessionPackage | None:
    """Get the pricing for the booking session for the user

    Args:
        app_user_id: The user ID
        session_type: The session type


    Returns:
        The booking session package
        If None is returned, the session is free
    """

get_coverage_status abstractmethod

get_coverage_status(app_user_id)

Return the start and optionally the end date of the current or upcoming period of eligibility to the clinic restricted services.

Parameters:

Name Type Description Default
app_user_id str

The user ID

required
Source code in components/clinic/public/dependencies.py
@abstractmethod
def get_coverage_status(self, app_user_id: str) -> CoverageStatus | None:
    """Return the start and optionally the end date of the current or upcoming period of eligibility to the clinic restricted services.

    Args:
        app_user_id: The user ID
    """

get_inactive_profile_ids_since abstractmethod

get_inactive_profile_ids_since(period_in_years)

Return profile IDs inactive for the given period. Used as GDPR deletion candidates.

Parameters:

Name Type Description Default
period_in_years int

Number of years of inactivity

required
Source code in components/clinic/public/dependencies.py
@abstractmethod
def get_inactive_profile_ids_since(self, period_in_years: int) -> list[UUID]:
    """Return profile IDs inactive for the given period. Used as GDPR deletion candidates.

    Args:
        period_in_years: Number of years of inactivity
    """

get_inactive_user_ids_since abstractmethod

get_inactive_user_ids_since(period_in_years)

Return app user IDs inactive for the given period. Used as GDPR deletion candidates.

Parameters:

Name Type Description Default
period_in_years int

Number of years of inactivity

required
Source code in components/clinic/public/dependencies.py
@abstractmethod
def get_inactive_user_ids_since(self, period_in_years: int) -> list[str]:
    """Return app user IDs inactive for the given period. Used as GDPR deletion candidates.

    Args:
        period_in_years: Number of years of inactivity
    """

get_last_active_id_verification_request_for_user abstractmethod

get_last_active_id_verification_request_for_user(
    app_user_id,
)

Get the last active ID verification request for the user

Parameters:

Name Type Description Default
app_user_id str

The user ID

required
Source code in components/clinic/public/dependencies.py
@abstractmethod
def get_last_active_id_verification_request_for_user(
    self, app_user_id: str
) -> IDVerificationRequest | None:
    """Get the last active ID verification request for the user

    Args:
        app_user_id: The user ID
    """

get_orientation_survey_url abstractmethod

get_orientation_survey_url(lang)

Get the orientation survey dataroom URL for the given language.

Parameters:

Name Type Description Default
lang str

The user's language code (e.g., 'en', 'fr', 'es', 'nl')

required
Source code in components/clinic/public/dependencies.py
@abstractmethod
def get_orientation_survey_url(self, lang: str) -> str:
    """Get the orientation survey dataroom URL for the given language.

    Args:
        lang: The user's language code (e.g., 'en', 'fr', 'es', 'nl')
    """

has_access_to_orientation_call abstractmethod

has_access_to_orientation_call(app_user_id)

Specifically check if the user has access to the orientation call in countries specifc code

Parameters:

Name Type Description Default
app_user_id str

The user ID

required
Source code in components/clinic/public/dependencies.py
@abstractmethod
def has_access_to_orientation_call(
    self,
    app_user_id: str,
) -> bool:
    """Specifically check if the user has access to the orientation call in countries specifc code

    Args:
        app_user_id: The user ID
    """

has_app_user_permission abstractmethod

has_app_user_permission(app_user_id, permission)

Check if the user has an employee permission

Parameters:

Name Type Description Default
app_user_id str

The user ID

required
permission EmployeePermission

The permission

required
Source code in components/clinic/public/dependencies.py
@abstractmethod
def has_app_user_permission(
    self,
    app_user_id: str,
    permission: EmployeePermission,
) -> bool:
    """Check if the user has an employee permission

    Args:
        app_user_id: The user ID
        permission: The permission
    """

is_app_user_admin_of_company abstractmethod

is_app_user_admin_of_company(app_user_id, app_company_id)

Check if the user is an admin of the company

Parameters:

Name Type Description Default
app_user_id str

The user ID

required
app_company_id str

The company ID

required
Source code in components/clinic/public/dependencies.py
@abstractmethod
def is_app_user_admin_of_company(
    self,
    app_user_id: str,
    app_company_id: str,
) -> bool:
    """Check if the user is an admin of the company

    Args:
        app_user_id: The user ID
        app_company_id: The company ID
    """

is_orientation_session_mandatory abstractmethod

is_orientation_session_mandatory()

Check if orientation session is mandatory in this country

Returns:

Type Description
bool

True if orientation session is mandatory, False otherwise

Source code in components/clinic/public/dependencies.py
@abstractmethod
def is_orientation_session_mandatory(self) -> bool:
    """Check if orientation session is mandatory in this country

    Returns:
        True if orientation session is mandatory, False otherwise
    """

release_date_of_conversations_created_for_therapy_sessions class-attribute instance-attribute

release_date_of_conversations_created_for_therapy_sessions = (
    None
)

request_id_verification_request_for_user abstractmethod

request_id_verification_request_for_user(
    app_user_id, user_info, commit=True
)

Get or request ID verification request for the user

Parameters:

Name Type Description Default
app_user_id str

The user ID

required
user_info ClinicUserDataForIdVerification

The user info for the ID verification request

required
commit bool

Whether to commit the changes

True
Source code in components/clinic/public/dependencies.py
@abstractmethod
def request_id_verification_request_for_user(
    self,
    app_user_id: str,
    user_info: ClinicUserDataForIdVerification,
    commit: bool = True,
) -> IDVerificationRequest | None:
    """Get or request ID verification request for the user

    Args:
        app_user_id: The user ID
        user_info: The user info for the ID verification request
        commit: Whether to commit the changes
    """

should_request_id_verification_for_user abstractmethod

should_request_id_verification_for_user(app_user_id)

Check if the ID verification should be requested for the user

Parameters:

Name Type Description Default
app_user_id str

The user ID

required
Source code in components/clinic/public/dependencies.py
@abstractmethod
def should_request_id_verification_for_user(
    self,
    app_user_id: str,
) -> bool:
    """Check if the ID verification should be requested for the user

    Args:
        app_user_id: The user ID
    """

should_send_orientation_survey_to_member class-attribute instance-attribute

should_send_orientation_survey_to_member = False

stripe_api_key class-attribute instance-attribute

stripe_api_key = 'STRIPE_EU_THERAPIST_BOOKING_API_KEY'

stripe_api_key_secret_name class-attribute instance-attribute

stripe_api_key_secret_name = (
    "STRIPE_EU_THERAPIST_BOOKING_API_KEY_SECRET_NAME"
)

stripe_webhook_secret class-attribute instance-attribute

stripe_webhook_secret = (
    "STRIPE_EU_THERAPIST_BOOKING_WEBHOOK_SECRET"
)

stripe_webhook_secret_name class-attribute instance-attribute

stripe_webhook_secret_name = (
    "STRIPE_EU_THERAPIST_BOOKING_WEBHOOK_SECRET_SECRET_NAME"
)

The date when the clinic consent AI was published. The value is None if the clinic consent AI is not published.

therapy_refundable_cancellation_limit_in_hours abstractmethod

therapy_refundable_cancellation_limit_in_hours()

Returns the minimum hours before session start required for a cancellation to be refundable.

Returns:

Type Description
int | None

The minimum hours before session start required for refundable cancellation, or None if no limit applies

Source code in components/clinic/public/dependencies.py
@abstractmethod
def therapy_refundable_cancellation_limit_in_hours(self) -> int | None:
    """Returns the minimum hours before session start required for a cancellation to be refundable.

    Returns:
        The minimum hours before session start required for refundable cancellation, or None if no limit applies
    """

update_app_user_phone abstractmethod

update_app_user_phone(app_user_id, phone)

Update the phone number of the user

Parameters:

Name Type Description Default
app_user_id str

The user ID

required
phone str | None

The phone number

required
Source code in components/clinic/public/dependencies.py
@abstractmethod
def update_app_user_phone(
    self,
    app_user_id: str,
    phone: str | None,
) -> None:
    """Update the phone number of the user

    Args:
        app_user_id: The user ID
        phone: The phone number
    """

update_app_user_ssn abstractmethod

update_app_user_ssn(app_user_id, ssn, commit=False)

Update the SSN of the user

Parameters:

Name Type Description Default
app_user_id str

The user ID

required
ssn str | None

The SSN

required
commit bool

Whether to commit the changes

False
Source code in components/clinic/public/dependencies.py
@abstractmethod
def update_app_user_ssn(
    self,
    app_user_id: str,
    ssn: str | None,
    commit: bool = False,
) -> None:
    """Update the SSN of the user

    Args:
        app_user_id: The user ID
        ssn: The SSN
        commit: Whether to commit the changes
    """

upload_invoice_as_insurance_document abstractmethod

upload_invoice_as_insurance_document(
    file, app_user_id, upload_invoice_data
)

Upload the invoice as an insurance document if relevant

Parameters:

Name Type Description Default
file IO

The file

required
app_user_id str

The user ID

required
upload_invoice_data UploadInvoiceData

The upload invoice data

required
Source code in components/clinic/public/dependencies.py
@abstractmethod
def upload_invoice_as_insurance_document(
    self,
    file: IO,  # type: ignore[type-arg]
    app_user_id: str,
    upload_invoice_data: UploadInvoiceData,
) -> bool:
    """Upload the invoice as an insurance document if relevant

    Args:
        file: The file
        app_user_id: The user ID
        upload_invoice_data: The upload invoice data
    """

user_has_24_hour_response_guarantee abstractmethod

user_has_24_hour_response_guarantee(app_user_id)

Check if the user has a 24-hour response guarantee (usually based on the company)

Parameters:

Name Type Description Default
app_user_id str

The user ID

required
Source code in components/clinic/public/dependencies.py
@abstractmethod
def user_has_24_hour_response_guarantee(
    self,
    app_user_id: str,
) -> bool:
    """Check if the user has a 24-hour response guarantee (usually based on the company)

    Args:
        app_user_id: The user ID
    """

validate_session_duration abstractmethod

validate_session_duration(session_duration)

Validate the session duration, throwing an error if it's not valid

Parameters:

Name Type Description Default
session_duration int

The session duration

required
Source code in components/clinic/public/dependencies.py
@abstractmethod
def validate_session_duration(
    self,
    session_duration: int,
) -> None:
    """Validate the session duration, throwing an error if it's not valid

    Args:
        session_duration: The session duration
    """

ClinicUserDataForIdVerification dataclass

ClinicUserDataForIdVerification(
    first_name,
    additional_first_names,
    last_name,
    birth_last_name,
    email,
    gender,
    date_of_birth,
    place_of_birth,
    session_id=None,
)

Data used for ID verification request for the clinic user

Attributes:

Name Type Description
first_name str

The first name of the user

last_name str

The last name of the user

email str

The email of the user

birth_last_name str

The birth last name of the user

place_of_birth str

The place of birth of the user (it's a string representing usually a city or town, to match with the information on ID cards or passports.)

date_of_birth date

The date of birth of the user

gender str

The gender of the user

additional_first_names instance-attribute

additional_first_names

birth_last_name instance-attribute

birth_last_name

date_of_birth instance-attribute

date_of_birth

email instance-attribute

email

first_name instance-attribute

first_name

gender instance-attribute

gender

last_name instance-attribute

last_name

place_of_birth instance-attribute

place_of_birth

session_id class-attribute instance-attribute

session_id = None

Dependent dataclass

Dependent(
    app_user_id,
    first_name,
    last_name,
    gender,
    age,
    birth_date,
    dependent_type,
)

Dependent of a user, used to represent a family member

age instance-attribute

age

app_user_id instance-attribute

app_user_id

birth_date instance-attribute

birth_date

dependent_type instance-attribute

dependent_type

first_name instance-attribute

first_name

gender instance-attribute

gender

last_name instance-attribute

last_name

DependentType

Bases: AlanBaseEnum

Type of dependent

CHILD class-attribute instance-attribute

CHILD = 'child'

PARTNER class-attribute instance-attribute

PARTNER = 'partner'

UploadInvoiceData dataclass

UploadInvoiceData(
    session_id,
    starts_at,
    ends_at,
    paid_amount,
    medical_admin_identifier,
    medical_admin_name,
)

Data used to upload an invoice as an insurance document

Attributes:

Name Type Description
session_id UUID

The session ID

starts_at date

The start date

ends_at date

The end date

paid_amount float

The paid amount

medical_admin_identifier Optional[str]

The medical admin identifier

medical_admin_name str

The medical admin name

ends_at instance-attribute

ends_at

medical_admin_identifier instance-attribute

medical_admin_identifier

medical_admin_name instance-attribute

medical_admin_name

paid_amount instance-attribute

paid_amount

session_id instance-attribute

session_id

starts_at instance-attribute

starts_at

UserData dataclass

UserData(
    first_name,
    last_name,
    gender,
    email,
    profile_id,
    birth_date,
    phone,
    country,
    address,
    ssn,
    lang,
    is_alaner,
    dependents,
    is_key_account_or_large_company_and_not_alaner=False,
    administrative_area=None,
)

Bases: BaseUserData

Full user data necessary for the clinic

Attributes:

Name Type Description
first_name str | None

First name

last_name str | None

Last name

gender UserGender | None

Gender

email str | None

Email

profile_id UUID

Profile ID

birth_date date | None

Birth date

phone str | None

Phone number

country str | None

Country

address str | None

Address

ssn str | None

Social security number

lang Lang

Language

is_alaner bool

Whether the user is an Alan employee

dependents list[Dependent]

List of dependents

is_key_account_or_large_company_and_not_alaner bool

Whether the user is a key account or large company and not an Alan employee

address instance-attribute

address

administrative_area class-attribute instance-attribute

administrative_area = None

Used in Canada only for now

birth_date instance-attribute

birth_date

country instance-attribute

country

dependents instance-attribute

dependents

email instance-attribute

email

gender instance-attribute

gender

is_alaner instance-attribute

is_alaner

is_key_account_or_large_company_and_not_alaner class-attribute instance-attribute

is_key_account_or_large_company_and_not_alaner = False

Used only for the alpha (and perhaps beta) releases of Mo-in-Clinic. To be removed afterwards.

lang instance-attribute

lang

phone instance-attribute

phone

profile_id instance-attribute

profile_id

ssn instance-attribute

ssn

get_app_dependency

get_app_dependency(app_name)

Get the ClinicAdapter dependency depends on the app name of the user

The app name needs to be passed because the clinic can call the logic from a country to a different one

Ex: Doctors are connected to the FR instance. This instance needs to call BE, ES, CA logic and data

Parameters:

Name Type Description Default
app_name str

The app name

required
Source code in components/clinic/public/dependencies.py
def get_app_dependency(app_name: str) -> ClinicAdapter:
    """
    Get the ClinicAdapter dependency depends on the app name of the user

    The app name needs to be passed because the clinic can call the logic from a country to a different one

    Ex: Doctors are connected to the FR instance. This instance needs to call BE, ES, CA logic and data

    Args:
        app_name: The app name
    """
    match AppName(app_name):
        case AppName.ALAN_FR:
            from components.fr.public.clinic.adapter import (
                clinic_adapter as fr_clinic_adapter,
            )

            return fr_clinic_adapter

        case AppName.ALAN_BE:
            from components.be.public.clinic.adapter import (
                clinic_adapter as be_clinic_adapter,
            )

            return be_clinic_adapter

        case AppName.ALAN_ES:
            from components.es.public.clinic.adapter import (
                clinic_adapter as es_clinic_adapter,
            )

            return es_clinic_adapter

        case AppName.ALAN_CA:
            from components.ca.public.clinic.adapter import (
                clinic_adapter as ca_clinic_adapter,
            )

            return ca_clinic_adapter

        case _:
            raise ValueError(f"Unknown app id {app_name}")

components.clinic.public.entities

available_health_service

AvailableHealthService dataclass

AvailableHealthService(
    name,
    has_access=None,
    is_recommended=None,
    has_upcoming_availability=None,
)

Bases: DataClassJsonMixin

Available health service available for a clinic user

has_access class-attribute instance-attribute
has_access = None
has_upcoming_availability class-attribute instance-attribute
has_upcoming_availability = None
is_recommended = None
name instance-attribute
name

external_onboarding_user

ExternalOnboardingUserData dataclass

ExternalOnboardingUserData(
    email,
    first_name,
    last_name,
    phone_number,
    password,
    prehashed_password,
    gender_str,
    date_of_birth,
    social_security_number,
    place_of_birth_name,
    place_of_birth_postal_code,
    terms_accepted,
    refresh_token_type,
    client_id=None,
    birth_last_name=None,
)

External onboarding user data. Attributes: email: The email of the user. first_name: The first name of the user. last_name: The last name of the user. phone_number: The phone number of the user. password: The password of the user. prehashed_password: The prehashed password of the user. gender_str: The gender of the user. date_of_birth: The date of birth of the user. social_security_number: The social security number of the user. place_of_birth_name: The name of the place of birth of the user. place_of_birth_postal_code: The postal code of the place of birth of the user. terms_accepted: Whether the user has accepted the terms and conditions. refresh_token_type: The refresh token type of the user. client_id: The Keycloak client ID for email verification flow. birth_last_name: The birth last name of the user (maiden name). Defaults to last_name if not provided.

birth_last_name class-attribute instance-attribute
birth_last_name = None
client_id class-attribute instance-attribute
client_id = None
date_of_birth instance-attribute
date_of_birth
email instance-attribute
email
first_name instance-attribute
first_name
gender_str instance-attribute
gender_str
last_name instance-attribute
last_name
password instance-attribute
password
phone_number instance-attribute
phone_number
place_of_birth_name instance-attribute
place_of_birth_name
place_of_birth_postal_code instance-attribute
place_of_birth_postal_code
prehashed_password instance-attribute
prehashed_password
refresh_token_type instance-attribute
refresh_token_type
social_security_number instance-attribute
social_security_number
terms_accepted instance-attribute
terms_accepted

components.clinic.public.enums

available_health_service_name

AvailableHealthServiceName

Bases: AlanBaseEnum

Health services available to members that can be shared to them in the clinic

DATO_CONTENT class-attribute instance-attribute
DATO_CONTENT = 'dato_content'
HEALTH_PROGRAM class-attribute instance-attribute
HEALTH_PROGRAM = 'health_program'
ORIENTATION_CALL class-attribute instance-attribute
ORIENTATION_CALL = 'orientation_call'
THERAPIST class-attribute instance-attribute
THERAPIST = 'therapist'
THERAPY_SESSION class-attribute instance-attribute
THERAPY_SESSION = 'therapy_session'
VIDEO_CONSULTATION class-attribute instance-attribute
VIDEO_CONSULTATION = 'video_consultation'
VIDEO_CONSULTATION_LIVI class-attribute instance-attribute
VIDEO_CONSULTATION_LIVI = 'video_consultation_livi'

pricing

ClinicInvoiceAmountCurrency

Bases: AlanBaseEnum

CAD class-attribute instance-attribute
CAD = 'CAD'
EUR class-attribute instance-attribute
EUR = 'EUR'
GBP class-attribute instance-attribute
GBP = 'GBP'
format_amount
format_amount(amount_in_cents)
Source code in components/clinic/internal/enums/clinic_invoice_amount_currency.py
def format_amount(self, amount_in_cents: int) -> str:
    amount = Decimal(amount_in_cents) / 100
    if self == self.EUR:
        return format_currency(amount, "EUR", locale="fr_FR")
    elif self == self.GBP:
        return format_currency(amount, "GBP", locale="en_GB")
    elif self == self.CAD:
        return format_currency(amount, "CAD", locale="en_CA")
    else:
        raise ValueError(f"Unsupported currency: {self}")

regions

CaAdministrativeAreas

Bases: AlanBaseEnum

Canadian provinces and territories.

Used to specify which administrative regions a medical admin is licensed to operate in. Values use ISO 3166-2:CA codes (CA- prefix + 2-letter region code). -

alberta class-attribute instance-attribute
alberta = 'CA-AB'
british_columbia class-attribute instance-attribute
british_columbia = 'CA-BC'
manitoba class-attribute instance-attribute
manitoba = 'CA-MB'
new_brunswick class-attribute instance-attribute
new_brunswick = 'CA-NB'
newfoundland_and_labrador class-attribute instance-attribute
newfoundland_and_labrador = 'CA-NL'
northwest_territories class-attribute instance-attribute
northwest_territories = 'CA-NT'
nova_scotia class-attribute instance-attribute
nova_scotia = 'CA-NS'
nunavut class-attribute instance-attribute
nunavut = 'CA-NU'
ontario class-attribute instance-attribute
ontario = 'CA-ON'
prince_edward_island class-attribute instance-attribute
prince_edward_island = 'CA-PE'
quebec class-attribute instance-attribute
quebec = 'CA-QC'
saskatchewan class-attribute instance-attribute
saskatchewan = 'CA-SK'
yukon class-attribute instance-attribute
yukon = 'CA-YT'

therapist_booking_session_type

TherapistBookingSessionType

Bases: AlanBaseEnum

Type of booking session supported by the clinic booking experience

consultation class-attribute instance-attribute
consultation = 'consultation'
dermatology class-attribute instance-attribute
dermatology = 'dermatology'
emergency_physiotherapy class-attribute instance-attribute
emergency_physiotherapy = 'emergency_physiotherapy'
orientation class-attribute instance-attribute
orientation = 'orientation'
physiotherapy class-attribute instance-attribute
physiotherapy = 'physiotherapy'
therapy class-attribute instance-attribute
therapy = 'therapy'

components.clinic.public.events

events

ClinicUserDataDeleted dataclass

ClinicUserDataDeleted(user_id, app_id)

Bases: Message

Published after a clinic user and all associated data have been deleted.

Other components should subscribe to clean up their own data linked to this user.

app_id instance-attribute
app_id
user_id instance-attribute
user_id

subscription

subscribe_to_events

subscribe_to_events()

All event subscriptions for the Clinic should be done here.

Source code in components/clinic/public/events/subscription.py
def subscribe_to_events() -> None:
    """
    All event subscriptions for the Clinic should be done here.
    """
    from components.clinic.internal.booking.business_logic.events_subscribers import (
        configure_medical_conversation_when_booking_sessions,
        setup_conversation_for_therapy_session,
        setup_lifecycle_tracking_for_booked_session_from_mo_redirection,
        track_lifecycle_event_for_session_from_mo_redirection,
        trigger_insi_create_or_update_from_id_verification_request,
    )
    from components.clinic.internal.booking.entities.events import (
        SessionCreatedForInternalTeleconsultation,
        SessionCreatedForTherapy,
        SessionRecommendedByMoWasBooked,
        SessionRecommendedByMoWasUpdated,
        TriggerINSiUpdate,
    )
    from components.clinic.internal.business_logic.identity.events_subscribers import (
        update_insi_identity_status_following_id_check,
    )
    from components.id_verification.public.events import (
        IdVerificationValidatedForClinic,
        IdVerificationValidatedForExternalOnboarding,
    )

    message_broker = get_message_broker()
    message_broker.subscribe_async(
        IdVerificationValidatedForClinic,
        update_insi_identity_status_following_id_check,
        queue_name=LOW_PRIORITY_QUEUE,
    )
    message_broker.subscribe_async(
        IdVerificationValidatedForExternalOnboarding,
        update_insi_identity_status_following_id_check,
        queue_name=LOW_PRIORITY_QUEUE,
    )
    message_broker.subscribe_async(
        SessionCreatedForInternalTeleconsultation,
        configure_medical_conversation_when_booking_sessions,
        queue_name=LOW_PRIORITY_QUEUE,
    )
    message_broker.subscribe_async(
        SessionCreatedForTherapy,
        setup_conversation_for_therapy_session,
        queue_name=LOW_PRIORITY_QUEUE,
    )
    message_broker.subscribe_async(
        TriggerINSiUpdate,
        trigger_insi_create_or_update_from_id_verification_request,
        queue_name=LOW_PRIORITY_QUEUE,
    )
    message_broker.subscribe_async(
        SessionRecommendedByMoWasBooked,
        setup_lifecycle_tracking_for_booked_session_from_mo_redirection,
        queue_name=LOW_PRIORITY_QUEUE,
    )
    message_broker.subscribe_async(
        SessionRecommendedByMoWasUpdated,
        track_lifecycle_event_for_session_from_mo_redirection,
        queue_name=LOW_PRIORITY_QUEUE,
    )