Skip to content

Api reference

components.pet.public.actions

Public actions for the pet component.

create_pet_profile

create_pet_profile(
    owner_global_user_id,
    name,
    species,
    breed,
    gender,
    birth_date,
    identification_number=None,
    lifestyle=None,
    picture_uri=None,
)

Create a PetProfile and return its id.

Does not commit — caller is responsible for committing.

Source code in components/pet/internal/business_logic/create_pet_profile.py
def create_pet_profile(
    owner_global_user_id: str,
    name: str,
    species: str,
    breed: str | None,
    gender: str,
    birth_date: date,
    identification_number: str | None = None,
    lifestyle: str | None = None,
    picture_uri: str | None = None,
) -> UUID:
    """Create a PetProfile and return its id.

    Does not commit — caller is responsible for committing.
    """
    from components.pet.internal.models.enums import PetGender, PetLifestyle, PetType
    from components.pet.internal.models.pet_profile import PetProfile

    pet = PetProfile()
    pet.owner_global_user_id = owner_global_user_id
    pet.name = name
    pet.species = PetType(species)
    pet.gender = PetGender(gender)
    pet.breed = breed
    pet.birth_date = birth_date
    pet.identification_number = identification_number
    pet.lifestyle = PetLifestyle(lifestyle) if lifestyle is not None else None
    pet.picture_uri = picture_uri

    current_session.add(pet)
    current_session.flush()

    return pet.id

create_pet_subscription_request

create_pet_subscription_request(
    owner_global_user_id,
    name,
    species,
    breed,
    gender,
    birth_date,
    identification_number=None,
    lifestyle=None,
    pet_picture_uri=None,
    draft_id=None,
)

Create a PetProfile, an InsurancePlan, and request CG/CP signature.

Enrollment (core_enrollment ChangeRequest) does NOT happen here anymore. It is deferred to the post-signature ContractCreated events_pipeline handler in pet.internal.events_pipeline_consumers — pet is only enrolled once the owner e-signs CP via Dropbox Sign and contracting publishes ContractCreated.

Does not commit — caller is responsible for committing.

Source code in components/pet/internal/business_logic/enroll_pet.py
def create_pet_subscription_request(
    owner_global_user_id: str,
    name: str,
    species: str,
    breed: str | None,
    gender: str,
    birth_date: date,
    identification_number: str | None = None,
    lifestyle: str | None = None,
    pet_picture_uri: str | None = None,
    draft_id: UUID | None = None,
) -> UUID:
    """Create a PetProfile, an InsurancePlan, and request CG/CP signature.

    Enrollment (core_enrollment ChangeRequest) does NOT happen here anymore.
    It is deferred to the post-signature `ContractCreated` events_pipeline
    handler in `pet.internal.events_pipeline_consumers` — pet is only
    enrolled once the owner e-signs CP via Dropbox Sign and contracting
    publishes `ContractCreated`.

    Does not commit — caller is responsible for committing.
    """
    from uuid import uuid4

    from components.insurance_plan_catalog.internal.product_custom_data.pet_insurance.factory import (  # noqa: ALN043
        create_and_save_pet_plan,
    )
    from components.member_lifecycle.public.enums import (
        ExtendedDependentEnrollmentType,
    )
    from components.member_lifecycle.subcomponents.add_dependents.public.dependent_draft import (
        create_dependent_draft,
        link_draft_to_profile,
    )
    from components.pet.internal.business_logic.create_pet_profile import (
        create_pet_profile,
    )
    from components.pet.internal.business_logic.pricer import (
        PetPricingParameters,
        compute_monthly_price_in_cents,
    )
    from shared.helpers.time.calculus import calendar_age_on
    from shared.helpers.time.utc import utctoday

    if breed is None:
        raise ValueError("breed is required to compute the pet insurance price")

    # 1. Create PetProfile + the generic onboarding draft. The draft carries
    # the in-flight AR id once the signature pipeline returns it, and is
    # deleted by the post-signature handler. Member-lifecycle owns the draft
    # model so partner/child can reuse the same shape later.
    pet_id = create_pet_profile(
        owner_global_user_id=owner_global_user_id,
        name=name,
        species=species,
        breed=breed,
        gender=gender,
        birth_date=birth_date,
        identification_number=identification_number,
        lifestyle=lifestyle,
        picture_uri=pet_picture_uri,
    )
    if draft_id is not None:
        # FE pre-created a draft at flow start and accreted form data
        # step-by-step; link it instead of creating a parallel one.
        link_draft_to_profile(draft_id=draft_id, dependent_profile_id=pet_id)
    else:
        # Backward-compat path: callers that haven't adopted the
        # per-step draft flow still get a fresh draft tied to the pet.
        create_dependent_draft(
            actor_global_user_id=owner_global_user_id,
            dependent_type=ExtendedDependentEnrollmentType.pet,
            dependent_profile_id=pet_id,
        )

    # 2. Create InsurancePlan with the priced premium.
    # Calendar age (rolls over Jan 1st) gives a price-friendly, deterministic
    # age across the calendar year — premium changes only at year boundaries.
    age_years = calendar_age_on(
        birth_date=birth_date, date=utctoday(), adulthood_age=100
    )
    monthly_price_cents = compute_monthly_price_in_cents(
        PetPricingParameters(breed_key=breed, age_years=age_years)
    )
    plan = create_and_save_pet_plan(
        pet_id=pet_id,
        owner_global_user_id=owner_global_user_id,
        pet_display_name=name,
        display_name=f"Pet Insurance - {name}",
        fixed_monthly_price_cents=monthly_price_cents,
    )

    # 3. Dispatch the contracting-v2 pipeline. Generates CG + CP, requests a
    # Dropbox Sign signature on CP and leaves the package in
    # `waiting_for_approval`. `ApproveContractingPackageCommandHandler` runs
    # later — triggered by `LegalDocumentsSignedEvent` once the owner signs —
    # and that's when the Contract row is persisted and the post-signature
    # handler fires to call `ChangeRequestService.create_and_process`.
    contract_id = uuid4()
    start_date = utctoday()

    from shared.helpers.env import env
    from shared.helpers.logging.logger import current_logger

    if env.bool("ALAN_DEV_AUTO_ENROLL_PET", default=False):
        current_logger.info(
            "ALAN_DEV_AUTO_ENROLL_PET=1 — short-circuiting Dropbox dispatch + "
            "events_pipeline drain; pet will be enrolled synchronously",
            pet_id=pet_id,
            contract_id=contract_id,
        )
        _dispatch_and_enroll_pet_synchronously(
            pet_id=pet_id,
            contract_id=contract_id,
            owner_global_user_id=owner_global_user_id,
            pet_name=name,
            plan_id=plan.id,
            start_date=start_date,
        )
        return pet_id

    _dispatch_contracting_v2_pipeline(
        pet_id=pet_id,
        contract_id=contract_id,
        owner_global_user_id=owner_global_user_id,
        pet_name=name,
        plan_id=plan.id,
        start_date=start_date,
    )

    return pet_id

components.pet.public.commands

components.pet.public.entities

DraftPetSummary dataclass

DraftPetSummary(
    id,
    owner_user_id,
    name,
    species,
    breed,
    birth_date,
    gender,
    identification_number,
    lifestyle,
    created_at,
)

Bases: DataClassJsonMixin

An in-flight DependentDraft of dependent_type=pet, not yet at Confirmation.

Carries whatever subset of fields the user has filled so far. The id is the DependentDraft.id — the FE uses it to fetch the full record and to seed the resume flow. Drafts without name are filtered server-side and never surface here.

birth_date instance-attribute

birth_date

breed instance-attribute

breed

created_at instance-attribute

created_at

gender instance-attribute

gender

id instance-attribute

id

identification_number instance-attribute

identification_number

kind class-attribute instance-attribute

kind = field(default='draft', init=False)

lifestyle instance-attribute

lifestyle

name instance-attribute

name

owner_user_id instance-attribute

owner_user_id

species instance-attribute

species

EnrolledPetSummary dataclass

EnrolledPetSummary(
    id,
    name,
    species,
    breed,
    birth_date,
    owner_user_id,
    gender,
    identification_number,
    lifestyle,
    picture_uri,
)

Bases: DataClassJsonMixin

A pet currently enrolled on a pet-insurance contract (PetProfile + core_enrollment).

birth_date instance-attribute

birth_date

breed instance-attribute

breed

gender instance-attribute

gender

id instance-attribute

id

identification_number instance-attribute

identification_number

kind class-attribute instance-attribute

kind = field(default='enrolled', init=False)

lifestyle instance-attribute

lifestyle

name instance-attribute

name

owner_user_id instance-attribute

owner_user_id

picture_uri instance-attribute

picture_uri

species instance-attribute

species

PendingPetSummary dataclass

PendingPetSummary(
    id,
    name,
    species,
    breed,
    birth_date,
    owner_user_id,
    gender,
    identification_number,
    picture_uri,
    lifestyle,
)

Bases: DataClassJsonMixin

A pet whose signature was submitted but whose backend enrollment is still in flight.

Materializes a PetProfile joined with a DependentDraft whose signature_submitted_at is set: the user finished the Dropbox Sign iframe but the async webhook hasn't yet completed enrollment. The frontend surfaces this as a "pending" pet in the profile list and polls until the draft is deleted and the pet transitions to enrolled.

birth_date instance-attribute

birth_date

breed instance-attribute

breed

gender instance-attribute

gender

id instance-attribute

id

identification_number instance-attribute

identification_number

kind class-attribute instance-attribute

kind = field(default='pending', init=False)

lifestyle instance-attribute

lifestyle

name instance-attribute

name

owner_user_id instance-attribute

owner_user_id

picture_uri instance-attribute

picture_uri

species instance-attribute

species

PetSummary module-attribute

PetSummary = (
    EnrolledPetSummary
    | PendingPetSummary
    | DraftPetSummary
    | WaitlistPetSummary
)

WaitlistPetSummary dataclass

WaitlistPetSummary(
    id,
    name,
    species,
    breed,
    birth_date,
    owner_user_id,
    gender,
    picture_uri,
    is_chipped,
    is_already_insured,
    expected_monthly_price,
    created_at,
)

Bases: DataClassJsonMixin

A pet registered on the waiting list (PetWaitlistEntry).

birth_date instance-attribute

birth_date

breed instance-attribute

breed

created_at instance-attribute

created_at

expected_monthly_price instance-attribute

expected_monthly_price

gender instance-attribute

gender

id instance-attribute

id

is_already_insured instance-attribute

is_already_insured

is_chipped instance-attribute

is_chipped

kind class-attribute instance-attribute

kind = field(default='waitlist', init=False)

name instance-attribute

name

owner_user_id instance-attribute

owner_user_id

picture_uri instance-attribute

picture_uri

species instance-attribute

species

components.pet.public.enums

Public pet enums.

Re-exports the canonical pet-domain enums so other components can use them at the public boundary (e.g. member_lifecycle referencing PetType for its draft pet_species column) without reaching into pet.internal.

PetLifestyle

Bases: AlanBaseEnum

Pet living environment. Values today are cat-specific (PET-29); other lifestyles to be added if needed.

always_indoors class-attribute instance-attribute

always_indoors = 'always_indoors'

balcony_or_window class-attribute instance-attribute

balcony_or_window = 'balcony_or_window'

category property

category

Indoor/outdoor grouping (PET-29): never-outside & balcony/window → indoor; garden/terrace & free access → outdoor.

freely_outside class-attribute instance-attribute

freely_outside = 'freely_outside'

garden_or_terrace class-attribute instance-attribute

garden_or_terrace = 'garden_or_terrace'

PetType

Bases: AlanBaseEnum

cat class-attribute instance-attribute

cat = 'cat'

dog class-attribute instance-attribute

dog = 'dog'

components.pet.public.labels

Localized display labels for pet attributes (species, gender, lifestyle, breed).

Pure i18n helpers (value + Lang → str). The label tables live in internal/business_logic/labels.py; these are the public wrappers.

get_breed_display_name

get_breed_display_name(breed_key, lang)

Localized display label for a breed i18n key (e.g. breeds.dog.beagle).

Falls back to French, then to the raw key when unmapped (e.g. a breed added to the frontend but not yet synced into the label map).

Source code in components/pet/public/labels.py
def get_breed_display_name(breed_key: str, lang: Lang) -> str:
    """Localized display label for a breed i18n key (e.g. ``breeds.dog.beagle``).

    Falls back to French, then to the raw key when unmapped (e.g. a breed added
    to the frontend but not yet synced into the label map).
    """
    from components.pet.internal.business_logic.labels import (
        BREED_LABELS,
        resolve_label,
    )

    return resolve_label(BREED_LABELS, lang, breed_key)

get_gender_label

get_gender_label(gender, lang)

Localized display label for a pet gender (e.g. male → "Mâle").

Source code in components/pet/public/labels.py
def get_gender_label(gender: str, lang: Lang) -> str:
    """Localized display label for a pet gender (e.g. ``male`` → "Mâle")."""
    from components.pet.internal.business_logic.labels import (
        GENDER_LABELS,
        resolve_label,
    )

    return resolve_label(GENDER_LABELS, lang, gender)

get_lifestyle_category_label

get_lifestyle_category_label(lifestyle, lang)

Localized indoor/outdoor category for the CP "Mode de vie" — "Intérieur" / "Extérieur". Collapses the fine-grained PetLifestyle (cat-specific) into a coarse category. None when the pet has no lifestyle (e.g. dogs), so callers can show their own fallback.

Source code in components/pet/public/labels.py
def get_lifestyle_category_label(lifestyle: str | None, lang: Lang) -> str | None:
    """Localized indoor/outdoor category for the CP "Mode de vie" — "Intérieur" /
    "Extérieur". Collapses the fine-grained ``PetLifestyle`` (cat-specific) into a
    coarse category. ``None`` when the pet has no lifestyle (e.g. dogs), so callers
    can show their own fallback.
    """
    if lifestyle is None:
        return None

    from components.pet.internal.business_logic.labels import (
        LIFESTYLE_CATEGORY_LABELS,
        resolve_label,
    )
    from components.pet.internal.models.enums import PetLifestyle

    category = PetLifestyle(lifestyle).category
    return resolve_label(LIFESTYLE_CATEGORY_LABELS, lang, category.value)

get_species_label

get_species_label(species, lang)

Localized display label for a pet species (e.g. dog → "Chien").

Source code in components/pet/public/labels.py
def get_species_label(species: str, lang: Lang) -> str:
    """Localized display label for a pet species (e.g. ``dog`` → "Chien")."""
    from components.pet.internal.business_logic.labels import (
        SPECIES_LABELS,
        resolve_label,
    )

    return resolve_label(SPECIES_LABELS, lang, species)

components.pet.public.queries

compute_pet_monthly_price_in_cents

compute_pet_monthly_price_in_cents(*, breed_key, age_years)

Monthly premium in cents at the currently sold (rate=80%, ceiling=2000€) combo.

Raises ValueError on unknown breed or age outside 0..7.

Source code in components/pet/public/queries.py
def compute_pet_monthly_price_in_cents(*, breed_key: str, age_years: int) -> int:
    """Monthly premium in cents at the currently sold (rate=80%, ceiling=2000€) combo.

    Raises ValueError on unknown breed or age outside 0..7.
    """
    from components.pet.internal.business_logic.pricer import (
        PetPricingParameters,
        compute_monthly_price_in_cents,
    )

    return compute_monthly_price_in_cents(
        PetPricingParameters(breed_key=breed_key, age_years=age_years)
    )

get_pet_profile

get_pet_profile(pet_id)

Retrieve a pet profile by ID.

Source code in components/pet/public/queries.py
def get_pet_profile(pet_id: UUID) -> EnrolledPetSummary | None:
    """Retrieve a pet profile by ID."""
    from components.pet.internal.models.pet_profile import PetProfile
    from shared.helpers.db import current_session

    pet = current_session.get(PetProfile, pet_id)
    if pet is None:
        return None

    return _to_enrolled_summary(pet)

is_known_breed_key

is_known_breed_key(breed_key)

Whether breed_key is a recognised canonical i18n breed key (e.g. breeds.dog.labrador_retriever).

Source code in components/pet/public/queries.py
def is_known_breed_key(breed_key: str) -> bool:
    """Whether `breed_key` is a recognised canonical i18n breed key (e.g. `breeds.dog.labrador_retriever`)."""
    from components.pet.internal.business_logic.price_tiers import BREED_TO_TIER

    return breed_key in BREED_TO_TIER

list_draft_pets_for_owner

list_draft_pets_for_owner(owner_user_id)

In-flight pet drafts for owner_user_id: no PetProfile yet, no signature dispatched, has a name. Surfaced on the profile list as clickable "resume" rows.

Source code in components/pet/public/queries.py
def list_draft_pets_for_owner(owner_user_id: str) -> list[DraftPetSummary]:
    """In-flight pet drafts for `owner_user_id`: no PetProfile yet, no
    signature dispatched, has a name. Surfaced on the profile list as
    clickable "resume" rows.
    """
    from components.member_lifecycle.public.enums import (
        ExtendedDependentEnrollmentType,
    )
    from components.member_lifecycle.public.queries import (
        list_in_flight_drafts_for_actor,
    )

    drafts = list_in_flight_drafts_for_actor(
        actor_global_user_id=owner_user_id,
        dependent_type=ExtendedDependentEnrollmentType.pet,
    )
    return [
        DraftPetSummary(
            id=draft.id,
            owner_user_id=draft.actor_global_user_id,
            # Filtered server-side: `first_name` is guaranteed non-null.
            name=draft.first_name,  # type: ignore[arg-type]
            species=draft.pet_species,
            breed=draft.pet_breed,
            birth_date=draft.birth_date,
            gender=draft.gender,
            identification_number=draft.pet_identification_number,
            lifestyle=draft.pet_lifestyle,
            created_at=draft.created_at,
        )
        for draft in drafts
    ]

list_pending_pets_for_owner

list_pending_pets_for_owner(owner_user_id)

List the user's pets whose Dropbox Sign signature was submitted but whose backend enrollment hasn't yet completed (DependentDraft still alive).

The draft row is deleted on ContractCreated so this list naturally drains as the webhook fires. Ordering is newest pet first by PetProfile.created_at (a faithful proxy: signature is submitted seconds-to-minutes after the profile is created).

Source code in components/pet/public/queries.py
def list_pending_pets_for_owner(owner_user_id: str) -> list[PendingPetSummary]:
    """List the user's pets whose Dropbox Sign signature was submitted but whose
    backend enrollment hasn't yet completed (DependentDraft still alive).

    The draft row is deleted on `ContractCreated` so this list naturally
    drains as the webhook fires. Ordering is newest pet first by
    `PetProfile.created_at` (a faithful proxy: signature is submitted
    seconds-to-minutes after the profile is created).
    """
    from sqlalchemy import select

    from components.member_lifecycle.public.enums import (
        ExtendedDependentEnrollmentType,
    )
    from components.member_lifecycle.public.queries import (
        list_drafts_with_submitted_signature_for_actor,
    )
    from components.pet.internal.models.pet_profile import PetProfile
    from shared.helpers.db import current_session

    drafts = list_drafts_with_submitted_signature_for_actor(
        actor_global_user_id=owner_user_id,
        dependent_type=ExtendedDependentEnrollmentType.pet,
    )
    profile_ids = {
        draft.dependent_profile_id
        for draft in drafts
        if draft.dependent_profile_id is not None
    }
    if not profile_ids:
        return []

    stmt = (
        select(PetProfile)
        .where(
            PetProfile.id.in_(profile_ids),
            PetProfile.owner_global_user_id == owner_user_id,
        )
        .order_by(PetProfile.created_at.desc())
    )
    pets = current_session.scalars(stmt).all()
    return [_to_pending_summary(pet) for pet in pets]

list_pet_profiles_for_owner

list_pet_profiles_for_owner(owner_global_user_id)

List pet profiles currently enrolled by owner_global_user_id.

Cross-checks core_enrollment: only pets with an active enrollment period (membership_type=pet, period not ended) are returned. Pets whose PetProfile row exists but never reached an EnrollmentPeriod are excluded.

Source code in components/pet/public/queries.py
def list_pet_profiles_for_owner(
    owner_global_user_id: str,
) -> list[EnrolledPetSummary]:
    """List pet profiles currently enrolled by `owner_global_user_id`.

    Cross-checks core_enrollment: only pets with an active enrollment period
    (membership_type=pet, period not ended) are returned. Pets whose
    PetProfile row exists but never reached an EnrollmentPeriod are excluded.
    """
    from sqlalchemy import select

    from components.core_enrollment.public.python_api.enrollment_group import (
        SourceOfTruth,
        list_enrollment_group_views_by_primary,
    )
    from components.pet.internal.models.pet_profile import PetProfile
    from shared.core_stack.enums.membership_type import MembershipType
    from shared.core_stack.typing import GlobalUserId
    from shared.helpers.db import current_session
    from shared.helpers.time.utc import utctoday

    today = utctoday()
    # Owner is the primary on pet enrollment groups; pet is the only member.
    # `list_enrollment_group_views_by_primary` matches the owner; `_by_member` would not.
    groups = list_enrollment_group_views_by_primary(
        GlobalUserId(owner_global_user_id),
        source_of_truth=SourceOfTruth.force_core_stack,
    )

    pet_id_to_start_date: dict[UUID, date] = {}
    for group in groups:
        for period in group.periods:
            if period.membership_type != MembershipType.pet.value:
                continue
            if period.end_date is not None and period.end_date < today:
                continue
            try:
                pet_id = UUID(period.member_id)
            except ValueError:
                # member_id is expected to be a UUID for pet periods.
                continue
            # Keep the earliest start_date if a pet appears in multiple periods.
            existing = pet_id_to_start_date.get(pet_id)
            if existing is None or period.start_date < existing:
                pet_id_to_start_date[pet_id] = period.start_date

    if not pet_id_to_start_date:
        return []

    stmt = select(PetProfile).where(
        PetProfile.id.in_(pet_id_to_start_date.keys()),
        PetProfile.owner_global_user_id == owner_global_user_id,
    )
    pets = current_session.scalars(stmt).all()
    # Newest enrollment first.
    pets_sorted = sorted(pets, key=lambda p: pet_id_to_start_date[p.id], reverse=True)
    return [_to_enrolled_summary(pet) for pet in pets_sorted]

list_pets_for_owner

list_pets_for_owner(owner_user_id)

List a user's pets — enrolled, then pending (signature done but enrollment in flight), then draft (pre-Confirmation, user-resumable), then waiting-list.

Enrolled pets come from core_enrollment (membership_type=pet, active periods). Pending pets come from DependentDraft rows whose signature was submitted but whose ContractCreated webhook hasn't fired yet (deletes the draft). Draft pets come from in-flight DependentDraft rows (no profile, no AR id, has a name) — user abandoned the add-pet flow mid-way and can resume. Waiting-list entries come from PetWaitlistEntry rows scoped to the user.

Ordering is stable: enrolled then pending then draft then waiting-list, each newest-first within its group.

owner_user_id is the true global_user_id: enrolled/pending/draft key on it directly; the waitlist (app-local app_user_id) is translated internally.

Source code in components/pet/public/queries.py
def list_pets_for_owner(owner_user_id: str) -> list[PetSummary]:
    """List a user's pets — enrolled, then pending (signature done but enrollment in flight),
    then draft (pre-Confirmation, user-resumable), then waiting-list.

    Enrolled pets come from `core_enrollment` (membership_type=pet, active periods).
    Pending pets come from `DependentDraft` rows whose signature was submitted
    but whose `ContractCreated` webhook hasn't fired yet (deletes the draft).
    Draft pets come from in-flight `DependentDraft` rows (no profile, no AR id,
    has a name) — user abandoned the add-pet flow mid-way and can resume.
    Waiting-list entries come from `PetWaitlistEntry` rows scoped to the user.

    Ordering is stable: enrolled then pending then draft then waiting-list,
    each newest-first within its group.

    `owner_user_id` is the true global_user_id: enrolled/pending/draft key on it
    directly; the waitlist (app-local `app_user_id`) is translated internally.
    """
    enrolled = list_pet_profiles_for_owner(owner_user_id)
    enrolled_ids = {pet.id for pet in enrolled}
    pending = [
        pet
        for pet in list_pending_pets_for_owner(owner_user_id)
        if pet.id not in enrolled_ids
    ]
    draft = list_draft_pets_for_owner(owner_user_id)
    waitlist = list_waitlist_pets_for_owner(owner_user_id)
    return [*enrolled, *pending, *draft, *waitlist]

list_waitlist_pets_for_owner

list_waitlist_pets_for_owner(owner_user_id)

List the user's pet waiting-list entries, newest first.

owner_user_id is the true global_user_id. Waitlist rows are keyed on the app-local app_user_id (FeatureUserMixin), so translate global → local before matching.

Source code in components/pet/public/queries.py
def list_waitlist_pets_for_owner(owner_user_id: str) -> list[WaitlistPetSummary]:
    """List the user's pet waiting-list entries, newest first.

    `owner_user_id` is the true global_user_id. Waitlist rows are keyed on the
    app-local `app_user_id` (FeatureUserMixin), so translate global → local
    before matching.
    """
    from sqlalchemy import select

    from components.pet.internal.business_logic.owner_id import (
        local_user_ids_for_owner,
    )
    from components.pet.internal.models.pet_waitlist_entry import PetWaitlistEntry
    from shared.helpers.db import current_session

    local_user_ids = local_user_ids_for_owner(owner_user_id)
    stmt = (
        select(PetWaitlistEntry)
        .where(PetWaitlistEntry.app_user_id.in_(local_user_ids))
        .order_by(PetWaitlistEntry.created_at.desc())
    )
    entries = current_session.scalars(stmt).all()
    return [
        _to_waitlist_summary(entry, owner_global_user_id=owner_user_id)
        for entry in entries
    ]