Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 25 additions & 34 deletions apps/worker/src/five08/worker/crm/resume_profile_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,6 @@ def extract_profile_proposal(
)
)

# Track extraction completion before user confirmation/apply step.
self._mark_resume_processed(contact_id)
self._record_processing_run(
contact_id=contact_id,
attachment_id=attachment_id,
Expand Down Expand Up @@ -501,18 +499,18 @@ def apply_profile_updates(
"cWebsiteLink",
"cSocialLinks",
}
sanitized_updates: dict[str, Any] = {
approved_updates: dict[str, Any] = {
field: value
for field, value in normalized_updates.items()
if field in allowed_fields and value
}
parsed_skills_for_apply = self._normalize_skills_for_apply(
sanitized_updates.get("skills")
approved_updates.get("skills")
)
if parsed_skills_for_apply is not None:
sanitized_updates["skills"] = parsed_skills_for_apply
approved_updates["skills"] = parsed_skills_for_apply

if not sanitized_updates:
if not approved_updates:
return ResumeApplyResult(
contact_id=contact_id,
updated_fields=[],
Expand All @@ -526,13 +524,13 @@ def apply_profile_updates(
discord_user_id = str(link_discord.get("user_id", "")).strip()
discord_username = str(link_discord.get("username", "")).strip()
if discord_user_id and discord_username:
sanitized_updates["cDiscordUserID"] = discord_user_id
sanitized_updates["cDiscordUsername"] = (
approved_updates["cDiscordUserID"] = discord_user_id
approved_updates["cDiscordUsername"] = (
f"{discord_username} (ID: {discord_user_id})"
)
link_applied = True

if not sanitized_updates:
if not approved_updates:
return ResumeApplyResult(
contact_id=contact_id,
updated_fields=[],
Expand All @@ -541,22 +539,28 @@ def apply_profile_updates(
error="No valid profile fields provided",
)

# NOTE: cResumeLastProcessed is stored as UTC for CRM compatibility.
crm_update_payload = dict(approved_updates)
crm_update_payload["cResumeLastProcessed"] = datetime.now(
tz=timezone.utc
).strftime("%Y-%m-%d %H:%M:%S")

try:
self.crm.update_contact(contact_id, sanitized_updates)
self.crm.update_contact(contact_id, crm_update_payload)
verified_fields = self._verify_updated_fields(
Comment on lines 548 to 550

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

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

The apply flow now builds update_payload with additional internal fields (e.g., cResumeLastProcessed) while sanitized_updates represents user-approved fields. Consider making that separation explicit (e.g., naming like approved_updates vs crm_update_payload) to reduce confusion about which dict is used for verification vs CRM writes.

Copilot uses AI. Check for mistakes.
contact_id=contact_id,
baseline_contact=pre_update_contact,
candidate_fields=list(sanitized_updates.keys()),
candidate_fields=list(approved_updates.keys()),
)
if verified_fields is None:
verified_fields = sorted(sanitized_updates.keys())
verified_fields = sorted(approved_updates.keys())
return ResumeApplyResult(
Comment on lines 548 to 557

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

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

cResumeLastProcessed is added to the CRM update payload, but verification and success calculation only consider approved_updates.keys(). If _verify_updated_fields returns an empty list (e.g., no user-facing fields changed), this can report success=False even though the timestamp field was updated. Consider either (a) including cResumeLastProcessed in the verified candidate fields/result fields, or (b) only adding the timestamp when at least one other field actually changed.

Copilot uses AI. Check for mistakes.
contact_id=contact_id,
updated_fields=verified_fields,
updated_values={
field: sanitized_updates[field]
field: approved_updates[field]
for field in verified_fields
if field in sanitized_updates
if field in approved_updates
},
link_discord_applied=link_applied,
success=bool(verified_fields),
Expand All @@ -571,7 +575,7 @@ def apply_profile_updates(

updated_fields: list[str] = []
batch_errors: list[str] = []
for field, value in sanitized_updates.items():
for field, value in approved_updates.items():
try:
self.crm.update_contact(contact_id, {field: value})
updated_fields.append(field)
Comment on lines 576 to 581

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

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

In the EspoAPIError fallback path (batch update fails), cResumeLastProcessed is never written because the per-field loop iterates over approved_updates only. This defeats the goal of tracking resume processing time for any apply that has to fall back to individual updates; include the timestamp update in the fallback path as well (e.g., add it to the set of fields being updated individually or perform a final best-effort update after the loop).

Copilot uses AI. Check for mistakes.
Expand All @@ -589,14 +593,13 @@ def apply_profile_updates(
if verified_fields is not None:
updated_fields = verified_fields

if len(updated_fields) == len(sanitized_updates):
return ResumeApplyResult(
contact_id=contact_id,
updated_fields=sorted(updated_fields),
updated_values={
field: sanitized_updates[field]
field: approved_updates[field]
for field in sorted(updated_fields)
if field in sanitized_updates
if field in approved_updates
},
link_discord_applied=link_applied,
success=True,
Expand All @@ -607,9 +610,9 @@ def apply_profile_updates(
contact_id=contact_id,
updated_fields=sorted(updated_fields),
updated_values={
field: sanitized_updates[field]
field: approved_updates[field]
for field in sorted(updated_fields)
if field in sanitized_updates
if field in approved_updates
},
link_discord_applied=link_applied,
success=False,
Expand All @@ -620,8 +623,8 @@ def apply_profile_updates(

return ResumeApplyResult(
contact_id=contact_id,
updated_fields=sorted(sanitized_updates.keys()),
updated_values=dict(sanitized_updates),
updated_fields=sorted(approved_updates.keys()),
updated_values=dict(approved_updates),
link_discord_applied=link_applied,
success=False,
error="; ".join(batch_errors)
Expand Down Expand Up @@ -1236,18 +1239,6 @@ def _build_email_address_data(

return list(merged.values())

def _mark_resume_processed(self, contact_id: str) -> None:
"""Best-effort update for extraction completion tracking."""
processed_at = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
try:
self.crm.update_contact(contact_id, {"cResumeLastProcessed": processed_at})
except Exception as exc:
logger.warning(
"Failed to update cResumeLastProcessed contact_id=%s error=%s",
contact_id,
exc,
)

def _configured_model_name(self) -> str:
"""Model identity used for idempotency/ledger keys."""
if settings.openai_api_key:
Expand Down
17 changes: 7 additions & 10 deletions tests/unit/test_resume_profile_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,7 @@ def test_extract_profile_proposal_filters_508_email() -> None:
json.loads(result.proposed_updates["cSkillAttrs"])["fastapi"]["strength"] == 4
)
assert any(item.field == "emailAddress" for item in result.skipped)
processor.crm.update_contact.assert_called_once()
update_contact_payload = processor.crm.update_contact.call_args.args[1]
assert "cResumeLastProcessed" in update_contact_payload
assert isinstance(update_contact_payload["cResumeLastProcessed"], str)
assert (
datetime.strptime(
update_contact_payload["cResumeLastProcessed"], "%Y-%m-%d %H:%M:%S"
)
is not None
)
processor.crm.update_contact.assert_not_called()
processor._record_processing_run.assert_called_once()
record_kwargs = processor._record_processing_run.call_args.kwargs
assert record_kwargs["status"] == "succeeded"
Expand Down Expand Up @@ -818,6 +809,12 @@ def test_apply_profile_updates_adds_discord_and_filters_email() -> None:
assert update_payload["skills"] == ["python", "fastapi"]
assert update_payload["cDiscordUserID"] == "123"
assert update_payload["cDiscordUsername"] == "member#0001 (ID: 123)"
assert "cResumeLastProcessed" in update_payload
assert isinstance(update_payload["cResumeLastProcessed"], str)
assert (
datetime.strptime(update_payload["cResumeLastProcessed"], "%Y-%m-%d %H:%M:%S")
is not None
)


def test_apply_profile_updates_normalizes_csv_skills_to_array() -> None:
Expand Down