refactor: simplify response reconciliation

This commit is contained in:
2026-07-21 12:04:48 +02:00
parent 4ddac65367
commit 98b797a95d

View File

@@ -787,16 +787,9 @@ def list_visible_scheduling_notifications(
return visible
def refresh_participant_response_state(
session: Session,
*,
request: SchedulingRequest,
actor_ids: tuple[str, ...] = (),
reset_missing: bool = False,
) -> None:
if request.poll_id is None:
return
participants = _active_participants(request)
def _participant_response_indexes(
participants: list[SchedulingParticipant],
) -> tuple[dict[str, SchedulingParticipant], dict[str, SchedulingParticipant]]:
by_invitation_id = {
participant.poll_invitation_id: participant
for participant in participants
@@ -808,6 +801,97 @@ def refresh_participant_response_state(
for identity in (participant.respondent_id, stored_identity):
if isinstance(identity, str) and identity:
by_respondent_id[identity] = participant
return by_invitation_id, by_respondent_id
def _response_participant(
response: PollResponseRef,
*,
by_invitation_id: dict[str, SchedulingParticipant],
by_respondent_id: dict[str, SchedulingParticipant],
actor_ids: set[str],
actor_participants: list[SchedulingParticipant],
) -> SchedulingParticipant | None:
participant = by_invitation_id.get(response.invitation_id)
if participant is None and response.respondent_id is not None:
participant = by_respondent_id.get(response.respondent_id)
# Poll deliberately ignores client-supplied invitation identifiers on
# authenticated submissions. While reconciling for that same actor, map
# the server-authenticated respondent to their unique Scheduling row.
if participant is None and response.respondent_id in actor_ids and len(actor_participants) == 1:
return actor_participants[0]
return participant
def _apply_participant_response(
session: Session,
*,
request: SchedulingRequest,
participant: SchedulingParticipant,
response: PollResponseRef,
) -> bool:
changed = False
if response.respondent_id:
metadata = participant.metadata_ or {}
if metadata.get("poll_response_respondent_id") != response.respondent_id:
participant.metadata_ = {
**metadata,
"poll_response_respondent_id": response.respondent_id,
}
changed = True
if participant.status == "responded":
return changed
participant.status = "responded"
participant.responded_at = response_datetime(response.submitted_at)
_emit_scheduling_center_notification(
session,
request=request,
participant=participant,
event_kind="scheduling.participant_responded",
subject=f"Scheduling response: {request.title}",
body_text=f"{participant.display_name or participant.email or 'A participant'} responded to {request.title}.",
)
return True
def _reset_missing_participant_responses(
participants: list[SchedulingParticipant],
*,
matched_participant_ids: set[str],
unmatched_response_count: int,
) -> bool:
changed = False
for participant in participants:
if participant.status != "responded" or participant.id in matched_participant_ids:
continue
stored_identity = (participant.metadata_ or {}).get("poll_response_respondent_id")
if not isinstance(stored_identity, str) and unmatched_response_count:
# Preserve an older projection while an active response exists
# that cannot yet be mapped safely to a participant.
continue
participant.status = "invited" if participant.poll_invitation_id else "draft"
participant.responded_at = None
if isinstance(stored_identity, str):
participant.metadata_ = {
key: value
for key, value in (participant.metadata_ or {}).items()
if key != "poll_response_respondent_id"
}
changed = True
return changed
def refresh_participant_response_state(
session: Session,
*,
request: SchedulingRequest,
actor_ids: tuple[str, ...] = (),
reset_missing: bool = False,
) -> None:
if request.poll_id is None:
return
participants = _active_participants(request)
by_invitation_id, by_respondent_id = _participant_response_indexes(participants)
try:
responses = _poll_provider().list_responses(
session,
@@ -826,60 +910,28 @@ def refresh_participant_response_state(
matched_participant_ids: set[str] = set()
unmatched_response_count = 0
for response in responses:
participant = by_invitation_id.get(response.invitation_id)
if participant is None and response.respondent_id is not None:
participant = by_respondent_id.get(response.respondent_id)
# Poll deliberately ignores client-supplied invitation identifiers on
# authenticated submissions. While reconciling for that same actor,
# map the server-authenticated respondent id to their unique Scheduling
# participant row (which may have been invited by email only).
if (
participant is None
and response.respondent_id in ids
and len(actor_participants) == 1
):
participant = actor_participants[0]
participant = _response_participant(
response,
by_invitation_id=by_invitation_id,
by_respondent_id=by_respondent_id,
actor_ids=ids,
actor_participants=actor_participants,
)
if participant is None:
unmatched_response_count += 1
continue
matched_participant_ids.add(participant.id)
if response.respondent_id:
metadata = participant.metadata_ or {}
if metadata.get("poll_response_respondent_id") != response.respondent_id:
participant.metadata_ = {
**metadata,
"poll_response_respondent_id": response.respondent_id,
}
changed = True
if participant.status != "responded":
participant.status = "responded"
participant.responded_at = response_datetime(response.submitted_at)
_emit_scheduling_center_notification(
changed = _apply_participant_response(
session,
request=request,
participant=participant,
event_kind="scheduling.participant_responded",
subject=f"Scheduling response: {request.title}",
body_text=f"{participant.display_name or participant.email or 'A participant'} responded to {request.title}.",
)
changed = True
if reset_missing:
for participant in participants:
if participant.status != "responded" or participant.id in matched_participant_ids:
continue
stored_identity = (participant.metadata_ or {}).get("poll_response_respondent_id")
if not isinstance(stored_identity, str) and unmatched_response_count:
# Do not reset an older projection while an active response
# exists that cannot yet be mapped safely to a participant.
continue
participant.status = "invited" if participant.poll_invitation_id else "draft"
participant.responded_at = None
if isinstance(stored_identity, str):
participant.metadata_ = {
key: value
for key, value in (participant.metadata_ or {}).items()
if key != "poll_response_respondent_id"
}
response=response,
) or changed
if reset_missing and _reset_missing_participant_responses(
participants,
matched_participant_ids=matched_participant_ids,
unmatched_response_count=unmatched_response_count,
):
changed = True
if changed:
session.flush()