fix(membership): show only the newest application per applicant #52

Merged
laoc merged 1 commit from fix/membership-latest-application into dev 2026-07-30 05:27:13 +00:00
Owner

Kind 1069 is a regular event, so re-submitting a membership application adds a copy rather than replacing the original. Both admin-facing surfaces counted and rendered raw events, so an applicant who edited their application appeared once per submission.

Found by TestOER while browser-testing dev @ 2f507ff5.

Why this is more than a cosmetic count

The badge double-count was the visible symptom. The sharper problem is in the approvals panel: approve() provisions decrypted.get(response.id).wished_handle — the handle from that row. With a superseded row still listed and still actionable, an admin approving the wrong card provisions the handle the applicant asked for before they edited it, and /api/nip05 has no reason to reject it.

The change

Both surfaces now go through a new selectAdminApplications() helper, which does the per-admin p-tag narrowing they already duplicated, plus a newest-per-applicant reduction. Ties on created_at break by event id so the choice does not depend on relay arrival order.

The reduction runs before the rejected-id filter: rejection means "reject this applicant", so rejecting the row the admin actually saw must not resurface an older submission from the same person. There is a test pinning that ordering.

Verification

  • Full suite: 5147 passed / 5147 (468 files), with .env exported so the publish-forms-build tests can read SCHEME_NADDR_*.
  • pnpm lint clean, pnpm check 0 errors (4 pre-existing warnings in test fixtures).
  • Negative control: disabling the dedupe inside the helper fails 5 of the new tests, including both badge-store cases. They test the fix, not the fixture.
  • Not browser-tested — reproducing it needs two submissions from one applicant against live relays.
Kind 1069 is a regular event, so re-submitting a membership application adds a copy rather than replacing the original. Both admin-facing surfaces counted and rendered raw events, so an applicant who edited their application appeared once per submission. Found by TestOER while browser-testing dev @ `2f507ff5`. ## Why this is more than a cosmetic count The badge double-count was the visible symptom. The sharper problem is in the approvals panel: `approve()` provisions `decrypted.get(response.id).wished_handle` — the handle from *that row*. With a superseded row still listed and still actionable, an admin approving the wrong card provisions the handle the applicant asked for **before** they edited it, and `/api/nip05` has no reason to reject it. ## The change Both surfaces now go through a new `selectAdminApplications()` helper, which does the per-admin p-tag narrowing they already duplicated, plus a newest-per-applicant reduction. Ties on `created_at` break by event id so the choice does not depend on relay arrival order. The reduction runs **before** the rejected-id filter: rejection means "reject this applicant", so rejecting the row the admin actually saw must not resurface an older submission from the same person. There is a test pinning that ordering. ## Verification - Full suite: **5147 passed / 5147** (468 files), with `.env` exported so the `publish-forms-build` tests can read `SCHEME_NADDR_*`. - `pnpm lint` clean, `pnpm check` **0 errors** (4 pre-existing warnings in test fixtures). - Negative control: disabling the dedupe inside the helper fails **5** of the new tests, including both badge-store cases. They test the fix, not the fixture. - Not browser-tested — reproducing it needs two submissions from one applicant against live relays.
Kind 1069 is a regular event, so re-submitting a membership application
adds a copy rather than replacing the original. Both admin-facing
surfaces counted and rendered raw events, so an applicant who edited
their application appeared once per submission.

The badge count was the visible symptom; the sharper problem is in the
approvals panel. `approve()` provisions `decrypted.get(response.id)
.wished_handle` — the handle from *that row*. With a superseded row
still listed and still actionable, an admin approving the wrong card
provisions the handle the applicant asked for before they edited it,
and `/api/nip05` has no reason to reject it.

Both surfaces now go through selectAdminApplications(), which does the
p-tag narrowing they already duplicated plus a newest-per-applicant
reduction, ties broken by event id so the choice does not depend on
relay arrival order. The reduction runs before the rejected-id filter:
rejection means "reject this applicant", so rejecting the row the admin
saw must not resurface an older submission from the same person.

Reported by TestOER against dev @ 2f507ff5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
laoc merged commit cc45a83590 into dev 2026-07-30 05:27:13 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
edufeed/edufeed-app!52
No description provided.