Skip to content

middleware/wordpress: verify merged-status tolerance across all consumers - #365

Closed
i12know wants to merge 2 commits into
mainfrom
claude/issue-309-audit
Closed

i12know wants to merge 2 commits into
mainfrom
claude/issue-309-audit

Conversation

@i12know

@i12know i12know commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

Track A2 of epic #307 — guardrail G4 of docs/CANONICAL_IDENTITY_RFC.mdcloses #309.

sf_participants.approval_status / sf_approvals.approval_status are plain VARCHAR(50), so the reconciliation command (#310) can tombstone a retired duplicate row with approval_status = 'merged' with zero schema changes. This PR is the audit #309 asks for: verify every consumer of approval_status tolerates that unfamiliar value before #310 starts writing it, and fix anywhere a consumer would crash or misrender.

Audit checklist

Safe as-is (exact-match or whitelist filters — verified with new regression tests where the logic lives in Python):

  • Badge generation (middleware/badges/runner.py) — double-filtered server- and client-side against {"approved"}, for both the bulk run and the targeted --chm-id path.
  • sync --type approvals (middleware/sync/manager.py) — exact != APPROVAL_STATUS["APPROVED"] skip (targeted) and a server-side approval_status=approved filter (bulk fetch).
  • church_teams_export.py's force-resend categorization (_handle_force_resend) — an explicit whitelist of pending/pending_approval/reapproval_required/validated.
  • [vaysf_churches] / [vaysf_badges] shortcodes and the admin dashboard stat tiles — exact approval_status = 'approved'/'denied' SQL matches.
  • process-token handling (class-vaysf-rest-approvals.php) — already rejects any non-'pending' approval, including merged, with a generic "already processed" error, so a stale pastor-approval email token can't resurrect a tombstoned row.
  • Validation-issue sync (sync/participants.py) — keyed by participant_id, never branches on approval_status; and once middleware: person alias map + resolution at sync choke point #308's alias resolution is live, normal sync never revisits a stale/merged chm_id in the first place.

Fixed — misrendered "Merged" as "Pending": the admin participants list, admin approvals list, and the [vaysf_participants] shortcode (found during this audit — shares the same status-class helper, not explicitly named in the issue but a real consumer) all fell through their switch/case status styling to the pending bucket for any unrecognized status. Added an explicit merged case (+ a .status-merged style) to get_status_class() in includes/shortcodes.php, vaysf_format_approval_status() in includes/functions.php, and the inline switches in admin/class-vaysf-admin-participants.php and admin/class-vaysf-admin-approvals.php.

Fixed — functional hazard, not just cosmetic: the admin Approvals page's "Resend Email" action (admin/class-vaysf-admin-approvals.phpvaysf_resend_approval_email()) unconditionally reset approval_status back to 'pending' and minted a fresh token/email for any row, including a merged one. Clicking Resend on a tombstoned duplicate would have undone the reconciler's tombstone (RFC §4.3 step 3 — "so an old email token can't resurrect it") and mailed the pastor a stale approval link. The button is now hidden for merged rows, and the action=resend handler independently rejects the request so a direct URL hit can't bypass the UI guard.

No plugin schema migration needed anywhere in this audit.

Test plan

  • 6 new mock tests: test_sync_manager.py (targeted + bulk approval sync skip merged), test_badges.py (bulk + targeted badge generation skip merged), test_church_teams_export.py (force-resend excludes merged, including a targeted --resend-chm-id hit).
  • Full mock suite (pytest tests/): 928 passed (922 pre-existing + 6 new), no regressions.
  • php -l on all 4 touched PHP files: no syntax errors. No PHPUnit harness exists yet for the plugin (tracked separately), so the PHP fixes are verified by lint plus manual review of the audited call paths.

🤖 Generated with Claude Code

https://claude.ai/code/session_0186mjKMbV8qy3jRQxNg83S3


Generated by Claude Code

…mers — closes #309

Track A2 of epic #307, guardrail G4 of docs/CANONICAL_IDENTITY_RFC.md: audit
every consumer of approval_status before #310's reconciler starts writing
'merged' rows.

Audited and confirmed safe (exact-match/whitelist filters): badge generation,
sync --type approvals, church_teams_export's force-resend categorization,
[vaysf_churches]/[vaysf_badges] shortcodes, admin dashboard stats,
process-token handling (already rejects any non-pending approval), and
validation-issue sync (never branches on approval_status).

Fixed two real issues found during the audit:
- Admin participants list, admin approvals list, and the [vaysf_participants]
  shortcode all fell through their status switch/case to the "pending"
  bucket for an unrecognized status, so a tombstoned duplicate would have
  displayed as awaiting action. Added an explicit 'merged' case + a
  .status-merged style everywhere the pattern appears.
- The admin Approvals page's "Resend Email" action unconditionally reset
  approval_status back to 'pending' and minted a fresh token/email for any
  row, including a merged one — undoing the reconciler's tombstone and
  resurrecting a retired duplicate. Now hidden for merged rows in the UI and
  independently rejected by the action handler.

No plugin schema change needed: approval_status is already a plain
VARCHAR(50), and the REST endpoints sanitize it with sanitize_text_field()
rather than validating against an enum.

6 new mock tests pin the middleware-side tolerance; touched PHP passes php -l.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186mjKMbV8qy3jRQxNg83S3
The resend guard added for #309 checked only the approval row's status. But
apply-aliases (#310) tombstones the participant *before* its approval rows,
so a partially-failed reconciliation leaves the participant 'merged' while
its approval is still 'pending' — the approval-only guard would happily
resend exactly that row, reset it to 'pending', and mail a fresh pastor
token for a retired duplicate identity.

Both the list query and the resend lookup now select p.approval_status, and
both the "Resend Email" button and the action=resend handler treat a merged
participant as blocking, independent of the approval row's own status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186mjKMbV8qy3jRQxNg83S3

i12know commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #367, which implements #308#310 as a single coherent change. Closing unmerged — the work continues on agent/canonical-identity-rewrite.

The merged-status audit from this PR is carried forward in #367, including the resend guard. One item from this PR remains open there: vaysf_format_approval_status() still has no callers, so the merged case added to it is not exercised by any code path.


Generated by Claude Code

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.

middleware/wordpress: verify merged-status tolerance across all consumers

2 participants