Skip to content

Api reference

components.gamification.public.business_logic

actions

player

merge_users_in_gamification_component
merge_users_in_gamification_component(
    source_user_id, target_user_id, commit=True
)

Support function for the "merge user" procedure: changes the source player user id to the target user id.

The targeted user id cannot already be a player, in this case this will fail

Source code in components/gamification/internal/business_logic/actions/player.py
def merge_users_in_gamification_component(
    source_user_id: str,
    target_user_id: str,
    commit: bool = True,
) -> None:
    """
    Support function for the "merge user" procedure: changes the source player user id
    to the target user id.

    The targeted user id cannot already be a player, in this case this will fail
    """
    from components.gamification.internal.models.player import Player

    source_player = current_session.scalars(
        select(Player).where(Player.app_user_id == source_user_id)
    ).one_or_none()

    if source_player:
        source_player.app_user_id = target_user_id

        if commit:
            current_session.commit()
        else:
            current_session.flush()

        current_logger.info(
            f"Updated player {source_player.id} app user id to {target_user_id}"
        )
    else:
        current_logger.info(f"No player found for {source_user_id}")

components.gamification.public.commands

app_group

gamification_commands module-attribute

gamification_commands = AppGroup('gamification')

backfill_favorite_categories

backfill_favorite_categories

backfill_favorite_categories(dry_run)

Backfill favorite_challenges for players with NULL values.

Source code in components/gamification/public/commands/backfill_favorite_categories.py
@gamification_commands.command()
@command_with_dry_run
def backfill_favorite_categories(dry_run: bool) -> None:
    """Backfill favorite_challenges for players with NULL values."""
    from components.gamification.internal.business_logic.actions.backfill_favorite_categories import (
        backfill_favorite_categories,
    )

    backfill_favorite_categories(dry_run=dry_run)

cleanup_duels

cleanup_duels

cleanup_duels(dry_run)

Cleanup duels that never had winners (usually if the player never opens the duel after results are ready) This also refunds both players

Source code in components/gamification/public/commands/cleanup_duels.py
@gamification_commands.command()
@command_with_dry_run
def cleanup_duels(dry_run: bool) -> None:
    """
    Cleanup duels that never had winners (usually if the player never opens the duel after results are ready)
    This also refunds both players
    """
    from components.gamification.internal.business_logic.actions.duels import (
        cleanup_old_duels_without_winners,
    )

    cleanup_old_duels_without_winners(
        dry_run=dry_run, older_than_timedelta=timedelta(days=5)
    )

cleanup_players

cleanup_players

cleanup_players(dry_run)

Cleanup players (eg. those who are not insured anymore)

Source code in components/gamification/public/commands/cleanup_players.py
@gamification_commands.command()
@command_with_dry_run
def cleanup_players(dry_run: bool) -> None:
    """
    Cleanup players (eg. those who are not insured anymore)
    """
    from components.gamification.internal.business_logic.actions.player import (
        cleanup_players,
    )

    cleanup_players(dry_run=dry_run)

compute_month_winners

compute_month_winners

compute_month_winners(
    dry_run,
    app_id,
    expected_paris_hour=None,
    expected_toronto_hour=None,
)

Compute monthly winners (team challenge)

Source code in components/gamification/public/commands/compute_month_winners.py
@gamification_commands.command()
@command_with_dry_run
@click.option("--app-id", type=click.STRING, required=True)
@click.option("--expected-paris-hour", type=click.INT, required=False)
@click.option("--expected-toronto-hour", type=click.INT, required=False)
def compute_month_winners(
    dry_run: bool,
    app_id: str,
    expected_paris_hour: Optional[int] = None,
    expected_toronto_hour: Optional[int] = None,
) -> None:
    """
    Compute monthly winners (team challenge)
    """
    from components.gamification.internal.business_logic.actions.team_challenge import (
        compute_team_challenge_winners,
    )

    # Check it's expected time and last day of the month

    if expected_paris_hour:
        paris_time = datetime.now(ZoneInfo("Europe/Paris"))
        if expected_paris_hour != paris_time.hour:
            current_logger.info(
                "Not expected Paris hour for winners consolidation",
                paris_current=paris_time.hour,
                paris_expected=expected_paris_hour,
            )
            return
        if paris_time.month == (paris_time + timedelta(days=1)).month:
            current_logger.info(
                "Not the last day of the month in Paris timezone",
                current_paris_date=paris_time.date(),
            )
            return
    if expected_toronto_hour:
        toronto_time = datetime.now(ZoneInfo("America/Toronto"))
        if expected_toronto_hour != toronto_time.hour:
            current_logger.info(
                "Not expected Toronto hour for winners consolidation",
                toronto_current=toronto_time.hour,
                toronto_expected=expected_toronto_hour,
            )
            return
        if toronto_time.month == (toronto_time + timedelta(days=1)).month:
            current_logger.info(
                "Not the last day of the month in Paris timezone",
                current_paris_date=toronto_time.date(),
            )
            return

    compute_team_challenge_winners(app_id=app_id, dry_run=dry_run)

compute_week_winners

compute_weekly_global_podium

compute_weekly_global_podium(
    dry_run, expected_paris_hour=None
)

Find the global podium for the week amongst all alan members

Source code in components/gamification/public/commands/compute_week_winners.py
@gamification_commands.command()
@command_with_dry_run
@click.option("--expected-paris-hour", type=click.INT, required=False)
def compute_weekly_global_podium(
    dry_run: bool, expected_paris_hour: Optional[int] = None
) -> None:
    """
    Find the global podium for the week amongst all alan members
    """
    from components.gamification.internal.business_logic.actions.walk_history import (
        compute_and_notify_weekly_global_podium,
    )

    paris_hour = datetime.now(ZoneInfo("Europe/Paris")).hour

    current_logger.info(
        "Weekly global podium calculations",
        paris_current=paris_hour,
        paris_expected=expected_paris_hour,
    )

    # The command is scheduled twice (at 18h and 19h UTC) to support summer time change because we want it to execute
    # exactly at 20h in reference time whatever the month, while crons declaration only support UTC
    if expected_paris_hour and expected_paris_hour != paris_hour:
        current_logger.warn(
            "Not expected Paris hour for global weekly podium calculations"
        )
        return

    compute_and_notify_weekly_global_podium(dry_run=dry_run)

compute_weekly_leaderboard_winners

compute_weekly_leaderboard_winners(
    dry_run,
    app_id,
    expected_paris_hour=None,
    expected_toronto_hour=None,
)

Compute week winners (league leaderboards)

Source code in components/gamification/public/commands/compute_week_winners.py
@gamification_commands.command()
@command_with_dry_run
@click.option("--app-id", type=click.STRING, required=True)
@click.option("--expected-paris-hour", type=click.INT, required=False)
@click.option("--expected-toronto-hour", type=click.INT, required=False)
def compute_weekly_leaderboard_winners(
    dry_run: bool,
    app_id: str,
    expected_paris_hour: Optional[int] = None,
    expected_toronto_hour: Optional[int] = None,
) -> None:
    """
    Compute week winners (league leaderboards)
    """
    from components.gamification.internal.business_logic.actions.leagues_records import (
        refresh_weekly_leaderboard_records as _refresh_weekly_leaderboard_records,
    )
    from components.gamification.internal.business_logic.actions.leagues_winners import (
        compute_weekly_leaderboard_winners as _compute_weekly_leaderboard_winners,
    )

    # Check it's expected time

    if expected_paris_hour:
        paris_hour = datetime.now(ZoneInfo("Europe/Paris")).hour
        if expected_paris_hour != paris_hour:
            current_logger.warn(
                "Not expected Paris hour for winners consolidation",
                paris_current=paris_hour,
                paris_expected=expected_paris_hour,
            )
            return
    if expected_toronto_hour:
        toronto_hour = datetime.now(ZoneInfo("America/Toronto")).hour
        if expected_toronto_hour != toronto_hour:
            current_logger.warn(
                "Not expected Toronto hour for winners consolidation",
                toronto_current=toronto_hour,
                toronto_expected=expected_toronto_hour,
            )
            return

    _compute_weekly_leaderboard_winners(app_id=app_id, dry_run=dry_run)
    _refresh_weekly_leaderboard_records(app_id=app_id, dry_run=dry_run)

compute_weekly_leaderboard_winners_retroactively

compute_weekly_leaderboard_winners_retroactively(
    dry_run, app_id, day
)

Compute weekly leaderboard winners retroactively

Source code in components/gamification/public/commands/compute_week_winners.py
@gamification_commands.command()
@command_with_dry_run
@click.option("--app-id", type=click.STRING, required=True)
@click.option("--day", type=click.STRING, required=True)
def compute_weekly_leaderboard_winners_retroactively(
    dry_run: bool, app_id: str, day: str
) -> None:
    """
    Compute weekly leaderboard winners retroactively
    """
    from components.gamification.internal.business_logic.actions.leagues_winners import (
        compute_weekly_leaderboard_winners_retroactively as _compute_weekly_leaderboard_winners_retroactively,
    )

    _compute_weekly_leaderboard_winners_retroactively(
        dry_run=dry_run, app_id=app_id, day=isoparse(day)
    )

refresh_weekly_leaderboard_records_retroactively

refresh_weekly_leaderboard_records_retroactively(
    dry_run, app_id, day
)

Refresh leaderboards records retroactively

Source code in components/gamification/public/commands/compute_week_winners.py
@gamification_commands.command()
@command_with_dry_run
@click.option("--app-id", type=click.STRING, required=True)
@click.option("--day", type=click.STRING, required=True)
def refresh_weekly_leaderboard_records_retroactively(
    dry_run: bool, app_id: str, day: str
) -> None:
    """
    Refresh leaderboards records retroactively
    """
    from components.gamification.internal.business_logic.actions.leagues_records import (
        refresh_weekly_leaderboard_records_retroactively as _refresh_weekly_leaderboard_records_retroactively,
    )

    _refresh_weekly_leaderboard_records_retroactively(
        dry_run=dry_run, app_id=app_id, day=isoparse(day)
    )

delete_old_achievements

delete_old_achievements

delete_old_achievements(dry_run)

Delete old achievements

Source code in components/gamification/public/commands/delete_old_achievements.py
@gamification_commands.command()
@command_with_dry_run
def delete_old_achievements(dry_run: bool) -> None:
    """
    Delete old achievements
    """
    from components.gamification.internal.business_logic.actions.achievements import (
        delete_old_achievements as _delete_old_achievements,
    )

    # Uses real calendar months: e.g. on April 15, deletes achievements achieved before January 1.

    today = date.today()
    month = today.month - 3
    year = today.year
    if month <= 0:
        month += 12
        year -= 1
    cutoff_date = date(year, month, 1)

    _delete_old_achievements(dry_run=dry_run, cutoff_date=cutoff_date)

eligibility

COUNTRIES_TO_APP_ID module-attribute

COUNTRIES_TO_APP_ID = {
    "be": ALAN_BE,
    "ca": ALAN_CA,
    "es": ALAN_ES,
    "fr": ALAN_FR,
}

check_gamification_configuration

check_gamification_configuration(dry_run=False)

Check gamification configurations

Source code in components/gamification/public/commands/eligibility.py
@gamification_commands.command()
@command_with_dry_run
def check_gamification_configuration(dry_run: bool = False) -> None:
    """
    Check gamification configurations
    """
    _check_duel_configurations(dry_run=dry_run)
    _check_league_configurations(dry_run=dry_run)

register_gamification_configurations

register_gamification_configurations(dry_run=False)

Register gamification configurations of future accounts or old accounts that were missed somehow

Source code in components/gamification/public/commands/eligibility.py
@gamification_commands.command()
@command_with_dry_run
def register_gamification_configurations(dry_run: bool = False) -> None:
    """
    Register gamification configurations of future accounts or old accounts that were missed somehow
    """
    _create_configurations(dry_run=dry_run)

reset_player_info

reset_player_info

reset_player_info(app_user_id, dry_run=True)

Removes all gamification info linked to a player following withdrawal of consent

Source code in components/gamification/public/commands/reset_player_info.py
@gamification_commands.command()
@command_with_dry_run
@click.option(
    "--app-user-id",
    type=click.STRING,
    required=True,
    help="App user ID of the player to clean up",
)
def reset_player_info(app_user_id: str, dry_run: bool = True) -> None:
    """
    Removes all gamification info linked to a player following withdrawal of consent
    """
    from components.gamification.public.services.player import remove_player_data

    remove_player_data(app_user_id, dry_run)

resync_players_preferences_with_customerio

resync_players_preferences_with_customerio

resync_players_preferences_with_customerio(dry_run=False)

Resync all players preferences with customer CIO

Source code in components/gamification/public/commands/resync_players_preferences_with_customerio.py
@gamification_commands.command()
@command_with_dry_run
def resync_players_preferences_with_customerio(dry_run: bool = False) -> None:
    """
    Resync all players preferences with customer CIO
    """
    from components.gamification.internal.models.player import Player
    from components.gamification.internal.services.tracking import gamification_events

    players = current_session.execute(
        select(
            Player.app_user_id,
            Player.walk_start_date,
            Player.enabled_duels_at,
            Player.enabled_reactions_at,
            Player.enabled_beneficiary_league_at,
            Player.enabled_marketing_notifications_at,
            Player.enabled_leaderboard_notifications_at,
            Player.enabled_berries_notifications_at,
            Player.enabled_social_notifications_at,
            Player.disabled_external_activity_notifications_at,
        )
        .filter(
            Player.walk_start_date.isnot(None),
        )
        .order_by(Player.created_at)
    ).all()

    count_walk_started = sum(1 for p in players if p[1] is not None)
    count_duels_enabled = sum(1 for p in players if p[2] is not None)
    count_reactions_enabled = sum(1 for p in players if p[3] is not None)
    count_beneficiary_leagues_enabled = sum(1 for p in players if p[4] is not None)
    count_marketing_notif_enabled = sum(1 for p in players if p[5] is not None)
    count_leaderboard_notif_enabled = sum(1 for p in players if p[6] is not None)
    count_berries_notif_enabled = sum(1 for p in players if p[7] is not None)
    count_social_notif_enabled = sum(1 for p in players if p[8] is not None)

    click.echo(
        f"Counts: {count_walk_started=} "
        f"{count_duels_enabled=}"
        f"{count_reactions_enabled=}"
        f"{count_beneficiary_leagues_enabled=}"
        f"{count_marketing_notif_enabled=}"
        f"{count_leaderboard_notif_enabled=}"
        f"{count_berries_notif_enabled=}"
        f"{count_social_notif_enabled=}"
    )

    for index, (
        app_user_id,
        _,
        enabled_duels_at,
        enabled_reactions_at,
        enabled_beneficiary_league_at,
        enabled_marketing_notifications_at,
        enabled_leaderboard_notifications_at,
        enabled_berries_notifications_at,
        enabled_social_notifications_at,
        disabled_external_activity_notifications_at,
    ) in enumerate(players):
        if (index + 1) % 1000 == 0:
            click.echo(f"PROCESSED {index + 1}")

        traits = {
            "duels_enabled": 1 if enabled_duels_at is not None else 0,
            "reactions_enabled": 1 if enabled_reactions_at is not None else 0,
            "beneficiary_league_enabled": 1
            if enabled_beneficiary_league_at is not None
            else 0,
            "walk_notifications_enabled": 1
            if enabled_marketing_notifications_at is not None
            else 0,
            "play_leaderboard_notifications_enabled": 1
            if enabled_leaderboard_notifications_at is not None
            else 0,
            "play_berries_notifications_enabled": 1
            if enabled_berries_notifications_at is not None
            else 0,
            "play_social_notifications_enabled": 1
            if enabled_social_notifications_at is not None
            else 0,
            "play_external_activity_notifications_disabled": 1
            if disabled_external_activity_notifications_at is not None
            else 0,
        }

        if dry_run:
            click.echo(f"Traits of {app_user_id}: {traits}")
        else:
            gamification_events._tracking_client.identify(  # noqa: ALN027
                user_id=app_user_id,
                traits=traits,
            )

team_challenge

auto_assign_unassigned_players_to_teams

auto_assign_unassigned_players_to_teams(
    dry_run, app_id, start_date
)

Auto-assign unassigned players to teams

Source code in components/gamification/public/commands/team_challenge.py
@gamification_commands.command()
@command_with_dry_run
@click.option("--app-id", type=click.STRING, required=True)
@click.option("--start-date", type=click.STRING, required=True)
def auto_assign_unassigned_players_to_teams(
    dry_run: bool, app_id: str, start_date: str
) -> None:
    """
    Auto-assign unassigned players to teams
    """
    from components.gamification.internal.business_logic.actions.team_challenge import (
        auto_assign_unassigned_players_to_teams as _auto_assign_unassigned_players_to_teams,
    )

    start_date_as_date = isoparse(start_date)

    _auto_assign_unassigned_players_to_teams(
        app_id=app_id, dry_run=dry_run, start_date=start_date_as_date
    )

cleanup_team_challenges

cleanup_team_challenges(dry_run=False)

Cleanup team challenges

For challenges that are started - cleanup teams that have been created automatically at initialization but have only one player assigned and are not customized

Source code in components/gamification/public/commands/team_challenge.py
@gamification_commands.command()
@command_with_dry_run
def cleanup_team_challenges(dry_run: bool = False) -> None:
    """
    Cleanup team challenges

    For challenges that are started
    - cleanup teams that have been created automatically at initialization but have only one player assigned and are not customized
    """
    from components.gamification.internal.business_logic.actions.team_challenge import (
        cleanup_team_challenges as _cleanup_team_challenges,
    )

    _cleanup_team_challenges(dry_run=dry_run)

initialize_team_challenges

initialize_team_challenges(dry_run=False)

Initialize team challenges

For accounts that are declared with a start date but not set yet: - initialize teams_size from number of enrolled players - generate default teams and captain assignation

Source code in components/gamification/public/commands/team_challenge.py
@gamification_commands.command()
@command_with_dry_run
def initialize_team_challenges(dry_run: bool = False) -> None:
    """
    Initialize team challenges

    For accounts that are declared with a start date but not set yet:
    - initialize teams_size from number of enrolled players
    - generate default teams and captain assignation
    """
    from components.gamification.internal.business_logic.actions.team_challenge import (
        initialize_team_challenges as _initialize_team_challenges,
    )

    _initialize_team_challenges(dry_run=dry_run)

vouchers

reconsolidate_gamification_vouchers_state

reconsolidate_gamification_vouchers_state(dry_run=False)

Reconsolidate gamification vouchers state based on shopify discounts state - If discount code appears in a not cancelled order, the command will ensure the related voucher is marked as used - If discount code appears in a cancelled order, the command will ensure the related voucher is re-regenerated and not marked as used

Source code in components/gamification/public/commands/vouchers.py
@gamification_commands.command()
@command_with_dry_run
def reconsolidate_gamification_vouchers_state(dry_run: bool = False) -> None:
    """
    Reconsolidate gamification vouchers state based on shopify discounts state
    - If discount code appears in a not cancelled order, the command will ensure the related voucher is marked as used
    - If discount code appears in a cancelled order, the command will ensure the related voucher is re-regenerated and not marked as used
    """
    from components.gamification.internal.helpers.vouchers import (
        VOUCHER_CODE_PROPS,
        generate_discount_code,
    )
    from components.gamification.internal.models.voucher import Voucher
    from components.gamification.public.enums.voucher import VoucherCode
    from components.shop.public.services.discounts import (
        create_shop_discount,
        get_processed_orders_with_discounts,
    )

    applied_discounts: list[str] = []
    cancelled_discounts: list[str] = []
    after = date.today() - timedelta(days=5)

    for order in get_processed_orders_with_discounts(after=after):
        discounts = order.discount_codes
        if discounts:
            if order.cancelled_at:
                cancelled_discounts.extend(discounts)
            else:
                applied_discounts.extend(discounts)

    count_applied_discounts = len(applied_discounts)
    count_cancelled_discounts = len(cancelled_discounts)
    if not count_applied_discounts and not count_cancelled_discounts:
        current_logger.info("No discounts to process")
        return

    current_logger.info(
        "Found discounts to process",
        count_applied=count_applied_discounts,
        count_cancelled=count_cancelled_discounts,
    )

    # HANDLE DEACTIVATIONS

    vouchers_to_deactivate = [
        row[0]
        for row in (
            current_session.query(Voucher.id)  # noqa: ALN085
            .filter(
                Voucher.discount_code.in_(applied_discounts),
                Voucher.used_at.is_(None),
            )
            .all()
        )
    ]

    count_to_deactivate = len(vouchers_to_deactivate)
    if count_to_deactivate > 0:
        current_logger.info("Found vouchers to deactivate", count=count_to_deactivate)

        current_session.query(Voucher).filter(  # noqa: ALN085
            Voucher.id.in_(vouchers_to_deactivate)
        ).update(
            {Voucher.used_at: datetime.utcnow(), Voucher.updated_at: datetime.utcnow()},
            synchronize_session=False,
        )
        if dry_run:
            current_session.rollback()
        else:
            current_session.commit()
            # TODO: Send slack notif?
    else:
        current_logger.info("No vouchers to deactivate")

    # HANDLE REGENERATIONS

    vouchers_to_regenerate: list[Voucher] = (
        current_session.query(Voucher)  # noqa: ALN085
        .filter(
            Voucher.discount_code.in_(cancelled_discounts),
            Voucher.used_at.isnot(None),
        )
        .all()
    )

    count_to_regenerate = len(vouchers_to_regenerate)
    if count_to_regenerate > 0:
        current_logger.info("Found vouchers to regenerate", count=count_to_regenerate)

        for voucher in vouchers_to_regenerate:
            current_logger.info(
                "Regenerating voucher", voucher_id=voucher.id, voucher_code=voucher.code
            )

            amount_in_euros, _ = VOUCHER_CODE_PROPS[VoucherCode[voucher.code]]

            title_suffix = "" if is_production_mode() else f" - {get_env_name()}"

            discount_code = generate_discount_code()

            try:
                create_shop_discount(
                    code=discount_code,
                    amount=amount_in_euros,
                    expires_at=voucher.expires_at,
                    title=f"Voucher {amount_in_euros}€ - {discount_code}"
                    + title_suffix,
                )
            except Exception as e:
                current_logger.error("Failed to create shop discount", error=e)
                continue

            voucher.discount_code = discount_code  # Apply new discount code
            voucher.used_at = None  # Reset used_at

        if dry_run:
            current_session.rollback()
        else:
            current_session.commit()
            # TODO: Send slack notif?
    else:
        current_logger.info("No vouchers to regenerate")

warmup_avatars

warmup_avatars

warmup_avatars()

Warm up avatars images in cache.

Source code in components/gamification/public/commands/warmup_avatars.py
@gamification_commands.command()
def warmup_avatars() -> None:
    """Warm up avatars images in cache."""
    from components.gamification.internal.business_logic.queries.image_composer import (
        warmup_avatar_images,
    )

    warmup_avatar_images()

components.gamification.public.controllers

achievements

AchievementsController

Bases: BaseController

CollectAchievementsPayload dataclass

CollectAchievementsPayload(day, before=None)

Query parameters for POST /achievements/collect

before class-attribute instance-attribute
before = None
day instance-attribute
day

DeclareAchievementPayload dataclass

DeclareAchievementPayload(code, day=None)

Query parameters for POST /achievements

code instance-attribute
code
day class-attribute instance-attribute
day = None

GetAchievementsPayload dataclass

GetAchievementsPayload(
    day=None, only_after=None, only_new=None, format=None
)

Query parameters for GET /achievements

day class-attribute instance-attribute
day = None
format class-attribute instance-attribute
format = None
only_after class-attribute instance-attribute
only_after = None
only_new class-attribute instance-attribute
only_new = None

WHITELISTED_CLIENT_ACHIEVEMENT_CODES module-attribute

WHITELISTED_CLIENT_ACHIEVEMENT_CODES = {
    activation_wheel,
    baby_sleep_completed,
    backpain_completed,
    debug,
    stress_program_completed,
    play_scratch,
    play_scratch_25_1,
    play_scratch_25_2,
    play_scratch_25_3,
    play_scratch_25_4,
    play_scratch_200_1,
    play_scratch_tp_card,
    play_scratch_200_prevenir,
    streak_7d,
    streak_20d,
    streak_50d,
    streak_100d,
    streak_150d,
    streak_200d,
    streak_250d,
    streak_300d,
    streak_365d,
    streak_500d,
    streak_600d,
    streak_700d,
    streak_800d,
    streak_900d,
    streak_1000d,
    shop_eyewear_vto_frames_tried,
    tp_card_added_to_wallet,
    mo_first_discussion,
    team_challenge_mission_completed,
    guess_who_try_1,
    guess_who_try_2,
    guess_who_try_3,
}

achievements_endpoint module-attribute

achievements_endpoint = Endpoint('achievements')

activities

ActivitiesController

Bases: BaseController

activities_endpoint module-attribute

activities_endpoint = Endpoint('activities')

activity_trip

ActivityTripController

Bases: BaseController

Activity trip endpoints.

activity_trip_endpoint module-attribute

activity_trip_endpoint = Endpoint('activity_trips')

badges

BadgesController

Bases: BaseController

GetBadgeSeriesGetQuerySchema

Bases: Schema

Query parameters for GET /badges/series

app_user_id class-attribute instance-attribute
app_user_id = Str(
    required=False, validate=OwnerController(NoOwner)
)

MarkBadgesAsSeenPostQuerySchema

Bases: Schema

Query parameters for POST /badges/mark_as_displayed

displayed_at class-attribute instance-attribute
displayed_at = DateTime(required=True)

badges_endpoint module-attribute

badges_endpoint = Endpoint('badges')

mark_badges_as_seen

mark_badges_as_seen(player_context, json_args)

Mark some badges as seen in the frontend. This ensures we don't show them again in the future.

Source code in components/gamification/public/controllers/badges.py
@BadgesController.action_route(
    "/mark_as_displayed",
    methods=["POST"],
    auth_strategy=GlobalAuthorizationStrategies().authenticated(),
)
@use_args(
    MarkBadgesAsSeenPostQuerySchema(),
    location="json",
    unknown=EXCLUDE,
    arg_name="json_args",
)
@inject_player_context
@obs.api_call()
def mark_badges_as_seen(
    player_context: PlayerContextData, json_args: dict[str, Any]
) -> Response:
    """
    Mark some badges as seen in the frontend. This ensures we don't show them again in the future.
    """
    from components.gamification.internal.business_logic.actions.player import (
        update_player,
    )

    try:
        update_player(
            player_id=player_context.player_id,
            badges_last_displayed_at=json_args["displayed_at"],
        )
        return make_empty_response()
    except ClientException as ex:
        return make_json_response(code=400, data={"error": ex.message})

charity

CharitiesController

Bases: BaseController

ContributionsByCodeGetQuerySchema

Bases: Schema

Query schema for GET /charity/contributions.

code class-attribute instance-attribute
code = Str(required=True, validate=OwnerController(NoOwner))

charity_endpoint module-attribute

charity_endpoint = Endpoint('charity')

cycling

CreateCyclingSessionSchema

Bases: Schema

Schema for POST /cycling/sessions endpoint.

day class-attribute instance-attribute
day = Date(
    required=True,
    metadata={"description": "Date of the cycling session"},
)
distance_meters class-attribute instance-attribute
distance_meters = Int(
    required=True,
    metadata={"description": "Distance in meters"},
)
external_id class-attribute instance-attribute
external_id = Str(
    required=True,
    metadata={
        "description": "Unique identifier from the mobile app (for idempotency)"
    },
)

CyclingController

Bases: BaseController

cycling_endpoint module-attribute

cycling_endpoint = Endpoint('cycling')

daily_challenge

CreateDailyChallengePostJsonSchema

Bases: Schema

Schema for POST /daily_challenge.

code class-attribute instance-attribute
code = Str(
    required=True,
    metadata={"description": "Daily challenge code"},
)
correct_answers class-attribute instance-attribute
correct_answers = Int(
    required=False,
    allow_none=True,
    metadata={
        "description": "Daily challenge correct answers (optional)"
    },
)
day class-attribute instance-attribute
day = Date(
    required=True,
    metadata={"description": "User current day"},
)
day_time class-attribute instance-attribute
day_time = Time(
    required=False,
    allow_none=True,
    metadata={"description": "User current day time"},
)
ended_at class-attribute instance-attribute
ended_at = DateTime(
    required=False,
    allow_none=True,
    metadata={
        "description": "Challenge end datetime in UTC"
    },
)
exercise_id class-attribute instance-attribute
exercise_id = Str(
    required=False,
    allow_none=True,
    validate=OwnerController(NoOwner),
    metadata={"description": "Exercise id"},
)
external_id class-attribute instance-attribute
external_id = Str(
    required=False,
    allow_none=True,
    validate=OwnerController(NoOwner),
    metadata={"description": "External id"},
)
extra_properties class-attribute instance-attribute
extra_properties = Dict(
    required=False,
    allow_none=True,
    metadata={
        "description": "Additional activity metadata (duration, activity_type, distance, kcal, source, met)"
    },
)
minutes class-attribute instance-attribute
minutes = Int(
    required=False,
    allow_none=True,
    metadata={
        "description": "Daily challenge minutes (optional)"
    },
)
started_at class-attribute instance-attribute
started_at = DateTime(
    required=False,
    allow_none=True,
    metadata={
        "description": "Challenge start datetime in UTC"
    },
)
steps class-attribute instance-attribute
steps = Int(
    required=False,
    allow_none=True,
    metadata={
        "description": "Daily challenge steps (optional)"
    },
)
tags class-attribute instance-attribute
tags = List(
    Str(),
    required=False,
    allow_none=True,
    metadata={
        "description": "Daily challenge special tags"
    },
)

CreateStreakFreezePostJsonSchema

Bases: Schema

Schema for POST /daily_challenge/freeze.

day class-attribute instance-attribute
day = Date(
    required=True,
    metadata={"description": "User current day"},
)

DailyChallengeController

Bases: BaseController

GetDailyChallengeStreakGetQuerySchema

Bases: Schema

Schema for GET /daily_challenge/streak.

app_user_id class-attribute instance-attribute
app_user_id = Str(
    required=False,
    allow_none=True,
    validate=OwnerController(NoOwner),
)
day class-attribute instance-attribute
day = Date(
    required=True,
    metadata={"description": "User current day"},
)

daily_challenge_endpoint module-attribute

daily_challenge_endpoint = Endpoint('daily_challenge')

duels

CreateDuelPostJsonSchema

Bases: Schema

JSON body for POST /duels.

berries_amount class-attribute instance-attribute
berries_amount = Int(
    required=True,
    metadata={
        "description": "Amount of berries for the duel"
    },
)
is_today_revenge class-attribute instance-attribute
is_today_revenge = Bool(
    required=False,
    load_default=False,
    metadata={
        "description": "Whether this is a same-day revenge duel against a special guest"
    },
)
receiver_id class-attribute instance-attribute
receiver_id = Str(
    required=True,
    metadata={
        "description": "Player ID or User ID of the receiver"
    },
    validate=OwnerController(NoOwner),
)

DuelsController

Bases: BaseController

Controller for handling duels.

This controller provides endpoints to create a duel, get a list of duels for a player, get a single duel by ID, and mark a duel as accepted.

GetReceivedDuelsGetQuerySchema

Bases: Schema

Query parameters for GET /duels/received

active class-attribute instance-attribute
active = Bool(
    required=False,
    load_default=False,
    metadata={"description": "Filter active duels"},
)

walk_duels_endpoint module-attribute

walk_duels_endpoint = Endpoint('duels')

image_composer

ImageComposerController

Bases: BaseController

image_composer_endpoint module-attribute

image_composer_endpoint = Endpoint('image_composer')

level

LevelController

Bases: BaseController

level_endpoint module-attribute

level_endpoint = Endpoint('level')

marmot

MarmotController

Bases: BaseController

marmot_endpoint module-attribute

marmot_endpoint = Endpoint('marmot')

play_history

GetRecentCumulativeProgressPayload dataclass

GetRecentCumulativeProgressPayload(day)

Payload for GET /play_history/recent-cumulative-progress.

day instance-attribute
day

PlayHistoryController

Bases: BaseController

play_history_endpoint module-attribute

play_history_endpoint = Endpoint('play_history')

player

GetPlayerBeneficiariesPayload dataclass

GetPlayerBeneficiariesPayload(adult_only=False)

Payload for GET /player/beneficiaries.

adult_only class-attribute instance-attribute
adult_only = False

GetPlayerPayload dataclass

GetPlayerPayload(app_user_id=None)

Payload for GET /player.

app_user_id class-attribute instance-attribute
app_user_id = None

PlayerController

Bases: BaseController

SetPlayerPayload dataclass

SetPlayerPayload(
    character_id=None,
    avatar=None,
    onboarded=None,
    walk_activation_dismissed=None,
    walk_promotion_dismissed=None,
    has_enabled_duels=None,
    has_enabled_leaderboard=None,
    has_enabled_reactions=None,
    has_enabled_marketing_notifications=None,
    has_enabled_reminders=None,
    has_enabled_leaderboard_notifications=None,
    has_enabled_berries_notifications=None,
    has_enabled_social_notifications=None,
    has_disabled_external_activity_notifications=None,
    widget_tuto_displayed=None,
    has_enabled_beneficiary_league=None,
)

Payload for PATCH /player.

avatar class-attribute instance-attribute
avatar = None
character_id class-attribute instance-attribute
character_id = None
has_disabled_external_activity_notifications class-attribute instance-attribute
has_disabled_external_activity_notifications = None
has_enabled_beneficiary_league class-attribute instance-attribute
has_enabled_beneficiary_league = None
has_enabled_berries_notifications class-attribute instance-attribute
has_enabled_berries_notifications = None
has_enabled_duels class-attribute instance-attribute
has_enabled_duels = None
has_enabled_leaderboard class-attribute instance-attribute
has_enabled_leaderboard = None
has_enabled_leaderboard_notifications class-attribute instance-attribute
has_enabled_leaderboard_notifications = None
has_enabled_marketing_notifications class-attribute instance-attribute
has_enabled_marketing_notifications = None
has_enabled_reactions class-attribute instance-attribute
has_enabled_reactions = None
has_enabled_reminders class-attribute instance-attribute
has_enabled_reminders = None
has_enabled_social_notifications class-attribute instance-attribute
has_enabled_social_notifications = None
onboarded class-attribute instance-attribute
onboarded = None
walk_activation_dismissed class-attribute instance-attribute
walk_activation_dismissed = None
walk_promotion_dismissed class-attribute instance-attribute
walk_promotion_dismissed = None
widget_tuto_displayed class-attribute instance-attribute
widget_tuto_displayed = None

player_endpoint module-attribute

player_endpoint = Endpoint('player')

players

MAX_PLAYERS_LIST_LIMIT module-attribute

MAX_PLAYERS_LIST_LIMIT = 1000

OtherPlayerStreakGetQuerySchema

Bases: Schema

Query parameters for GET /players//streak

day class-attribute instance-attribute
day = Date(
    required=True,
    metadata={"description": "User current day"},
)

PlayersController

Bases: BaseController

PlayersGetQuerySchema

Bases: Schema

Query parameters for GET /players

search_term class-attribute instance-attribute
search_term = Str(
    required=False,
    allow_none=True,
    load_default=None,
    metadata={"description": "Search term"},
)
with_streak class-attribute instance-attribute
with_streak = Bool(
    required=False,
    load_default=False,
    metadata={"description": "Include player streak"},
)

players_endpoint module-attribute

players_endpoint = Endpoint('players')

points

GetPointsPayload dataclass

GetPointsPayload(
    app_user_id=None,
    only_collected=None,
    since=None,
    today=None,
)

Payload for GET /points.

app_user_id class-attribute instance-attribute
app_user_id = None
only_collected class-attribute instance-attribute
only_collected = None
since class-attribute instance-attribute
since = None
today class-attribute instance-attribute
today = None

PointsController

Bases: BaseController

points_endpoint module-attribute

points_endpoint = Endpoint('points')

purchases

CreatePurchasePayload dataclass

CreatePurchasePayload(code, berries=None)

Payload for POST /purchases.

berries class-attribute instance-attribute
berries = None
code instance-attribute
code

PurchasesController

Bases: BaseController

get_purchases

get_purchases(player_context)
Source code in components/gamification/public/controllers/purchases.py
@PurchasesController.action_route(
    "",
    methods=["GET"],
    auth_strategy=GlobalAuthorizationStrategies().authenticated(),
)
@inject_player_context
@obs.api_call()
def get_purchases(player_context: PlayerContextData) -> Response:  # noqa: D103
    from components.gamification.internal.business_logic.queries.purchases import (
        list_purchases,
    )

    return make_json_response(list_purchases(player_id=player_context.player_id))

purchases_endpoint module-attribute

purchases_endpoint = Endpoint('purchases')

reactions

CreateReactionPostJsonSchema

Bases: Schema

Args for POST /reactions.

emoji class-attribute instance-attribute
emoji = Enum(
    GamificationReactionEmoji,
    by_value=True,
    dump_default=None,
    metadata={"description": "Emoji of the reaction"},
)
limit_per_day class-attribute instance-attribute
limit_per_day = Int(
    dump_default=None,
    metadata={
        "description": "Specify a set amount of max reactions per day"
    },
)
note class-attribute instance-attribute
note = Str(
    dump_default=None,
    metadata={
        "description": "Note attached to the reaction"
    },
)
origin class-attribute instance-attribute
origin = Str(
    dump_default=None,
    metadata={
        "description": "Origin of the request (e.g. duel or league)"
    },
)
origin_id class-attribute instance-attribute
origin_id = Str(
    dump_default=None,
    metadata={
        "validate": OwnerController(NoOwner),
        "description": "ID associated with the origin of the request, if any",
    },
)
receiver_id class-attribute instance-attribute
receiver_id = Str(
    dump_default=None,
    metadata={
        "validate": OwnerController(NoOwner),
        "description": "Player ID or App User ID of the receiver",
    },
)
team_id class-attribute instance-attribute
team_id = UUID(
    dump_default=None,
    metadata={
        "validate": OwnerController(NoOwner),
        "description": "Team id",
    },
)

DeprecatedReportReactionPostJsonSchema

Bases: Schema

Args for POST /reactions/report (deprecated).

message class-attribute instance-attribute
message = Str(
    required=True,
    metadata={"description": "Report message"},
)
reaction_id class-attribute instance-attribute
reaction_id = UUID(
    required=True,
    metadata={
        "validate": OwnerController(NoOwner),
        "description": "Reported reaction ID",
    },
)

ReactionsController

Bases: BaseController

Controller for handling reactions.

This controller provides an endpoint to submit reactions from users. It accepts a POST request with the receiver's ID and the emoji representing the reaction. The reaction is then stored using the internal business logic.

ReportReactionPostJsonSchema

Bases: Schema

Args for POST /reactions//report.

message class-attribute instance-attribute
message = Str(
    required=True,
    metadata={"description": "Report message"},
)

reactions_endpoint module-attribute

reactions_endpoint = Endpoint('reactions')

records

GetRecordsGetQuerySchema

Bases: Schema

Schema for GET /records endpoint.

app_user_id class-attribute instance-attribute
app_user_id = Str(
    required=False, validate=OwnerController(NoOwner)
)

RecordsController

Bases: BaseController

Handles requests related to personal records (e.g. most steps in a day, longest streak..)

records_endpoint module-attribute

records_endpoint = Endpoint('records')

scores

GetScoresPayload dataclass

GetScoresPayload(week=None)
week class-attribute instance-attribute
week = None

ScoresController

Bases: BaseController

Handles requests related to scores of players related to current players (e.g. colleagues, beneficiaries, etc.)

scores_endpoint module-attribute

scores_endpoint = Endpoint('scores')

stats

GetEntityStatsPayload dataclass

GetEntityStatsPayload(entity_id, entity_type)

Query parameters for the get entity stats endpoint.

entity_id instance-attribute
entity_id
entity_type instance-attribute
entity_type

GetStatsPayload dataclass

GetStatsPayload(day, app_user_id=None)

Query parameters for the get stats endpoint.

app_user_id class-attribute instance-attribute
app_user_id = None
day instance-attribute
day

StatsController

Bases: BaseController

Handles requests related to player stats (e.g. total steps today, this week, this month, this year)

walk_stats_endpoint module-attribute

walk_stats_endpoint = Endpoint('stats')

streaks

CreateStreakFreezePayload dataclass

CreateStreakFreezePayload(day)
day instance-attribute
day

GetStreakPayload dataclass

GetStreakPayload(day, app_user_id=None)
app_user_id class-attribute instance-attribute
app_user_id = None
day instance-attribute
day

StreaksController

Bases: BaseController

streaks_endpoint module-attribute

streaks_endpoint = Endpoint('streaks')

team_challenge

AskBoostWithMessagePayload dataclass

AskBoostWithMessagePayload(message=None)
message class-attribute instance-attribute
message = None

CreateBoostPayload dataclass

CreateBoostPayload(other_player_id)
other_player_id instance-attribute
other_player_id

CreateTeamPayload dataclass

CreateTeamPayload(color, name, visual)
color instance-attribute
color
name instance-attribute
name
visual instance-attribute
visual

GetFeedPayload dataclass

GetFeedPayload(cursor=None)
cursor class-attribute instance-attribute
cursor = None

GetLeaderboardPayload dataclass

GetLeaderboardPayload(month=None)
month class-attribute instance-attribute
month = None

GetTeamPayload dataclass

GetTeamPayload(month=None)
month class-attribute instance-attribute
month = None

InviteToTeamChallengePayload dataclass

InviteToTeamChallengePayload(other_player_ids)
other_player_ids instance-attribute
other_player_ids

JoinTeamLegacyPayload dataclass

JoinTeamLegacyPayload(team_id)
team_id instance-attribute
team_id

TeamChallengeController

Bases: BaseController

UpdateTeamPayload dataclass

UpdateTeamPayload(color=None, name=None, visual=None)
color class-attribute instance-attribute
color = None
name class-attribute instance-attribute
name = None
visual class-attribute instance-attribute
visual = None

team_challenge_endpoint module-attribute

team_challenge_endpoint = Endpoint('team_challenge')

users

InviteUsersPostJsonSchema

Bases: Schema

JSON schema for POST /users/invite.

app_user_ids class-attribute instance-attribute
app_user_ids = List(
    Str(),
    required=True,
    metadata={"description": "IDs of the users to invite"},
)

SearchUsersGetQuerySchema

Bases: Schema

Query schema for GET /users/search.

search class-attribute instance-attribute
search = Str(
    required=True, metadata={"description": "search string"}
)

UsersController

Bases: BaseController

Controller for user-related needs in the gamification module. Currently used to search users who aren't enrolled in Play

users_endpoint module-attribute

users_endpoint = Endpoint('users')

utils

inject_player_context

inject_player_context(f)
Source code in components/gamification/public/controllers/utils.py
def inject_player_context(f: Callable) -> Callable:  # type: ignore[type-arg]  # noqa: D103
    method_signature = signature(f)

    @wraps(f)
    def decorated_function(*args, **kwargs):  # type: ignore[no-untyped-def]
        from components.gamification.internal.business_logic.actions.player import (
            get_or_create_player,
        )
        from components.gamification.internal.business_logic.queries.player import (
            get_player_context,
            get_player_context_from_user_id_cached,
        )

        user = g.current_user if hasattr(g, "current_user") else None
        if not user or not user.feature_user:
            current_logger.exception("Gamification endpoint expect a user")
            abort(400, "No user")

        feature_user = user.feature_user
        app_id = str(
            feature_user.app_id
        )  # Force casting to str, else app_id might be consider as an AlanApp Enum (which cause issue when used in cached_for)
        app_user_id = feature_user.app_user_id

        player_context = get_player_context_from_user_id_cached(app_user_id=app_user_id)
        if player_context:
            if "account_id" in method_signature.parameters:
                kwargs["account_id"] = player_context.account_id
            if "company_id" in method_signature.parameters:
                kwargs["company_id"] = (
                    player_context.company_id
                )  # relative to entity configuration not to user enrollment
            if "player_context" in method_signature.parameters:
                kwargs["player_context"] = player_context
            if "player_id" in method_signature.parameters:
                kwargs["player_id"] = player_context.player_id
        else:
            player = get_or_create_player(app_id=app_id, app_user_id=app_user_id)
            player_context = get_player_context(player)
            if "account_id" in method_signature.parameters:
                kwargs["account_id"] = player.account_id
            if "company_id" in method_signature.parameters:
                kwargs["company_id"] = (
                    player_context.company_id
                )  # relative to entity configuration not to user enrollment
            if "player_context" in method_signature.parameters:
                kwargs["player_context"] = player_context
            if "player_id" in method_signature.parameters:
                kwargs["player_id"] = player.id

        if "app_id" in method_signature.parameters:
            kwargs["app_id"] = app_id
        if "app_user_id" in method_signature.parameters:
            kwargs["app_user_id"] = app_user_id

        return f(*args, **kwargs)

    return decorated_function

vouchers

CreateVoucherPayload dataclass

CreateVoucherPayload(code)

Payload for POST /vouchers.

code class-attribute instance-attribute
code = field(metadata={'by_value': True})

VouchersController

Bases: BaseController

vouchers_endpoint module-attribute

vouchers_endpoint = Endpoint('vouchers')

walk_challenge

CreateWalkFeedbackPayload dataclass

CreateWalkFeedbackPayload(comment)

Payload for the create walk feedback endpoint.

comment instance-attribute
comment

JoinWalkPayload dataclass

JoinWalkPayload(start_date, referral_code=None)

Payload for the join walk endpoint.

referral_code class-attribute instance-attribute
referral_code = None
start_date instance-attribute
start_date

SetProgressPayload dataclass

SetProgressPayload(day, day_steps, week_steps)

Payload for the set walk progress with token endpoint.

day instance-attribute
day
day_steps instance-attribute
day_steps
week_steps instance-attribute
week_steps

WalkChallengeController

Bases: BaseController

walk_challenge_endpoint module-attribute

walk_challenge_endpoint = Endpoint('walking_challenge')

walk_history

WalkHistoryController

Bases: BaseController

walk_history_endpoint module-attribute

walk_history_endpoint = Endpoint('walking_history')

walk_leagues

BeneficiarySkinPayload dataclass

BeneficiarySkinPayload(name, emoji)
emoji instance-attribute
emoji
name instance-attribute
name

CreateLeaguePayload dataclass

CreateLeaguePayload(name, emoji)
emoji instance-attribute
emoji
name instance-attribute
name

GetAllLeaguesPayload dataclass

GetAllLeaguesPayload(
    past_week=None,
    include_entries=True,
    include_members=False,
    include_mines=True,
    include_others=True,
    include_statistics=True,
)
include_entries class-attribute instance-attribute
include_entries = True
include_members class-attribute instance-attribute
include_members = False
include_mines class-attribute instance-attribute
include_mines = True
include_others class-attribute instance-attribute
include_others = True
include_statistics class-attribute instance-attribute
include_statistics = True
past_week class-attribute instance-attribute
past_week = None

GetAllLeaguesPositionsPayload dataclass

GetAllLeaguesPositionsPayload(past_weeks=None)
past_weeks class-attribute instance-attribute
past_weeks = None

InvitePlayersPayload dataclass

InvitePlayersPayload(player_app_user_ids)
player_app_user_ids instance-attribute
player_app_user_ids

WalkLeaguesController

Bases: BaseController

walk_leagues_endpoint module-attribute

walk_leagues_endpoint = Endpoint('walk_leagues')

components.gamification.public.dependency

BeneficiaryData dataclass

BeneficiaryData(user_id, first_name)

Bases: DataClassJsonMixin

Data for a single beneficiary.

first_name instance-attribute

first_name

user_id instance-attribute

user_id

COMPONENT_NAME module-attribute

COMPONENT_NAME = 'gamification'

GamificationAccountAdminEnrollmentData dataclass

GamificationAccountAdminEnrollmentData(
    account_ids, account_names, company_ids, company_names
)

...

account_ids instance-attribute

account_ids

account_names instance-attribute

account_names

company_ids instance-attribute

company_ids

company_names instance-attribute

company_names

GamificationDependency

Bases: ABC

get_account_admin_enrollment abstractmethod

get_account_admin_enrollment(app_user_id)

...

Source code in components/gamification/public/dependency.py
@abstractmethod
def get_account_admin_enrollment(
    self, app_user_id: str
) -> GamificationAccountAdminEnrollmentData | None:
    """..."""

get_account_from_company abstractmethod

get_account_from_company(company_id)

...

Source code in components/gamification/public/dependency.py
@abstractmethod
def get_account_from_company(self, company_id: str) -> str | None:
    """..."""

get_account_name abstractmethod

get_account_name(account_id)

...

Source code in components/gamification/public/dependency.py
@abstractmethod
def get_account_name(self, account_id: str) -> str | None:
    """..."""

get_first_and_last_names_from_user_ids abstractmethod

get_first_and_last_names_from_user_ids(app_user_ids)

...

Source code in components/gamification/public/dependency.py
@abstractmethod
def get_first_and_last_names_from_user_ids(
    self,
    app_user_ids: list[str],
) -> list[tuple[str, str | None, str | None]]:
    """..."""

get_player_ids_to_cleanup abstractmethod

get_player_ids_to_cleanup()

...

Source code in components/gamification/public/dependency.py
@abstractmethod
def get_player_ids_to_cleanup(self) -> Sequence[UUID]:
    """..."""

get_reference_timezone abstractmethod

get_reference_timezone()

...

Source code in components/gamification/public/dependency.py
@abstractmethod
def get_reference_timezone(self) -> ZoneInfo:
    """..."""

get_user_beneficiaries_cached abstractmethod

get_user_beneficiaries_cached(
    app_user_id, adult_only=False
)

Get beneficiaries of a user's policy with policy information

Source code in components/gamification/public/dependency.py
@abstractmethod
def get_user_beneficiaries_cached(
    self, app_user_id: str, adult_only: bool = False
) -> UserBeneficiariesResult | None:
    """Get beneficiaries of a user's policy with policy information"""

get_user_enrollment abstractmethod

get_user_enrollment(app_user_id)

Return the country code for the current country

Source code in components/gamification/public/dependency.py
@abstractmethod
def get_user_enrollment(
    self, app_user_id: str
) -> GamificationUserEnrollmentData | None:
    """Return the country code for the current country"""

is_feature_enabled_for_user_id abstractmethod

is_feature_enabled_for_user_id(feature_name, app_user_id)

Return True if the feature is enabled for the user, False otherwise

Source code in components/gamification/public/dependency.py
@abstractmethod
def is_feature_enabled_for_user_id(
    self, feature_name: str, app_user_id: str
) -> bool:
    """Return True if the feature is enabled for the user, False otherwise"""

is_user_alaner abstractmethod

is_user_alaner(app_user_id)

...

Source code in components/gamification/public/dependency.py
@abstractmethod
def is_user_alaner(self, app_user_id: str) -> bool:
    """..."""

search_entity_users abstractmethod

search_entity_users(search, account_id, company_id)

...

Source code in components/gamification/public/dependency.py
@abstractmethod
def search_entity_users(
    self, search: str, account_id: str, company_id: str | None
) -> list[tuple[str, str | None, str | None]]:
    """..."""

GamificationUserEnrollmentData dataclass

GamificationUserEnrollmentData(
    account_id,
    account_name,
    company_id,
    company_name,
    user_state,
)

...

account_id instance-attribute

account_id

account_name instance-attribute

account_name

company_id instance-attribute

company_id

company_name instance-attribute

company_name

user_state instance-attribute

user_state

UserBeneficiariesResult dataclass

UserBeneficiariesResult(beneficiaries, policy_id)

Bases: DataClassJsonMixin

Result containing user beneficiaries and policy information.

__post_init__

__post_init__()

Backward compatibility property for user IDs only.

Source code in components/gamification/public/dependency.py
def __post_init__(self):  # type: ignore[no-untyped-def]
    """Backward compatibility property for user IDs only."""
    self.user_ids = [b.user_id for b in self.beneficiaries]

beneficiaries instance-attribute

beneficiaries

policy_id instance-attribute

policy_id

user_ids class-attribute instance-attribute

user_ids = field(init=False)

get_app_dependency

get_app_dependency()

Get the GamificationDependency for the current Flask app

Source code in components/gamification/public/dependency.py
def get_app_dependency() -> GamificationDependency:
    """Get the GamificationDependency for the current Flask app"""
    from flask import current_app

    return cast("CustomFlask", current_app).get_component_dependency(COMPONENT_NAME)  # type: ignore[no-any-return]

set_app_dependency

set_app_dependency(dependency)

Set the GamificationDependency for the current Flask app

Source code in components/gamification/public/dependency.py
def set_app_dependency(dependency: GamificationDependency) -> None:
    """Set the GamificationDependency for the current Flask app"""
    from flask import current_app

    cast("CustomFlask", current_app).add_component_dependency(
        COMPONENT_NAME, dependency
    )

components.gamification.public.enums

achievement_declarations

AchievementDeclarationLimit

Bases: AlanBaseEnum

daily class-attribute instance-attribute
daily = 'daily'
monthly class-attribute instance-attribute
monthly = 'monthly'
no_limit class-attribute instance-attribute
no_limit = 'no_limit'
once class-attribute instance-attribute
once = 'once'
weekly class-attribute instance-attribute
weekly = 'weekly'

achievement_definition

AchievementCode

Bases: AlanBaseEnum

Achievement codes for the gamification system. These codes are used to identify specific achievements that players can earn.

activation_wheel class-attribute instance-attribute
activation_wheel = 'activation_wheel'
baby_sleep_completed class-attribute instance-attribute
baby_sleep_completed = 'baby_sleep_completed'
back_pain_program_exercise class-attribute instance-attribute
back_pain_program_exercise = 'back_pain_program_exercise'
backpain_completed class-attribute instance-attribute
backpain_completed = 'backpain_completed'
berries_gift class-attribute instance-attribute
berries_gift = 'berries_gift'
berries_leaderboard_1 class-attribute instance-attribute
berries_leaderboard_1 = 'berries_leaderboard_1'
berries_leaderboard_10p class-attribute instance-attribute
berries_leaderboard_10p = 'berries_leaderboard_10p'
berries_leaderboard_2 class-attribute instance-attribute
berries_leaderboard_2 = 'berries_leaderboard_2'
berries_leaderboard_3 class-attribute instance-attribute
berries_leaderboard_3 = 'berries_leaderboard_3'
berries_leaderboard_5p class-attribute instance-attribute
berries_leaderboard_5p = 'berries_leaderboard_5p'
breathing class-attribute instance-attribute
breathing = 'breathing'
breathing_qvct_zen_d1 class-attribute instance-attribute
breathing_qvct_zen_d1 = 'breathing_qvct_zen_d1'
care_incompatible_device class-attribute instance-attribute
care_incompatible_device = 'care_incompatible_device'
care_reward class-attribute instance-attribute
care_reward = 'care_reward'
cycling class-attribute instance-attribute
cycling = 'cycling'
debug class-attribute instance-attribute
debug = 'debug'
duel_100 class-attribute instance-attribute
duel_100 = 'duel_100'
duel_1000 class-attribute instance-attribute
duel_1000 = 'duel_1000'
duel_500 class-attribute instance-attribute
duel_500 = 'duel_500'
duel_participation class-attribute instance-attribute
duel_participation = 'duel_participation'
external_activity class-attribute instance-attribute
external_activity = 'external_activity'
guess_who_try_1 class-attribute instance-attribute
guess_who_try_1 = 'guess_who_try_1'
guess_who_try_2 class-attribute instance-attribute
guess_who_try_2 = 'guess_who_try_2'
guess_who_try_3 class-attribute instance-attribute
guess_who_try_3 = 'guess_who_try_3'
health_program_exercise class-attribute instance-attribute
health_program_exercise = 'health_program_exercise'
health_program_qvct_move_d2 class-attribute instance-attribute
health_program_qvct_move_d2 = 'health_program_qvct_move_d2'
health_program_qvct_move_d4 class-attribute instance-attribute
health_program_qvct_move_d4 = 'health_program_qvct_move_d4'
health_program_qvct_zen_d4 class-attribute instance-attribute
health_program_qvct_zen_d4 = 'health_program_qvct_zen_d4'
joined_longevity_waitlist class-attribute instance-attribute
joined_longevity_waitlist = 'joined_longevity_waitlist'
journaling class-attribute instance-attribute
journaling = 'journaling'
journaling_qvct_zen_d3 class-attribute instance-attribute
journaling_qvct_zen_d3 = 'journaling_qvct_zen_d3'
km_charity_donation class-attribute instance-attribute
km_charity_donation = 'km_charity_donation'
mbappe_raffle class-attribute instance-attribute
mbappe_raffle = 'mbappe_raffle'
meditation class-attribute instance-attribute
meditation = 'meditation'
mo_first_discussion class-attribute instance-attribute
mo_first_discussion = 'mo_first_discussion'
perfect_week class-attribute instance-attribute
perfect_week = 'perfect_week'
pink_october class-attribute instance-attribute
pink_october = 'pink_october'
play_scratch class-attribute instance-attribute
play_scratch = 'play_scratch'
play_scratch_200_1 class-attribute instance-attribute
play_scratch_200_1 = 'play_scratch_200_1'
play_scratch_200_prevenir class-attribute instance-attribute
play_scratch_200_prevenir = 'play_scratch_200_prevenir'
play_scratch_25_1 class-attribute instance-attribute
play_scratch_25_1 = 'play_scratch_25_1'
play_scratch_25_2 class-attribute instance-attribute
play_scratch_25_2 = 'play_scratch_25_2'
play_scratch_25_3 class-attribute instance-attribute
play_scratch_25_3 = 'play_scratch_25_3'
play_scratch_25_4 class-attribute instance-attribute
play_scratch_25_4 = 'play_scratch_25_4'
play_scratch_tp_card class-attribute instance-attribute
play_scratch_tp_card = 'play_scratch_tp_card'
quiz class-attribute instance-attribute
quiz = 'quiz'
qvct_full_move_week class-attribute instance-attribute
qvct_full_move_week = 'qvct_full_move_week'
qvct_full_zen_week class-attribute instance-attribute
qvct_full_zen_week = 'qvct_full_zen_week'
referral class-attribute instance-attribute
referral = 'referral'
shop_contacts_purchase class-attribute instance-attribute
shop_contacts_purchase = 'shop_contacts_purchase'
shop_eyewear_purchase class-attribute instance-attribute
shop_eyewear_purchase = 'shop_eyewear_purchase'
shop_eyewear_vto_frames_tried class-attribute instance-attribute
shop_eyewear_vto_frames_tried = (
    "shop_eyewear_vto_frames_tried"
)
shop_wellbeing_purchase class-attribute instance-attribute
shop_wellbeing_purchase = 'shop_wellbeing_purchase'
streak_1000d class-attribute instance-attribute
streak_1000d = 'streak_1000d'
streak_100d class-attribute instance-attribute
streak_100d = 'streak_100d'
streak_150d class-attribute instance-attribute
streak_150d = 'streak_150d'
streak_200d class-attribute instance-attribute
streak_200d = 'streak_200d'
streak_20d class-attribute instance-attribute
streak_20d = 'streak_20d'
streak_250d class-attribute instance-attribute
streak_250d = 'streak_250d'
streak_300d class-attribute instance-attribute
streak_300d = 'streak_300d'
streak_365d class-attribute instance-attribute
streak_365d = 'streak_365d'
streak_500d class-attribute instance-attribute
streak_500d = 'streak_500d'
streak_50d class-attribute instance-attribute
streak_50d = 'streak_50d'
streak_600d class-attribute instance-attribute
streak_600d = 'streak_600d'
streak_700d class-attribute instance-attribute
streak_700d = 'streak_700d'
streak_7d class-attribute instance-attribute
streak_7d = 'streak_7d'
streak_800d class-attribute instance-attribute
streak_800d = 'streak_800d'
streak_900d class-attribute instance-attribute
streak_900d = 'streak_900d'
stress_program_completed class-attribute instance-attribute
stress_program_completed = 'stress_program_completed'
stress_program_exercise class-attribute instance-attribute
stress_program_exercise = 'stress_program_exercise'
team_challenge_1st class-attribute instance-attribute
team_challenge_1st = 'team_challenge_1st'
team_challenge_2nd class-attribute instance-attribute
team_challenge_2nd = 'team_challenge_2nd'
team_challenge_3rd class-attribute instance-attribute
team_challenge_3rd = 'team_challenge_3rd'
team_challenge_mission_completed class-attribute instance-attribute
team_challenge_mission_completed = (
    "team_challenge_mission_completed"
)
tp_card_added_to_wallet class-attribute instance-attribute
tp_card_added_to_wallet = 'tp_card_added_to_wallet'
walk_30min_milestone class-attribute instance-attribute
walk_30min_milestone = 'walk_30min_milestone'
walk_5min_milestone class-attribute instance-attribute
walk_5min_milestone = 'walk_5min_milestone'
walk_brisk_10min_milestone1 class-attribute instance-attribute
walk_brisk_10min_milestone1 = 'walk_brisk_10min_milestone1'
walk_brisk_10min_milestone2 class-attribute instance-attribute
walk_brisk_10min_milestone2 = 'walk_brisk_10min_milestone2'
walk_brisk_10min_milestone3 class-attribute instance-attribute
walk_brisk_10min_milestone3 = 'walk_brisk_10min_milestone3'
walk_brisk_20min_milestone1 class-attribute instance-attribute
walk_brisk_20min_milestone1 = 'walk_brisk_milestone1'
walk_brisk_20min_milestone2 class-attribute instance-attribute
walk_brisk_20min_milestone2 = 'walk_brisk_milestone2'
walk_brisk_20min_milestone3 class-attribute instance-attribute
walk_brisk_20min_milestone3 = 'walk_brisk_milestone3'
walk_day_10000 class-attribute instance-attribute
walk_day_10000 = 'walk_day_10000'
walk_day_20000 class-attribute instance-attribute
walk_day_20000 = 'walk_day_20000'
walk_day_2500 class-attribute instance-attribute
walk_day_2500 = 'walk_day_2500'
walk_day_30000 class-attribute instance-attribute
walk_day_30000 = 'walk_day_30000'
walk_day_5000 class-attribute instance-attribute
walk_day_5000 = 'walk_day_5000'
walk_day_7500 class-attribute instance-attribute
walk_day_7500 = 'walk_day_7500'
walk_qvct_move_d1 class-attribute instance-attribute
walk_qvct_move_d1 = 'walk_qvct_move_d1'
walk_qvct_move_d3 class-attribute instance-attribute
walk_qvct_move_d3 = 'walk_qvct_move_d3'
walk_qvct_move_d5 class-attribute instance-attribute
walk_qvct_move_d5 = 'walk_qvct_move_d5'
walk_qvct_zen_d2 class-attribute instance-attribute
walk_qvct_zen_d2 = 'walk_qvct_zen_d2'
walk_qvct_zen_d5 class-attribute instance-attribute
walk_qvct_zen_d5 = 'walk_qvct_zen_d5'
walking_challenge_referral_use class-attribute instance-attribute
walking_challenge_referral_use = (
    "walking_challenge_referral_use"
)
walking_challenge_referral_used class-attribute instance-attribute
walking_challenge_referral_used = (
    "walking_challenge_referral_used"
)
walking_challenge_started class-attribute instance-attribute
walking_challenge_started = 'walking_challenge_started'
walking_challenge_top_10_percent class-attribute instance-attribute
walking_challenge_top_10_percent = (
    "walking_challenge_top_10_percent"
)
walking_challenge_top_5_percent class-attribute instance-attribute
walking_challenge_top_5_percent = (
    "walking_challenge_top_5_percent"
)
walking_challenge_week_1st class-attribute instance-attribute
walking_challenge_week_1st = 'walking_challenge_week_1st'
walking_challenge_week_2nd class-attribute instance-attribute
walking_challenge_week_2nd = 'walking_challenge_week_2nd'
walking_challenge_week_3rd class-attribute instance-attribute
walking_challenge_week_3rd = 'walking_challenge_week_3rd'

activity_trip

ActivityTripReviewStatus

Bases: AlanBaseEnum

Operator review state for disputed trips.

none: never flagged for review. pending: under operator review. completed: reviewed; outcome reflected in the status column. cannot_determine: reviewed but operator could not conclude.

cannot_determine class-attribute instance-attribute
cannot_determine = 'cannot_determine'
completed class-attribute instance-attribute
completed = 'completed'
none class-attribute instance-attribute
none = 'none'
pending class-attribute instance-attribute
pending = 'pending'

ActivityTripStatus

Bases: AlanBaseEnum

Lifecycle status of an activity trip.

accepted: trip detected and kept. rejected: trip detected but discarded by the algorithm. not_detected: user-reported missing ride.

accepted class-attribute instance-attribute
accepted = 'accepted'
not_detected class-attribute instance-attribute
not_detected = 'not_detected'
rejected class-attribute instance-attribute
rejected = 'rejected'

leagues

PlayerStatus

Bases: AlanBaseEnum

Enum representing the status of a player in a league.

INVITED class-attribute instance-attribute
INVITED = 'invited'
MEMBER class-attribute instance-attribute
MEMBER = 'member'

players

PlayerUserState

Bases: AlanBaseEnum

admin class-attribute instance-attribute
admin = 'admin'
beneficiary class-attribute instance-attribute
beneficiary = 'beneficiary'
employee class-attribute instance-attribute
employee = 'employee'
exempted class-attribute instance-attribute
exempted = 'exempted'
individual class-attribute instance-attribute
individual = 'individual'

records

PlayerRecordType

Bases: AlanBaseEnum

Player best records types.

daily_berries class-attribute instance-attribute
daily_berries = 'daily_berries'
daily_steps class-attribute instance-attribute
daily_steps = 'daily_steps'
streak class-attribute instance-attribute
streak = 'streak'
weekly_berries_ranking class-attribute instance-attribute
weekly_berries_ranking = 'weekly_berries_ranking'
weekly_steps_ranking class-attribute instance-attribute
weekly_steps_ranking = 'weekly_steps_ranking'

voucher

VoucherCode

Bases: AlanBaseEnum

Voucher codes for play and shop vouchers.

voucher10 class-attribute instance-attribute
voucher10 = 'voucher10'
voucher150 class-attribute instance-attribute
voucher150 = 'voucher150'
voucher25 class-attribute instance-attribute
voucher25 = 'voucher25'
voucher442 class-attribute instance-attribute
voucher442 = 'voucher442'
voucher5 class-attribute instance-attribute
voucher5 = 'voucher5'
voucher8 class-attribute instance-attribute
voucher8 = 'voucher8'
voucher956 class-attribute instance-attribute
voucher956 = 'voucher956'

VoucherDataAmount module-attribute

VoucherDataAmount = Union[int, float]

components.gamification.public.helpers

data_loader

We need static data to exist in the local database for the component to work properly.

load_gamification_store_base_data

load_gamification_store_base_data(commit=True)
Source code in components/gamification/public/helpers/data_loader.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
def load_gamification_store_base_data(commit: bool = True) -> None:  # noqa: D103
    from components.gamification.internal.models.store_item import StoreItem

    STORE_ITEMS = (
        (
            "5d25cb86-c4fa-46ee-95f2-86651f61aca2",
            "debug",
            "misc",
            1000,
        ),
        (
            "e41f3b6d-2105-4318-b9a6-9c8c2fcbfc9e",
            "gustave_roussy2",
            "charity",
            100,
        ),
        (
            "7c5489b5-c5a9-471d-9a29-03c015bc2f34",
            "avatar_booker",
            "avatar",
            1000,
        ),
        (
            "77ba52c8-4eca-48f0-8c6f-ab71fae9f468",
            "avatar_nova",
            "avatar",
            2000,
        ),
        (
            "ed617f8a-5b1a-4e68-ab60-17d554ac31a0",
            "avatar_blaze",
            "avatar",
            5000,
        ),
        (
            "dfcab7e4-064d-4adb-ae87-ca5842fd2d75",
            "avatar_skye",
            "avatar",
            500,
        ),
        (
            "6f4f4440-15a3-4623-bfe0-1092990ddd6e",
            "avatar_ignatius",
            "avatar",
            0,
        ),
        (
            "0c38d3e8-01a6-4260-bd39-0bc3137d0b05",
            "avatar_nereus",
            "avatar",
            250,
        ),
        (
            "d38c9096-6347-4ac9-a1d5-aa54b497c2bc",
            "avatar_terra",
            "avatar",
            0,
        ),
        (
            "f3c2b1ba-a68f-4a8e-a162-867d00b23ec8",
            "avatar_aqua",
            "avatar",
            250,
        ),
        (
            "5b97bad3-2255-4296-be2a-7c3f4f24ebb8",
            "avatar_alanventura",
            "avatar",
            1000,
        ),
        (
            "249adca0-0ede-4d4b-a00b-674f05075bf1",
            "avatar_hazel",
            "avatar",
            1000,
        ),
        (
            "f00837ac-4bbb-4542-82bd-5795d1dd5c3c",
            "avatar_cristal",
            "avatar",
            0,
        ),
        (
            "42c42366-402b-4f78-ac72-5020c7a80dbb",
            "avatar_ambre",
            "avatar",
            500,
        ),
        (
            "9d544dc2-39b3-496d-87b7-61090d33abad",
            "avatar_spike",
            "avatar",
            500,
        ),
        (
            "6e781b57-c39a-4970-99e2-86d03db506e2",
            "avatar_indianalan",
            "avatar",
            2000,
        ),
        (
            "7ab799a3-64dc-43a3-89ae-c053d8e83644",
            "avatar_skipper",
            "avatar",
            250,
        ),
        (
            "aa4253b5-d7b8-4af4-bd72-57559d01fd7b",
            "avatar_nadalan",
            "avatar",
            1000,
        ),
        (
            "27d27023-eebf-49de-b925-41e1bad49e3d",
            "avatar_scout",
            "avatar",
            500,
        ),
        (
            "f905a263-a3a2-40be-9dd3-e00d0d81a46a",
            "avatar_fleur",
            "avatar",
            250,
        ),
        (
            "aaec0582-f0c1-4783-aa12-aff7d3b5995b",
            "against_malaria_foundation",
            "charity",
            128,
        ),
        (
            "94f0732e-dfa0-4aa4-a761-43de36c1c95f",
            "gustave_roussy",
            "charity",
            100,
        ),
        (
            "9713ecaf-be2e-4d62-92f4-087ea96a3d3c",
            "avatar_amira",
            "avatar",
            1000,
        ),
        (
            "3f5fb7c8-1ab8-4685-801d-04453f8e7bea",
            "avatar_luminos",
            "avatar",
            0,
        ),
        (
            "5547a337-af18-44e8-b403-743e670b4109",
            "avatar_monarch",
            "avatar",
            2000,
        ),
        (
            "19cab99c-86b8-480d-ae24-2fde8932db04",
            "avatar_westlan",
            "avatar",
            500,
        ),
        (
            "378c78d5-b2db-44b6-afa0-9251b66e2282",
            "avatar_peanut",
            "avatar",
            500,
        ),
        (
            "e862ba38-bf1b-4d78-8f44-e50d46af2429",
            "avatar_batlan",
            "avatar",
            5000,
        ),
        (
            "f2ac1455-488a-4a3a-9cd0-c09871b67dbe",
            "avatar_ninjalan",
            "avatar",
            2000,
        ),
        (
            "d4099764-39cb-469c-97c2-79df0950ab36",
            "avatar_mc_alan",
            "avatar",
            2000,
        ),
        (
            "e25adc4c-846b-43fa-9df1-53e7ea57a4d3",
            "avatar_melody",
            "avatar",
            250,
        ),
        (
            "51fc8b23-74b7-49d0-8913-721d915c9423",
            "avatar_monsieur",
            "avatar",
            2000,
        ),
        (
            "e3502714-f3d8-40af-9d68-a6549ecdc367",
            "avatar_starwalker",
            "avatar",
            5000,
        ),
        (
            "a130d173-0a83-43b5-99b6-09fa9e706d87",
            "avatar_scarlet",
            "avatar",
            1000,
        ),
        (
            "3358991b-90cf-4390-a93d-19ec5d00ce80",
            "avatar_jade",
            "avatar",
            250,
        ),
        (
            "314b343c-a3e8-422f-ac7f-006208c37bab",
            "avatar_altruist",
            "avatar",
            250,
        ),
        (
            "7c680857-c2fa-47e8-b90e-19e931cf9209",
            "avatar_chillax",
            "avatar",
            500,
        ),
        (
            "e56e60c6-1347-40b3-b461-9ad8156fbfe3",
            "avatar_magus",
            "avatar",
            10000,
        ),
    )

    ACHIEVEMENT_DEFINITIONS = (
        (
            "d805fb78-8f7e-4264-85e8-b604742907de",
            "walk",
            "walk_brisk_10min_milestone3",
            50,
            "daily",
        ),
        (
            "4f937f73-6b74-4989-b706-0e5ced90e2cf",
            "walk",
            "walk_brisk_10min_milestone2",
            25,
            "daily",
        ),
        (
            "a3a72296-7667-48af-9cec-3266d3d3345f",
            "walk",
            "walk_brisk_10min_milestone1",
            10,
            "daily",
        ),
        (
            "6490d1a6-3b29-442b-a0c9-aec877579c91",
            "walk",
            "walk_brisk_milestone3",
            100,
            "daily",
        ),
        (
            "6bcbebd4-bb64-4254-a847-3399b431edb1",
            "walk",
            "walk_brisk_milestone2",
            50,
            "daily",
        ),
        (
            "94854b5a-109d-4549-bd02-0d2c8ea28b0a",
            "walk",
            "walk_brisk_milestone1",
            25,
            "daily",
        ),
        (
            "a911c8ab-1ef9-4c91-b702-d653d86501ef",
            "walk",
            "walk_world_tour",
            500,
            "once",
        ),
        (
            "67cf91b5-d808-4b35-b297-6f7f571697ac",
            "walk",
            "walk_day_10000",
            25,
            "daily",
        ),
        (
            "fbc47bf5-15fc-4f7b-a544-520b6972380a",
            "walk",
            "walk_day_5000",
            25,
            "daily",
        ),
        (
            "d21149c1-31de-4067-8f95-75f367c537c5",
            "walk",
            "walk_day_2500",
            25,
            "daily",
        ),
        (
            "dc7e6c1e-a9e5-481e-a0b5-086c1f556587",
            "walk",
            "walk_helper",
            500,
            "once",
        ),
        ("479312fd-ff87-45aa-85d9-8324ba1af9ca", "", "debug", 1000, "no_limit"),
        (
            "7eed5277-6183-47b3-8f60-e895408ff06a",
            "walk",
            "walking_challenge_daily14k",
            20,
            "daily",
        ),
        (
            "564e2654-d335-4b69-99d8-986d96ecbbd7",
            "walk",
            "walking_challenge_daily12k",
            20,
            "daily",
        ),
        (
            "88879826-0c29-40d0-8362-d68e3aa5de1b",
            "walk",
            "walking_challenge_referral_used",
            500,
            "no_limit",
        ),
        (
            "4e26dd33-fb9a-4222-a2d4-5ca194773c9e",
            "walk",
            "walking_challenge_referral_use",
            0,
            "once",
        ),
        (
            "ce46d386-5054-4482-871a-68ec305e53f5",
            "leaderboard",
            "walking_challenge_week_3rd",
            100,
            "weekly",
        ),
        (
            "f0ff37cf-2a2c-47dc-be18-82d0c7d5d98b",
            "leaderboard",
            "walking_challenge_week_2nd",
            500,
            "weekly",
        ),
        (
            "1d688775-1f87-4c72-9403-01ef2a50b655",
            "leaderboard",
            "walking_challenge_week_1st",
            1000,
            "weekly",
        ),
        (
            "02662b13-514a-45ee-8c8f-bd86df0d09e3",
            "leaderboard",
            "berries_leaderboard_1",
            0,
            "weekly",
        ),
        (
            "2142e9dc-4f1a-4ce9-8d1f-ba92990535ea",
            "leaderboard",
            "berries_leaderboard_2",
            0,
            "weekly",
        ),
        (
            "b28f6d0a-436a-4c1e-ad6b-25a8136822cf",
            "leaderboard",
            "berries_leaderboard_3",
            0,
            "weekly",
        ),
        (
            "1d04e89d-058a-4ea8-83ee-b02caa6a98fe",
            "leaderboard",
            "berries_leaderboard_5p",
            0,
            "weekly",
        ),
        (
            "89fc2435-d1bd-4c3a-87c9-e180a24ee475",
            "leaderboard",
            "berries_leaderboard_10p",
            0,
            "weekly",
        ),
        (
            "6d5c1dc9-7d12-47af-b00a-8b6f086596c9",
            "walk",
            "walking_challenge_daily10k",
            20,
            "daily",
        ),
        (
            "c3f28d67-fc37-466f-98c2-a110c321a23e",
            "walk",
            "walking_challenge_daily8k",
            20,
            "daily",
        ),
        (
            "d3046e23-bd91-4e99-aabb-fc09b0494058",
            "walk",
            "walking_challenge_daily6k",
            20,
            "daily",
        ),
        (
            "4fa07f0b-6820-45bd-b3ac-58b31ad526dd",
            "walk",
            "walking_challenge_daily4k",
            20,
            "daily",
        ),
        (
            "a39176b6-6ca2-49a4-ae33-d52d3402c892",
            "walk",
            "walking_challenge_daily2k",
            20,
            "daily",
        ),
        (
            "df2994fc-d509-435b-b84f-291a76d28264",
            "walk",
            "walking_challenge_started",
            100,
            "once",
        ),
        (
            "1104b4dd-5dca-4da5-8f93-d2af3e8eb3a9",
            "program",
            "baby_sleep_completed",
            100,
            "once",
        ),
        (
            "2c8a745e-3fe5-49aa-9507-f8fb0e5dabd5",
            "program",
            "backpain_completed",
            100,
            "once",
        ),
        (
            "a2ef539b-e67f-4049-8bba-888139186e5d",
            "care",
            "care_reward",
            25,
            "no_limit",
        ),
        (
            "e8b805ac-8491-4833-8795-9fd0ddfa240f",
            "care",
            "care_incompatible_device",
            100,
            "once",
        ),
        (
            "0f303a50-cf37-4d8e-a73a-dcd7369b1dfe",
            "mo",
            "mo_first_discussion",
            50,
            "once",
        ),
        (
            "f47ac10b-58cc-4372-a567-0e02b2c3d479",
            "quiz",
            "quiz",
            0,  # Points will be calculated dynamically based on correct answers
            "daily",
        ),
        (
            "d81dc10d-de37-4b9a-afa5-3c664ed7a029",
            "play",
            "team_challenge_1st",
            1000,
            "monthly",
        ),
        (
            "0197b629-6d81-4d25-9db9-59391611ec5a",
            "play",
            "team_challenge_2nd",
            500,
            "monthly",
        ),
        (
            "90719d1a-cf81-4d8e-b596-9c10b927f39b",
            "play",
            "team_challenge_3rd",
            200,
            "monthly",
        ),
    )

    current_session.add_all(
        [
            StoreItem(
                code=code,
                id=UUID(store_item_id),
                points=points,
                category=category,
            )
            for store_item_id, code, category, points in STORE_ITEMS
        ],
    )
    current_session.add_all(
        [
            AchievementDefinition(
                id=UUID(achievement_definition_id),
                category=category,
                code=code,
                points=points,
                declaration_limit=AchievementDeclarationLimit(declaration_limit),
            )
            for achievement_definition_id, category, code, points, declaration_limit in ACHIEVEMENT_DEFINITIONS
        ]
    )

    if commit:
        current_session.commit()
    else:
        current_session.flush()

player_authorisation

get_other_player_context_if_authorised

get_other_player_context_if_authorised(
    player_context, other_player_id=None, other_user_id=None
)
Source code in components/gamification/public/helpers/player_authorisation.py
def get_other_player_context_if_authorised(  # noqa: D103
    player_context: PlayerContextData,
    other_player_id: UUID | None = None,
    other_user_id: str | None = None,
) -> PlayerContextData | None:
    from components.gamification.internal.business_logic.queries.player import (
        get_player_context_from_player_id_cached,
        get_player_context_from_user_id_cached,
    )

    current_account_id = player_context.account_id
    current_player_id = player_context.player_id
    current_user_id = player_context.app_user_id

    if other_player_id == MBAPPE_PLAYER_ID:
        return MBAPPE_FAKE_PLAYER_CONTEXT
    if not other_player_id and not other_user_id:
        return player_context
    if other_player_id and other_player_id == current_player_id:
        return player_context
    if other_user_id and other_user_id == current_user_id:
        return player_context

    other_player_context = (
        get_player_context_from_player_id_cached(player_id=other_player_id)
        if other_player_id
        else get_player_context_from_user_id_cached(app_user_id=other_user_id)
        if other_user_id
        else None
    )
    if not other_player_context:
        return None

    other_account_id = other_player_context.account_id
    other_player_id = other_player_context.player_id
    other_user_id = other_player_context.app_user_id

    if current_account_id and current_account_id != other_account_id:
        current_logger.warning(
            "Requested user is not in the same account", other_user_id=other_user_id
        )
        abort(
            make_json_response(
                code=400,
                data={"error": "Requested user is not in the same account"},
            )
        )

    return other_player_context

get_other_player_id_if_authorized

get_other_player_id_if_authorized(
    player_context, other_player_id=None, other_user_id=None
)
Source code in components/gamification/public/helpers/player_authorisation.py
def get_other_player_id_if_authorized(  # noqa: D103
    player_context: PlayerContextData,
    other_player_id: UUID | None = None,
    other_user_id: str | None = None,
) -> UUID:
    from components.gamification.internal.models.player import Player

    current_account_id = player_context.account_id
    current_player_id = player_context.player_id
    current_user_id = player_context.app_user_id

    if other_player_id in SPECIAL_GUEST_PLAYER_IDS:
        return other_player_id
    if not other_player_id and not other_user_id:
        return current_player_id
    if other_player_id and other_player_id == current_player_id:
        return current_player_id
    if other_user_id and other_user_id == current_user_id:
        return current_player_id

    if other_player_id:
        other_account_id = current_session.scalar(
            select(Player.account_id).where(Player.id == other_player_id)
        )
    else:
        other_player_id, other_account_id = current_session.execute(
            select(Player.id, Player.account_id).where(
                Player.app_user_id == other_user_id
            )
        ).one()

    if current_account_id and current_account_id != other_account_id:
        current_logger.warning(
            "Requested user is not in the same account",
            other_user_id=other_user_id,
        )
        abort(
            make_json_response(
                code=400,
                data={"error": "Requested user is not in the same account"},
            )
        )

    return other_player_id

shop_rewards

compute_shop_berries_reward_for_product_cost

compute_shop_berries_reward_for_product_cost(product_cost)

Compute the berries reward for a product cost in the shop. The reward is calculated as 1% of the product cost, multiplied by 30. Make sure to keep in sync with computeShopBerriesRewardForProductCost

Source code in components/gamification/public/helpers/shop_rewards.py
1
2
3
4
5
6
7
def compute_shop_berries_reward_for_product_cost(product_cost: float) -> int:
    """
    Compute the berries reward for a product cost in the shop.
    The reward is calculated as 1% of the product cost, multiplied by 30.
    Make sure to keep in sync with computeShopBerriesRewardForProductCost
    """
    return int((1 / 100) * product_cost * 30)

strings

normalize_text

normalize_text(text)

Normalize player text search

Source code in components/gamification/public/helpers/strings.py
def normalize_text(text: str) -> str:
    """Normalize player text search"""
    # Normalize and remove diacritics
    text = unicodedata.normalize("NFD", text)
    text = "".join(c for c in text if unicodedata.category(c) != "Mn")
    # Convert to lower case
    return text.lower()

components.gamification.public.services

achievements

declare_achievement_for_app_user

declare_achievement_for_app_user(
    user_id, code, berries=None, collected=False
)

Declare an achievement for a player identified by their app user ID.

Source code in components/gamification/public/services/achievements.py
def declare_achievement_for_app_user(
    user_id: str,
    code: AchievementCode,
    berries: int | None = None,
    collected: bool = False,
) -> None:
    """
    Declare an achievement for a player identified by their app user ID.
    """
    player_id = get_player_id_from_user_id(user_id=user_id)
    if not player_id:
        current_logger.warning(
            "Player not found while trying to declare achievement",
            user_id=user_id,
            code=code,
        )
        return

    day = date.today()
    create_achievements(
        player_id=player_id,
        items=[AchievementCreationPayload(code=code, day=day, berries=berries)],
        collected=collected,
    )

has_achieved

has_achieved(player_id, code)

Check if a player has ever earned a given achievement.

Source code in components/gamification/public/services/achievements.py
def has_achieved(player_id: UUID, code: str) -> bool:
    """Check if a player has ever earned a given achievement."""
    from components.gamification.internal.models.achievement_summary import (
        AchievementSummary,
    )

    return bool(
        current_session.scalar(
            select(
                exists().where(
                    AchievementSummary.player_id == player_id,
                    AchievementSummary.code == code,
                )
            )
        )
    )

has_achieved_after

has_achieved_after(player_id, code, after_date)

Check if a player has earned a given achievement after a given date.

Source code in components/gamification/public/services/achievements.py
def has_achieved_after(player_id: UUID, code: str, after_date: date) -> bool:
    """Check if a player has earned a given achievement after a given date."""
    from components.gamification.internal.models.achievement_summary import (
        AchievementSummary,
    )

    return bool(
        current_session.scalar(
            select(
                exists().where(
                    AchievementSummary.player_id == player_id,
                    AchievementSummary.code == code,
                    AchievementSummary.last_achieved_on >= after_date,
                )
            )
        )
    )

player

get_player_id_from_user_id

get_player_id_from_user_id(user_id)

Retrieve players based on an app user IDs.

Parameters:

Name Type Description Default
user_id str

app user IDs.

required

Returns:

Name Type Description
UUID UUID | None

Corresponding player ID

Source code in components/gamification/public/services/player.py
def get_player_id_from_user_id(user_id: str) -> UUID | None:
    """
    Retrieve players based on an app user IDs.

    Args:
        user_id: app user IDs.

    Returns:
        UUID: Corresponding player ID
    """
    from components.gamification.internal.models.player import Player

    return current_session.scalar(select(Player.id).filter_by(app_user_id=user_id))

remove_player_data

remove_player_data(app_user_id, dry_run=True)

Removes all gamification info linked to a player Connects purchases to a fake player Reset player object fields

Source code in components/gamification/public/services/player.py
def remove_player_data(app_user_id: str, dry_run: bool = True) -> None:
    """
    Removes all gamification info linked to a player
    Connects purchases to a fake player
    Reset player object fields
    """
    from components.gamification.internal.business_logic.actions.achievement_summary import (
        delete_achievements_summary_for_player,
    )
    from components.gamification.internal.business_logic.actions.achievements import (
        delete_achievements_for_player,
    )
    from components.gamification.internal.business_logic.actions.activity_trip_cleanup import (
        delete_activity_trips_for_player,
    )
    from components.gamification.internal.business_logic.actions.berry_balance import (
        delete_berry_balance_for_player,
    )
    from components.gamification.internal.business_logic.actions.cycling_session import (
        delete_cycling_sessions_for_player,
    )
    from components.gamification.internal.business_logic.actions.daily_challenge import (
        delete_challenges_for_player,
    )
    from components.gamification.internal.business_logic.actions.duels import (
        delete_duels_for_player,
    )
    from components.gamification.internal.business_logic.actions.leagues import (
        remove_player_from_leagues,
    )
    from components.gamification.internal.business_logic.actions.player import (
        maybe_create_fake_player,
        reset_player,
    )
    from components.gamification.internal.business_logic.actions.purchases import (
        remove_purchases_for_player,
    )
    from components.gamification.internal.business_logic.actions.reactions import (
        delete_reactions_for_player,
    )
    from components.gamification.internal.business_logic.actions.records import (
        delete_records_for_player,
    )
    from components.gamification.internal.business_logic.actions.team_challenge import (
        delete_challenge_boosts_for_player,
        delete_challenge_invitations_for_player,
        delete_challenge_teams_for_player,
    )
    from components.gamification.internal.business_logic.actions.walk_history import (
        delete_steps_for_player,
    )

    maybe_create_fake_player(dry_run)

    player_id = get_player_id_from_user_id(user_id=app_user_id)  # Ensure player exists
    if player_id is None:
        current_logger.error("Player not found for cleanup", user_id=app_user_id)
        return

    reset_player(player_id, dry_run)
    delete_reactions_for_player(player_id, dry_run)
    delete_duels_for_player(player_id, dry_run)
    delete_steps_for_player(player_id, dry_run)
    delete_challenges_for_player(player_id, dry_run)
    delete_achievements_for_player(player_id, dry_run)
    delete_achievements_summary_for_player(player_id, dry_run)
    delete_cycling_sessions_for_player(player_id, dry_run)
    delete_activity_trips_for_player(player_id, dry_run)
    remove_player_from_leagues(player_id, dry_run)
    remove_purchases_for_player(player_id, dry_run)
    delete_challenge_boosts_for_player(player_id, dry_run)
    delete_challenge_invitations_for_player(player_id, dry_run)
    delete_challenge_teams_for_player(player_id, dry_run)
    delete_records_for_player(player_id, dry_run)
    delete_berry_balance_for_player(player_id, dry_run)

vouchers

create_onboarding_cookie_voucher(feature_user)

Create an onboarding cookie voucher for the given user.

Source code in components/gamification/public/services/vouchers.py
def create_onboarding_cookie_voucher(feature_user: FeatureUser) -> None:
    """
    Create an onboarding cookie voucher for the given user.
    """
    from components.gamification.internal.business_logic.actions.vouchers import (
        create_shop_onboarding_cookie_voucher,
    )

    if not get_onboarding_cookie_voucher_code_available():
        raise Exception("No onboarding cookie vouchers available")

    other_voucher = has_onboarding_cookie_voucher(feature_user)
    if other_voucher:
        current_logger.error("Alaner already unlocked shop onboarding voucher")
        raise Exception("Alaner already unlocked shop onboarding voucher")

    create_shop_onboarding_cookie_voucher(app_user_id=feature_user.app_user_id)

generate_discount_code

generate_discount_code()

Public wrapper to generate a fresh voucher discount code.

Source code in components/gamification/public/services/vouchers.py
def generate_discount_code() -> str:
    """
    Public wrapper to generate a fresh voucher discount code.
    """
    from components.gamification.internal.helpers.vouchers import (
        generate_discount_code as _generate,
    )

    return _generate()
get_onboarding_cookie_voucher_code_available()

Check if onboarding cookie vouchers are still available. Returns the voucher code if we haven't reached the limit of 200 vouchers (100 of each type). Returns None if we have reached the limit.

Source code in components/gamification/public/services/vouchers.py
def get_onboarding_cookie_voucher_code_available() -> VoucherCode | None:
    """
    Check if onboarding cookie vouchers are still available.
    Returns the voucher code if we haven't reached the limit of 200 vouchers (100 of each type).
    Returns None if we have reached the limit.
    """
    from components.gamification.internal.helpers.vouchers import (
        SHOP_ONBOARDING_442_COOKIE_VOUCHER_PREFIX,
        SHOP_ONBOARDING_956_COOKIE_VOUCHER_PREFIX,
    )
    from components.gamification.internal.models.voucher import Voucher

    amount_of_442_vouchers = (
        current_session.query(Voucher)  # noqa: ALN085
        .filter(
            Voucher.discount_code.like(f"{SHOP_ONBOARDING_442_COOKIE_VOUCHER_PREFIX}%")
        )
        .count()
    )
    amount_of_956_vouchers = (
        current_session.query(Voucher)  # noqa: ALN085
        .filter(
            Voucher.discount_code.like(f"{SHOP_ONBOARDING_956_COOKIE_VOUCHER_PREFIX}%")
        )
        .count()
    )

    if (amount_of_442_vouchers + amount_of_956_vouchers) >= 200:
        return None

    return (
        VoucherCode.voucher442
        if amount_of_442_vouchers < 100
        else VoucherCode.voucher956
    )

get_voucher_amount_in_euros

get_voucher_amount_in_euros(code)

Public accessor for the euro amount associated with a VoucherCode.

Source code in components/gamification/public/services/vouchers.py
def get_voucher_amount_in_euros(code: VoucherCode) -> float | int:
    """
    Public accessor for the euro amount associated with a VoucherCode.
    """
    from components.gamification.internal.helpers.vouchers import VOUCHER_CODE_PROPS

    if code not in VOUCHER_CODE_PROPS:
        raise KeyError(f"Voucher code {code.value!r} has no amount mapping")
    amount, _ = VOUCHER_CODE_PROPS[code]
    return amount
has_onboarding_cookie_voucher(feature_user)

Check if the user has an onboarding cookie voucher.

Source code in components/gamification/public/services/vouchers.py
def has_onboarding_cookie_voucher(feature_user: FeatureUser) -> bool:
    """
    Check if the user has an onboarding cookie voucher.
    """
    from sqlalchemy import or_

    from components.gamification.internal.helpers.vouchers import (
        SHOP_ONBOARDING_442_COOKIE_VOUCHER_PREFIX,
        SHOP_ONBOARDING_956_COOKIE_VOUCHER_PREFIX,
    )
    from components.gamification.internal.models.player import Player
    from components.gamification.internal.models.voucher import Voucher

    player = (
        current_session.query(Player)  # noqa: ALN085
        .filter(Player.app_user_id == feature_user.app_user_id)
        .filter(Player.app_id == feature_user.app_id)
        .one_or_none()
    )

    if not player:
        current_logger.error(
            "Player not found for user",
            feature_user=feature_user,
        )
        return False

    return (
        current_session.query(Voucher)  # noqa: ALN085
        .filter(
            Voucher.player_id == player.id,
            or_(
                Voucher.discount_code.like(
                    f"{SHOP_ONBOARDING_956_COOKIE_VOUCHER_PREFIX}%"
                ),
                Voucher.discount_code.like(
                    f"{SHOP_ONBOARDING_442_COOKIE_VOUCHER_PREFIX}%"
                ),
            ),
        )
        .one_or_none()
        is not None
    )

record_voucher_for_user

record_voucher_for_user(
    app_id, app_user_id, code, discount_code, expires_at
)

Public wrapper to persist a Voucher row for the given user. Caller owns any external side-effect (e.g. discount provider creation).

Source code in components/gamification/public/services/vouchers.py
def record_voucher_for_user(
    app_id: str,
    app_user_id: str,
    code: VoucherCode,
    discount_code: str,
    expires_at: datetime,
) -> "Voucher":
    """
    Public wrapper to persist a Voucher row for the given user.
    Caller owns any external side-effect (e.g. discount provider creation).
    """
    from components.gamification.internal.business_logic.actions.vouchers import (
        record_voucher_for_user as _record,
    )

    return _record(
        app_id=app_id,
        app_user_id=app_user_id,
        code=code,
        discount_code=discount_code,
        expires_at=expires_at,
    )