Skip to content

Add per-season camp membership (#488)#228

Open
peterdrier wants to merge 2 commits intomainfrom
sprint/20260415/batch-15
Open

Add per-season camp membership (#488)#228
peterdrier wants to merge 2 commits intomainfrom
sprint/20260415/batch-15

Conversation

@peterdrier
Copy link
Copy Markdown
Owner

Summary

Introduces CampMember — a post-hoc per-season membership record so humans can tell Humans which camp they joined this year. Humans does not manage a camp's real membership; each camp runs its own process (website, spreadsheet, WhatsApp). This record enables per-camp roles, Early Entry allocations, and future notification flows.

  • New CampMember entity (Pending/Active/Removed) owned by CampService, with a partial unique index on (CampSeasonId, UserId) WHERE Status <> 'Removed' so removed rows retain audit history and allow re-requesting.
  • Camp detail page shows a clearly-worded "Request to join" flow for authenticated humans. Copy makes explicit that this does NOT join you to the camp — do that through the camp's own process first. Button is disabled when no open season exists; membership state is never rendered on anonymous or public views.
  • Camp edit page shows pending requests (Approve/Reject) and active members (Remove) to leads / CampAdmin / Admin via the existing CampAuthorizationHandler.
  • Approving or rejecting a request sends an in-app notification to the requester (CampMembershipApproved / CampMembershipRejected, mapped to MessageCategory.TeamUpdates).
  • Rejecting or withdrawing a camp season auto-withdraws its pending membership requests.
  • Profile page gets a MyCamps view component grouping a human's memberships by year with links to each camp.

Closes nobodies-collective#488

Test plan

  • Build clean (dotnet build -v q -clp:ErrorsOnly)
  • Format clean (dotnet format --verify-no-changes)
  • Application tests pass (962/962 including 13 new CampMember tests covering request, approve, reject, withdraw, leave, remove, re-request after removal, auto-withdraw-on-season-change, membership state lookup, user-cannot-withdraw-others)
  • EF migration reviewer: no HasDefaultValue(false) traps, no hand edits, Humans.Infrastructure.Migrations namespace, snapshot updated, partial unique index correct.
  • Manual smoke on QA: request → approve → notification → human sees badge on profile; try season withdraw to verify auto-cleanup of pending rows.

Integration tests (Humans.Integration.Tests) require Docker/Testcontainers and are not runnable in this sandbox; unaffected by this change.

🤖 Generated with Claude Code

peterdrier and others added 2 commits April 15, 2026 03:46
Promote QA batch: service ownership, dashboard cluster, rota partials
Introduces a post-hoc CampMember record so humans can tell Humans which
camp they have joined for the current season. Humans do not manage a
camp's real membership here — each camp runs its own process. This
record enables per-camp roles, Early Entry allocations, and future
notification flows.

- CampMember entity (Pending/Active/Removed) with partial unique index
  on (CampSeasonId, UserId) WHERE Status <> 'Removed' to allow
  re-requesting after removal.
- CampService owns camp_members; new methods cover request, approve,
  reject, withdraw, leave, remove, list, and per-user grouping.
- Camp detail page shows a clearly-worded "Request to join" flow for
  authenticated humans; the button is disabled when no open season
  exists, and membership state (Pending/Active) is never rendered on
  anonymous or public views.
- Camp edit page shows pending requests (Approve/Reject) and active
  members (Remove) to leads/CampAdmin/Admin via the existing
  CampAuthorizationHandler.
- Approving or rejecting a request sends an in-app notification to the
  requester via the existing NotificationService, mapped to
  MessageCategory.TeamUpdates.
- Rejecting or withdrawing a camp season auto-withdraws its pending
  member requests.
- Profile page includes a MyCamps view component grouping memberships
  by year so humans can see what they belong to without hunting.
- Migration reviewer gate: no bool sentinel traps, no hand edits,
  snapshot updated, namespace Humans.Infrastructure.Migrations.

Closes nobodies-collective#488

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coolify-nuc-humans
Copy link
Copy Markdown

coolify-nuc-humans bot commented Apr 15, 2026

The preview deployment for humans-qa is ready. 🟢

Open Preview | Open Build Logs | Open Application Logs

Last updated at: 2026-04-15 02:28:43 CET

@github-actions github-actions bot added the db PR includes EF Core migration label Apr 15, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 17b8ddfc82

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


try
{
await _campService.ApproveCampMemberAsync(campMemberId, user.Id);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Scope membership mutations to the authorized camp

This action authorizes the caller against the camp identified by slug, but then mutates membership state using only campMemberId. Because the service methods also load by membership ID alone, a lead of camp A can submit a crafted member ID from camp B and approve/reject/remove another camp’s membership record. Validate that the target membership belongs to camp.Id before mutating (or require campId in the service API and enforce it there).

Useful? React with 👍 / 👎.

Comment on lines +1338 to +1340
var existing = await _dbContext.CampMembers
.Where(m => m.CampSeasonId == season.Id && m.UserId == userId && m.Status != CampMemberStatus.Removed)
.FirstOrDefaultAsync(cancellationToken);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle duplicate-key races in membership requests

The request flow is a non-atomic read-then-insert (FirstOrDefaultAsync followed by Add/SaveChangesAsync), so concurrent submissions from the same user/season can both pass the existence check and one will hit the unique index (IX_camp_members_active_unique). That exception is not translated to an idempotent result, and the controller catches only InvalidOperationException, so this can surface as a 500 instead of AlreadyPending.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

db PR includes EF Core migration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add camp membership per season — humans request, leads confirm

1 participant