Skip to content

feature/capr 59 service layer profile#162

Merged
shamikkarkhanis merged 4 commits intodevelopfrom
feature/capr-59-service-layer-profile
Apr 7, 2026
Merged

feature/capr 59 service layer profile#162
shamikkarkhanis merged 4 commits intodevelopfrom
feature/capr-59-service-layer-profile

Conversation

@YC-5002
Copy link
Copy Markdown

@YC-5002 YC-5002 commented Apr 7, 2026

  • Added service layer.. Testing...
  • cleaned up profile.py with safer button setup
  • clean up function; added finalize function to service
  • added to test_profile to test service

Summary by Sourcery

Extract profile storage, validation, and embed construction into a dedicated service layer and update the profile cog to use it while expanding test coverage of profile flows.

New Features:

  • Introduce a ProfileService to own profile persistence, validation, and embed construction for the profile feature.
  • Add a dedicated ProfileLaunchButton to safely delegate opening the profile editor modal from views.

Enhancements:

  • Refactor the profile cog to delegate business logic to the ProfileService instead of managing in-memory storage directly.
  • Improve error handling in profile flows by using custom exceptions and a shared helper for sending error embeds.

Tests:

  • Expand profile tests to cover service-backed create, update, show, and validation failure flows, including embed content and success messaging.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Apr 7, 2026

Reviewer's Guide

Refactors the profile cog to delegate persistence, validation, and embed construction to a new ProfileService, introduces safer button/view wiring for the profile modal launcher, and expands tests to cover the new service-driven behavior and error conditions.

Sequence diagram for service-based profile create and update flow

sequenceDiagram
    actor User
    participant Discord as DiscordClient
    participant Profile as ProfileCog
    participant Service as ProfileService
    participant Launcher as ProfileModalLauncherView

    User->>Discord: invoke /profile action=create|update
    Discord->>Profile: profile(interaction, action)
    Profile->>Profile: handle_edit_action(interaction, action)
    Profile->>Service: start_edit(user.id, action)

    alt profile exists for create
        Service-->>Profile: raise ProfileExistsError
        Profile->>Discord: _send_error("Profile Exists")
    else profile missing for update
        Service-->>Profile: raise ProfileNotFoundError
        Profile->>Discord: _send_error("No Profile")
    else valid start_edit
        Service-->>Profile: initial_data
        Profile->>Profile: _open_profile_identity_modal(interaction, action, initial_data)
        Profile->>Discord: show identity modal

        User->>Discord: submit identity
        Discord->>Profile: _handle_profile_identity_submit(interaction, identity, action)
        Profile->>Service: merge_identity_step(user.id, identity)
        Service-->>Profile: profile_data
        Profile->>Launcher: create ProfileModalLauncherView(callback=_open_profile_details_modal)
        Profile->>Discord: send button view

        User->>Discord: click profile button
        Discord->>Launcher: ProfileLaunchButton.callback(interaction)
        Launcher->>Launcher: open_modal(interaction)
        Launcher->>Profile: _open_profile_details_modal(interaction, action, profile_data)
        Profile->>Discord: show details modal

        User->>Discord: submit details
        Discord->>Profile: _handle_profile_details_submit(interaction, details, profile_data, action)
        Profile->>Service: finalize_profile(user, details, profile_data, action)

        alt invalid combined profile
            Service-->>Profile: raise InvalidProfileError
            Profile->>Discord: _send_error("Profile Validation Failed")
        else valid combined profile
            Service-->>Profile: profile, result(created|updated)
            Profile->>Profile: _handle_profile_submit(interaction, profile, result)
            Profile->>Service: create_profile_embed(user, profile)
            Service-->>Profile: embed
            Profile->>Discord: send success + profile embeds
        end
    end
Loading

Class diagram for new ProfileService and updated profile cog

classDiagram
    class ProfileService {
      -logging.Logger log
      -dict~int,UserProfileSchema~ _profiles
      +ProfileService(bot: discord.Client, log: logging.Logger)
      +start_edit(user_id: int, action: str) dict~str,Any~
      +merge_identity_step(user_id: int, identity: UserProfileIdentitySchema) dict~str,Any~
      +build_profile(user: discord.abc.User, details: UserProfileDetailsSchema, profile_data: dict~str,Any~) UserProfileSchema
      +finalize_profile(user: discord.abc.User, details: UserProfileDetailsSchema, profile_data: dict~str,Any~, action: Literal_create_update_) tuple~UserProfileSchema,Literal_created_updated_~
      +get_profile(user_id: int) UserProfileSchema
      +save_profile(user: discord.abc.User, profile: UserProfileSchema) void
      +delete_profile(user: discord.abc.User) void
      +create_profile_embed(user: discord.User|discord.Member, profile: UserProfileSchema) discord.Embed
    }

    class ProfileExistsError {
    }

    class ProfileNotFoundError {
    }

    class InvalidProfileError {
    }

    class Profile {
      -commands.Bot bot
      -logging.Logger log
      -ProfileService service
      +handle_edit_action(interaction: discord.Interaction, action: str) void
      +_handle_profile_identity_submit(interaction: discord.Interaction, identity: UserProfileIdentitySchema, action: str) void
      +_open_profile_details_modal(interaction: discord.Interaction, action: str, profile_data: dict~str,Any~) void
      +_handle_profile_details_submit(interaction: discord.Interaction, details: UserProfileDetailsSchema, profile_data: dict~str,Any~, action: Literal_create_update_) void
      +handle_show_action(interaction: discord.Interaction) void
      +handle_delete_action(interaction: discord.Interaction) void
      +_handle_profile_submit(interaction: discord.Interaction, profile: UserProfileSchema, result: Literal_created_updated_) void
      +_send_error(interaction: discord.Interaction, title: str, message: str) void
    }

    class ConfirmDeleteView {
      +bool|None value
      +ConfirmDeleteView()
      +confirm(interaction: discord.Interaction, button: ui.Button) void
      +cancel(interaction: discord.Interaction, button: ui.Button) void
    }

    class ProfileModalLauncherView {
      -Callable callback
      +ProfileModalLauncherView(callback: Callable, button_label: str, button_emoji: str|None, button_style: discord.ButtonStyle)
      +open_modal(interaction: discord.Interaction) void
    }

    class ProfileLaunchButton {
      +launcher_view ProfileModalLauncherView
      +callback(interaction: discord.Interaction) void
    }

    class BaseView {
    }

    class UIButton {
    }

    ProfileService ..> UserProfileSchema
    ProfileService ..> UserProfileIdentitySchema
    ProfileService ..> UserProfileDetailsSchema
    ProfileService ..> ProfileExistsError
    ProfileService ..> ProfileNotFoundError
    ProfileService ..> InvalidProfileError

    Profile --> ProfileService

    ConfirmDeleteView --|> BaseView
    ProfileModalLauncherView --|> BaseView
    ProfileLaunchButton --|> UIButton

    ProfileModalLauncherView o--> ProfileLaunchButton
    ProfileLaunchButton --> ProfileModalLauncherView
Loading

File-Level Changes

Change Details Files
Extract profile persistence, validation, and embed creation into a dedicated ProfileService and integrate it into the Profile cog.
  • Replace in-cog in-memory profile store with a ProfileService instance attached to the bot
  • Introduce ProfileExistsError, ProfileNotFoundError, and InvalidProfileError to model service-layer failure modes
  • Delegate create/update/show/delete actions and profile validation to ProfileService methods, including finalize_profile and get_profile
  • Use service-provided create_profile_embed for building profile embeds and centralize logging and storage there
  • Add a shared _send_error helper for consistent ephemeral error responses
capy_discord/exts/profile/profile.py
capy_discord/exts/profile/_service.py
Harden the profile modal launcher button wiring using a typed button subclass and explicit async callback typing.
  • Add ProfileLaunchButton subclass that validates its parent view and delegates clicks to ProfileModalLauncherView.open_modal
  • Change ProfileModalLauncherView callback type to return an Awaitable and expose open_modal instead of a private button callback
  • Replace dynamically-assigned Button.callback with construction-time wiring of ProfileLaunchButton
capy_discord/exts/profile/profile.py
Expand and refactor tests to exercise service-backed profile flows and new error handling.
  • Introduce a reusable interaction fixture that fully mocks interaction user, response, followup, and original_response
  • Add tests for create/update/show flows with and without existing profiles, asserting correct embeds and modal behavior
  • Add tests for profile creation via _handle_profile_details_submit and for invalid data producing a validation error embed
tests/capy_discord/exts/test_profile.py

Possibly linked issues

  • #service layer profile: PR implements the requested service layer by extracting profile business logic into _service and updating tests accordingly.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • Tests currently reach into cog.service._profiles to set up state; consider exposing a small public helper (e.g. set_profile or using save_profile) to avoid coupling tests to the service’s internal storage details.
  • ProfileService.start_edit takes action: str while finalize_profile uses Literal["create", "update"]; tightening start_edit to the same Literal (and possibly validating inputs centrally) would make the API safer and more self-documenting.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Tests currently reach into `cog.service._profiles` to set up state; consider exposing a small public helper (e.g. `set_profile` or using `save_profile`) to avoid coupling tests to the service’s internal storage details.
- `ProfileService.start_edit` takes `action: str` while `finalize_profile` uses `Literal["create", "update"]`; tightening `start_edit` to the same `Literal` (and possibly validating inputs centrally) would make the API safer and more self-documenting.

## Individual Comments

### Comment 1
<location path="tests/capy_discord/exts/test_profile.py" line_range="48-57" />
<code_context>
+
+
+@pytest.mark.asyncio
+async def test_profile_create_with_existing_profile_shows_error(cog, interaction):
+    cog.service._profiles[interaction.user.id] = UserProfileSchema(
+        preferred_name="Existing User",
+        student_id="123456789",
+        school_email="existing@school.edu",
+        graduation_year=2028,
+        major="CS",
+        minor="ITWS",
+        description="Already here",
+    )
+
+    await cog.profile.callback(cog, interaction, "create")
+
+    interaction.response.send_modal.assert_not_called()
+    interaction.response.send_message.assert_called_once()
+    embed = interaction.response.send_message.await_args.kwargs["embed"]
+    assert embed.title == "Profile Exists"
+
+
</code_context>
<issue_to_address>
**nitpick (testing):** Avoid reaching into the service's internal _profiles dict directly in tests

The tests seed state via `cog.service._profiles[...]`, which tightly couples them to `ProfileService`’s internal storage. To keep tests resilient to refactors, prefer seeding via a public method (e.g. `save_profile`) or by using the handler flow to create a profile where feasible. This keeps tests behaviour‑focused and avoids breakage if the storage implementation changes.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +48 to +57
async def test_profile_create_with_existing_profile_shows_error(cog, interaction):
cog.service._profiles[interaction.user.id] = UserProfileSchema(
preferred_name="Existing User",
student_id="123456789",
school_email="existing@school.edu",
graduation_year=2028,
major="CS",
minor="ITWS",
description="Already here",
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (testing): Avoid reaching into the service's internal _profiles dict directly in tests

The tests seed state via cog.service._profiles[...], which tightly couples them to ProfileService’s internal storage. To keep tests resilient to refactors, prefer seeding via a public method (e.g. save_profile) or by using the handler flow to create a profile where feasible. This keeps tests behaviour‑focused and avoids breakage if the storage implementation changes.

Copy link
Copy Markdown
Member

@shamikkarkhanis shamikkarkhanis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perfect 🔥

@shamikkarkhanis shamikkarkhanis merged commit f1cfe24 into develop Apr 7, 2026
4 checks passed
@shamikkarkhanis shamikkarkhanis deleted the feature/capr-59-service-layer-profile branch April 7, 2026 20:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants