diff --git a/apps/discord_bot/src/five08/discord_bot/cogs/crm.py b/apps/discord_bot/src/five08/discord_bot/cogs/crm.py index 40a8c73..857c7a0 100644 --- a/apps/discord_bot/src/five08/discord_bot/cogs/crm.py +++ b/apps/discord_bot/src/five08/discord_bot/cogs/crm.py @@ -1709,6 +1709,78 @@ def _format_applied_updates_value(cls, applied_lines: list[str]) -> str: joined = "\n".join(kept) return cls._truncate_embed_field(joined, cls._APPLIED_FIELD_TOTAL_LIMIT) + @staticmethod + def _format_discord_link_value( + *, + link_discord: dict[str, str] | None = None, + link_member: discord.Member | None = None, + ) -> str | None: + user_id = "" + username = "" + + if link_member is not None: + user_id = str(link_member.id) + username = str(link_member).strip() + elif link_discord: + user_id = str(link_discord.get("user_id") or "").strip() + username = str(link_discord.get("username") or "").strip() + + if not user_id.isdigit(): + return None + + mention = f"<@{user_id}>" + safe_username = discord.utils.escape_markdown( + discord.utils.escape_mentions(username) + ) + return f"{mention} ({safe_username})" if username else mention + + def _build_apply_success_embed( + self, + *, + updated_fields: list[str], + updated_values: dict[str, Any], + link_discord_applied: bool | None, + ) -> discord.Embed: + embed = discord.Embed( + title="✅ CRM Updated", + description=f"Applied updates for **{self.contact_name}**.", + color=0x00FF00, + ) + display_fields = self._collapse_updated_fields(updated_fields) + labeled_fields = [self._field_label(field) for field in display_fields] + updated_fields_value = self._format_updated_fields_value(labeled_fields) + embed.add_field( + name="Updated Fields", + value=updated_fields_value, + inline=False, + ) + applied_lines = self._build_applied_updates_lines( + updated_fields=updated_fields, + updated_values=updated_values, + ) + if applied_lines: + applied_updates_value = self._format_applied_updates_value(applied_lines) + embed.add_field( + name="Applied Updates", + value=applied_updates_value, + inline=False, + ) + + if link_discord_applied: + discord_link_value = self._format_discord_link_value( + link_discord=self.link_discord + ) + if discord_link_value: + embed.add_field( + name="Discord Link", + value=f"Linked contact to {discord_link_value}", + inline=False, + ) + + profile_url = f"{self.crm_cog.base_url}/#Contact/view/{self.contact_id}" + embed.add_field(name="🔗 CRM Profile", value=f"[View in CRM]({profile_url})") + return embed + @classmethod def _collapse_updated_fields(cls, updated_fields: list[str]) -> list[str]: """Collapse skill fields into a single logical skills entry.""" @@ -2027,32 +2099,11 @@ def _audit_apply_event(result: str, metadata: dict[str, Any]) -> None: ) return - embed = discord.Embed( - title="✅ CRM Updated", - description=f"Applied updates for **{self.contact_name}**.", - color=0x00FF00, - ) - display_fields = self._collapse_updated_fields(updated_fields) - labeled_fields = [self._field_label(field) for field in display_fields] - updated_fields_value = self._format_updated_fields_value(labeled_fields) - embed.add_field( - name="Updated Fields", - value=updated_fields_value, - inline=False, - ) - applied_lines = self._build_applied_updates_lines( + embed = self._build_apply_success_embed( updated_fields=updated_fields, updated_values=updated_values, + link_discord_applied=link_discord_applied, ) - if applied_lines: - applied_updates_value = self._format_applied_updates_value(applied_lines) - embed.add_field( - name="Applied Updates", - value=applied_updates_value, - inline=False, - ) - profile_url = f"{self.crm_cog.base_url}/#Contact/view/{self.contact_id}" - embed.add_field(name="🔗 CRM Profile", value=f"[View in CRM]({profile_url})") _audit_apply_event( "success", { @@ -3194,11 +3245,14 @@ def format_skill_delta(current: Any, proposed: Any) -> str: inline=True, ) - if link_member: + discord_link_value = ResumeUpdateConfirmationView._format_discord_link_value( + link_member=link_member + ) + if discord_link_value: embed.add_field( name="Discord Link", value=truncate_field_value( - f"Will link contact to {link_member.mention}" + f"Will link contact to {discord_link_value}" ), inline=False, ) diff --git a/tests/unit/test_crm.py b/tests/unit/test_crm.py index d948f34..61edff7 100644 --- a/tests/unit/test_crm.py +++ b/tests/unit/test_crm.py @@ -251,6 +251,43 @@ async def test_resume_apply_confirmation_maps_skill_attrs_only_to_skills( assert collapsed == ["skills", "phoneNumber"] + @pytest.mark.asyncio + async def test_resume_apply_success_embed_includes_discord_link_mention( + self, crm_cog + ): + view = ResumeUpdateConfirmationView( + crm_cog=crm_cog, + requester_id=123, + contact_id="contact-1", + contact_name="Test User", + proposed_updates={}, + link_discord={"user_id": "123456789", "username": "test-user"}, + ) + + embed = view._build_apply_success_embed( + updated_fields=["skills"], + updated_values={"skills": ["python"]}, + link_discord_applied=True, + ) + + discord_link_field = next( + field for field in embed.fields if field.name == "Discord Link" + ) + assert "<@123456789>" in discord_link_field.value + assert "test-user" in discord_link_field.value + + def test_format_discord_link_value_escapes_username_mentions_and_markdown(self): + username = "@everyone **ops**" + expected_username = discord.utils.escape_markdown( + discord.utils.escape_mentions(username) + ) + + value = ResumeUpdateConfirmationView._format_discord_link_value( + link_discord={"user_id": "123456789", "username": username} + ) + + assert value == f"<@123456789> ({expected_username})" + @pytest.mark.asyncio async def test_resume_apply_confirmation_groups_location_fields(self, crm_cog): """Applied updates should render location fields as one combined line.""" @@ -1049,6 +1086,24 @@ def test_resume_preview_embed_includes_debug_field(self, crm_cog): assert "current role" in evidence_field.value assert "developer profile" in evidence_field.value + def test_resume_preview_embed_includes_discord_link_mention(self, crm_cog): + link_member = Mock() + link_member.id = 123456789 + link_member.__str__ = Mock(return_value="resume-user") + + embed, _ = crm_cog._build_resume_preview_embed( + contact_id="contact-1", + contact_name="Test User", + result={"proposed_changes": []}, + link_member=link_member, + ) + + discord_link_field = next( + field for field in embed.fields if field.name == "Discord Link" + ) + assert "<@123456789>" in discord_link_field.value + assert "resume-user" in discord_link_field.value + def test_build_resume_extract_debug_file_serializes_raw_payload(self, crm_cog): """The debug attachment should include raw and normalized extraction payloads.""" debug_file = crm_cog._build_resume_extract_debug_file(