diff --git a/docs/features/20-camps.md b/docs/features/20-camps.md
index 1a6595f3..2ea7f4f0 100644
--- a/docs/features/20-camps.md
+++ b/docs/features/20-camps.md
@@ -121,6 +121,25 @@ Nobodies Collective organizes camping areas ("barrios") at Nowhere and related e
- `GET /api/camps/{year}/placement` returns placement-relevant data (space, sound zone, containers, electrical)
- Both endpoints are public (no authentication required)
+### US-20.12: Per-season Camp Membership (issue #488)
+**As an** authenticated human
+**I want to** tell Humans which camp I have joined for this year
+**So that** the system can assign me camp-specific roles and early-entry slots
+
+**Important framing:** Humans does NOT manage a camp's real membership. Each camp runs its own process (website, spreadsheet, WhatsApp). A `CampMember` record is a **post-hoc** representation — humans join through the camp's own channel first, then come to Humans to record the relationship. The UI must make this clear.
+
+**Acceptance Criteria:**
+- On `/Camps/{slug}`, authenticated humans see a "Request to join for {year}" button when the camp has an Active or Full season for the current public year. Tooltip clarifies that this doesn't join you to the camp — do that through the camp's own process first.
+- When no eligible open season exists the button is disabled with "Camp not open for membership this year".
+- Clicking creates a `CampMember` row with status `Pending` and the button changes to "Request pending".
+- Camp leads and CampAdmin/Admin see a "Humans in this camp" section on `/Camps/{slug}/Edit` listing pending requests (Approve / Reject) and active members (Remove).
+- On Approve: status → `Active`, `ConfirmedAt`/`ConfirmedByUserId` set, the requester receives an in-app notification (`CampMembershipApproved`).
+- On Reject: status → `Removed`, requester is notified (`CampMembershipRejected`). Removed rows are preserved for audit and allow re-requesting later.
+- A human can withdraw their own pending request and leave their own active membership from the camp detail page.
+- When a season transitions to `Rejected` or `Withdrawn`, any pending membership rows for that season are auto-withdrawn (status → Removed).
+- The member list is **private** — never rendered on anonymous or public views, only to camp leads, CampAdmin, Admin, and the human themselves.
+- Humans see their own camps, grouped by year (Active + Pending), on their profile page via the `MyCamps` view component.
+
### US-20.11: Export Camps CSV
**As a** CampAdmin or Admin
**I want to** export camp data as CSV
@@ -208,10 +227,12 @@ CampSettings
### Supporting Entities
- **CampHistoricalName**: Id, CampId, Name, Year (int?), Source (CampNameSource), CreatedAt
- **CampImage**: Id, CampId, FileName, StoragePath, ContentType, SortOrder, UploadedAt
+- **CampMember**: Id, CampSeasonId, UserId, Status (CampMemberStatus), RequestedAt, ConfirmedAt?, ConfirmedByUserId?, RemovedAt?, RemovedByUserId?. Unique index on `(CampSeasonId, UserId) WHERE Status <> 'Removed'` — one active/pending row per season; removed rows allow re-requesting.
### Enums
```
CampSeasonStatus: Pending(0), Active(1), Full(2), Rejected(4), Withdrawn(5)
+CampMemberStatus: Pending(0), Active(1), Removed(2)
CampLeadRole: Primary(0), CoLead(1)
CampVibe: Adult(0), ChillOut(1), ElectronicMusic(2), Games(3), Queer(4), Sober(5), Lecture(6), LiveMusic(7), Wellness(8), Workshop(9)
CampNameSource: Manual(0), NameChange(1)
@@ -310,6 +331,11 @@ Transitions:
| Set name lock date | CampAdmin or Admin |
| Delete camp | Admin only |
| JSON API | Public (AllowAnonymous) |
+| Request camp membership | Authenticated |
+| Withdraw own membership request | Authenticated (owner only) |
+| Leave own camp membership | Authenticated (owner only) |
+| Approve / Reject / Remove camp member | Camp Lead, CampAdmin, or Admin |
+| View camp member list | Camp Lead, CampAdmin, or Admin (never public) |
## URL Structure
@@ -337,6 +363,12 @@ Transitions:
| `POST /Camps/Admin/SetPublicYear` | Set public year |
| `POST /Camps/Admin/SetNameLockDate` | Set name lock date |
| `POST /Camps/Admin/Delete/{campId}` | Delete camp |
+| `POST /Camps/{slug}/Members/Request` | Request to join camp (authenticated human) |
+| `POST /Camps/{slug}/Members/Withdraw/{campMemberId}` | Withdraw own pending request |
+| `POST /Camps/{slug}/Members/Leave/{campMemberId}` | Leave own active membership |
+| `POST /Camps/{slug}/Members/Approve/{campMemberId}` | Approve pending request (lead/CampAdmin) |
+| `POST /Camps/{slug}/Members/Reject/{campMemberId}` | Reject pending request (lead/CampAdmin) |
+| `POST /Camps/{slug}/Members/Remove/{campMemberId}` | Remove active member (lead/CampAdmin) |
| `GET /api/camps/{year}` | JSON API: camps for year |
| `GET /api/camps/{year}/placement` | JSON API: placement data |
diff --git a/docs/sections/Camps.md b/docs/sections/Camps.md
index bfff3db1..c7a34cc1 100644
--- a/docs/sections/Camps.md
+++ b/docs/sections/Camps.md
@@ -5,6 +5,7 @@
- A **Camp** (also called "Barrio") is a themed community camp. Each camp has a unique URL slug, one or more leads, and optional images.
- A **Camp Season** is a per-year registration for a camp, containing the year-specific name, description, community info, and placement details.
- A **Camp Lead** is a human responsible for managing a camp. Leads have a role: Primary or CoLead.
+- A **Camp Member** is a human who belongs to a camp for a specific season. Membership is **post-hoc** — humans join through the camp's own process (website, spreadsheet, WhatsApp) first, then tell Humans about it. Per-season, not per-camp; no carry-forward across years. Private: never rendered on anonymous or public views.
- **Camp Settings** is a singleton controlling which year is public (shown in the directory) and which seasons accept new registrations.
## Actors & Roles
@@ -12,8 +13,8 @@
| Actor | Capabilities |
|-------|-------------|
| Anyone (including anonymous) | Browse the camps directory, view camp details and season details |
-| Any authenticated human | Register a new camp (which creates a new season in Pending status) |
-| Camp lead | Edit their camp's details, manage season registrations, manage co-leads, upload/manage images, manage historical names |
+| Any authenticated human | Register a new camp (which creates a new season in Pending status). Request to join a camp for the open season. Withdraw own pending request. Leave own active membership. |
+| Camp lead | Edit their camp's details, manage season registrations, manage co-leads, upload/manage images, manage historical names. Approve/reject pending member requests. Remove active members. View the camp member list. |
| CampAdmin, Admin | All camp lead capabilities on all camps. Approve/reject season registrations. Manage camp settings (public year, open seasons, name lock dates). View withdrawn and rejected seasons. Export camp data |
| Admin | Delete camps |
@@ -25,18 +26,24 @@
- Camp images are stored on disk; metadata and display order are tracked per camp.
- Historical names are recorded when a camp is renamed.
- Camp settings control which year is shown publicly and which seasons accept registrations.
+- Camp membership is per-season. A human can only have one active or pending `CampMember` row per season (enforced by a partial unique index on `(CampSeasonId, UserId) WHERE Status <> 'Removed'`). Removed rows are retained for audit and allow re-requesting.
+- A camp is "open for membership" for the current public year only when its season for that year is Active or Full. Pending/Rejected/Withdrawn seasons do not accept membership requests.
+- Camp member lists are never rendered on anonymous or public views.
## Negative Access Rules
- Regular humans **cannot** edit camps they do not lead.
- Camp leads **cannot** approve or reject season registrations — that requires CampAdmin or Admin.
- CampAdmin **cannot** delete camps. Only Admin can delete a camp.
-- Anonymous visitors **cannot** register camps or edit any camp data.
+- Anonymous visitors **cannot** register camps, edit any camp data, request to join a camp, or see a camp's member list.
+- A human **cannot** withdraw another human's pending membership request or leave another human's active membership.
## Triggers
- When a camp is registered, its initial season is created with Pending status.
- Season approval or rejection is performed by CampAdmin.
+- When a camp season transitions to `Rejected` or `Withdrawn`, all pending `CampMember` rows for that season are auto-withdrawn (status → Removed).
+- When a camp membership request is approved or rejected, the requester receives an in-app notification (`CampMembershipApproved` / `CampMembershipRejected`).
## Cross-Section Dependencies
@@ -48,7 +55,7 @@
See `.claude/DESIGN_RULES.md` for the full rules.
**Owning services:** `CampService`, `CampContactService`
-**Owned tables:** `camps`, `camp_seasons`, `camp_leads`, `camp_images`, `camp_historical_names`, `camp_settings`
+**Owned tables:** `camps`, `camp_seasons`, `camp_leads`, `camp_members`, `camp_images`, `camp_historical_names`, `camp_settings`
### Current Violations
diff --git a/src/Humans.Application/Interfaces/ICampService.cs b/src/Humans.Application/Interfaces/ICampService.cs
index d9ea96d2..5e4f33a2 100644
--- a/src/Humans.Application/Interfaces/ICampService.cs
+++ b/src/Humans.Application/Interfaces/ICampService.cs
@@ -99,6 +99,67 @@ Task UpdateCampAsync(Guid campId, string contactEmail, string contactPhone,
// Name change (handles historical name logging)
Task ChangeSeasonNameAsync(Guid seasonId, string newName, CancellationToken cancellationToken = default);
+
+ // ======================================================================
+ // Camp membership per season
+ // ======================================================================
+
+ ///
+ /// Human requests to join a camp for the currently-open season.
+ /// Creates a in Pending status. No-op if an active or pending
+ /// record already exists for the human in that season.
+ ///
+ Task RequestCampMembershipAsync(
+ Guid campId, Guid userId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Approve a pending camp membership request. Sets status to Active.
+ ///
+ Task ApproveCampMemberAsync(
+ Guid campMemberId, Guid approvedByUserId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Reject a pending camp membership request. Sets status to Removed.
+ ///
+ Task RejectCampMemberAsync(
+ Guid campMemberId, Guid rejectedByUserId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Lead removes an active camp member. Sets status to Removed.
+ ///
+ Task RemoveCampMemberAsync(
+ Guid campMemberId, Guid removedByUserId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Human withdraws their own pending request. Sets status to Removed.
+ ///
+ Task WithdrawCampMembershipRequestAsync(
+ Guid campMemberId, Guid userId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Human leaves an active membership. Sets status to Removed.
+ ///
+ Task LeaveCampAsync(
+ Guid campMemberId, Guid userId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the current user's membership state for a camp's open-season (or null if no open season).
+ ///
+ Task GetMembershipStateForCampAsync(
+ Guid campId, Guid userId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Lists members (pending + active) for a given camp season. Privileged view.
+ ///
+ Task GetCampMembersAsync(
+ Guid campSeasonId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Lists camps a human belongs to or has requested, grouped by year.
+ /// Used for the human's own profile dashboard.
+ ///
+ Task> GetCampMembershipsForUserAsync(
+ Guid userId, CancellationToken cancellationToken = default);
}
public record CampSeasonData(
@@ -276,3 +337,68 @@ public record CampSeasonDisplayData(string Name, string CampSlug, SoundZone? Sou
/// Lightweight camp season summary (ID, name, camp slug) for listing.
///
public record CampSeasonBrief(Guid CampSeasonId, string Name, string CampSlug);
+
+///
+/// Result of a camp membership request action.
+///
+public record CampMemberRequestResult(
+ Guid CampMemberId,
+ CampMemberRequestOutcome Outcome,
+ string? Message = null);
+
+public enum CampMemberRequestOutcome
+{
+ /// A new pending request was created.
+ Created,
+ /// An existing pending request already existed for the human.
+ AlreadyPending,
+ /// The human is already an active member of the camp for this season.
+ AlreadyActive,
+ /// No open season for the camp — the request was not created.
+ NoOpenSeason
+}
+
+///
+/// Current human's camp membership state relative to a camp's open-season.
+///
+public record CampMembershipState(
+ int? OpenSeasonYear,
+ Guid? OpenSeasonId,
+ Guid? CampMemberId,
+ CampMemberStatusSummary Status);
+
+public enum CampMemberStatusSummary
+{
+ /// No open season for the camp.
+ NoOpenSeason,
+ /// Open season exists but human has no record.
+ None,
+ /// Human has a pending request.
+ Pending,
+ /// Human is an active member.
+ Active
+}
+
+public record CampMemberListData(
+ Guid CampSeasonId,
+ int Year,
+ IReadOnlyList Pending,
+ IReadOnlyList Active);
+
+public record CampMemberRow(
+ Guid CampMemberId,
+ Guid UserId,
+ string DisplayName,
+ Instant RequestedAt,
+ Instant? ConfirmedAt);
+
+public record CampMembershipSummary(
+ Guid CampMemberId,
+ Guid CampId,
+ string CampSlug,
+ string CampName,
+ Guid CampSeasonId,
+ int Year,
+ CampMemberStatus Status,
+ Instant RequestedAt,
+ Instant? ConfirmedAt);
diff --git a/src/Humans.Application/NotificationSourceMapping.cs b/src/Humans.Application/NotificationSourceMapping.cs
index a5dc2a3a..fb44863a 100644
--- a/src/Humans.Application/NotificationSourceMapping.cs
+++ b/src/Humans.Application/NotificationSourceMapping.cs
@@ -33,6 +33,9 @@ public static class NotificationSourceMapping
NotificationSource.GoogleDriftDetected => MessageCategory.System,
NotificationSource.FacilitatedMessageReceived => MessageCategory.FacilitatedMessages,
NotificationSource.LegalDocumentPublished => MessageCategory.System,
+ NotificationSource.CampMembershipRequested => MessageCategory.TeamUpdates,
+ NotificationSource.CampMembershipApproved => MessageCategory.TeamUpdates,
+ NotificationSource.CampMembershipRejected => MessageCategory.TeamUpdates,
_ => MessageCategory.System
};
}
diff --git a/src/Humans.Domain/Entities/CampMember.cs b/src/Humans.Domain/Entities/CampMember.cs
new file mode 100644
index 00000000..00d192d7
--- /dev/null
+++ b/src/Humans.Domain/Entities/CampMember.cs
@@ -0,0 +1,35 @@
+using Humans.Domain.Enums;
+using NodaTime;
+
+namespace Humans.Domain.Entities;
+
+///
+/// A human's membership in a camp for a given season. This is a post-hoc record —
+/// humans join a camp through the camp's own process (website, spreadsheet, WhatsApp) first,
+/// then create a CampMember so Humans knows about the relationship for roles, early entry,
+/// and notifications.
+///
+/// Per-season, not per-camp. Privacy: never rendered on anonymous or public views.
+///
+public class CampMember
+{
+ public Guid Id { get; init; }
+
+ public Guid CampSeasonId { get; init; }
+ public CampSeason CampSeason { get; set; } = null!;
+
+ public Guid UserId { get; init; }
+ public User User { get; set; } = null!;
+
+ public CampMemberStatus Status { get; set; } = CampMemberStatus.Pending;
+
+ public Instant RequestedAt { get; init; }
+
+ public Instant? ConfirmedAt { get; set; }
+ public Guid? ConfirmedByUserId { get; set; }
+ public User? ConfirmedByUser { get; set; }
+
+ public Instant? RemovedAt { get; set; }
+ public Guid? RemovedByUserId { get; set; }
+ public User? RemovedByUser { get; set; }
+}
diff --git a/src/Humans.Domain/Enums/AuditAction.cs b/src/Humans.Domain/Enums/AuditAction.cs
index a88f4977..bb871308 100644
--- a/src/Humans.Domain/Enums/AuditAction.cs
+++ b/src/Humans.Domain/Enums/AuditAction.cs
@@ -51,6 +51,12 @@ public enum AuditAction
CampPrimaryLeadTransferred,
CampImageUploaded,
CampImageDeleted,
+ CampMemberRequested,
+ CampMemberApproved,
+ CampMemberRejected,
+ CampMemberRemoved,
+ CampMemberWithdrawn,
+ CampMemberLeft,
ShiftSignupConfirmed,
ShiftSignupRefused,
ShiftSignupVoluntold,
diff --git a/src/Humans.Domain/Enums/CampMemberStatus.cs b/src/Humans.Domain/Enums/CampMemberStatus.cs
new file mode 100644
index 00000000..82650c5a
--- /dev/null
+++ b/src/Humans.Domain/Enums/CampMemberStatus.cs
@@ -0,0 +1,16 @@
+namespace Humans.Domain.Enums;
+
+///
+/// Status of a human's membership in a camp for a specific season.
+///
+public enum CampMemberStatus
+{
+ /// The human has requested membership and is awaiting lead approval.
+ Pending = 0,
+
+ /// The human is an active member of the camp for the season.
+ Active = 1,
+
+ /// Membership was removed or withdrawn. Soft-deleted row preserved for audit.
+ Removed = 2
+}
diff --git a/src/Humans.Domain/Enums/NotificationSource.cs b/src/Humans.Domain/Enums/NotificationSource.cs
index ed70a500..c3b6e569 100644
--- a/src/Humans.Domain/Enums/NotificationSource.cs
+++ b/src/Humans.Domain/Enums/NotificationSource.cs
@@ -76,5 +76,14 @@ public enum NotificationSource
FacilitatedMessageReceived = 23,
/// A new legal document version was published.
- LegalDocumentPublished = 24
+ LegalDocumentPublished = 24,
+
+ /// A camp membership request was approved by a lead.
+ CampMembershipApproved = 25,
+
+ /// A camp membership request was rejected by a lead.
+ CampMembershipRejected = 26,
+
+ /// A human requested membership in a camp (notifies leads).
+ CampMembershipRequested = 27
}
diff --git a/src/Humans.Infrastructure/Data/Configurations/CampMemberConfiguration.cs b/src/Humans.Infrastructure/Data/Configurations/CampMemberConfiguration.cs
new file mode 100644
index 00000000..7452ed1d
--- /dev/null
+++ b/src/Humans.Infrastructure/Data/Configurations/CampMemberConfiguration.cs
@@ -0,0 +1,45 @@
+using Humans.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Humans.Infrastructure.Data.Configurations;
+
+public class CampMemberConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("camp_members");
+
+ builder.Property(m => m.Status).HasConversion().HasMaxLength(50).IsRequired();
+ builder.Property(m => m.RequestedAt).IsRequired();
+
+ builder.HasOne(m => m.CampSeason)
+ .WithMany()
+ .HasForeignKey(m => m.CampSeasonId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder.HasOne(m => m.User)
+ .WithMany()
+ .HasForeignKey(m => m.UserId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder.HasOne(m => m.ConfirmedByUser)
+ .WithMany()
+ .HasForeignKey(m => m.ConfirmedByUserId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ builder.HasOne(m => m.RemovedByUser)
+ .WithMany()
+ .HasForeignKey(m => m.RemovedByUserId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ // Partial unique index: one active-or-pending membership per (season, user).
+ // Removed rows are allowed to coexist so users can re-request later.
+ builder.HasIndex(m => new { m.CampSeasonId, m.UserId })
+ .HasFilter("\"Status\" <> 'Removed'")
+ .IsUnique()
+ .HasDatabaseName("IX_camp_members_active_unique");
+
+ builder.HasIndex(m => m.UserId);
+ }
+}
diff --git a/src/Humans.Infrastructure/Data/HumansDbContext.cs b/src/Humans.Infrastructure/Data/HumansDbContext.cs
index fd2af434..824ca28a 100644
--- a/src/Humans.Infrastructure/Data/HumansDbContext.cs
+++ b/src/Humans.Infrastructure/Data/HumansDbContext.cs
@@ -43,6 +43,7 @@ public HumansDbContext(DbContextOptions options)
public DbSet Camps => Set();
public DbSet CampSeasons => Set();
public DbSet CampLeads => Set();
+ public DbSet CampMembers => Set();
public DbSet CampHistoricalNames => Set();
public DbSet CampImages => Set();
public DbSet CampSettings => Set();
diff --git a/src/Humans.Infrastructure/Migrations/20260415021854_AddCampMembers.Designer.cs b/src/Humans.Infrastructure/Migrations/20260415021854_AddCampMembers.Designer.cs
new file mode 100644
index 00000000..705be831
--- /dev/null
+++ b/src/Humans.Infrastructure/Migrations/20260415021854_AddCampMembers.Designer.cs
@@ -0,0 +1,4708 @@
+//
+using System;
+using Humans.Infrastructure.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using NodaTime;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Humans.Infrastructure.Migrations
+{
+ [DbContext(typeof(HumansDbContext))]
+ [Migration("20260415021854_AddCampMembers")]
+ partial class AddCampMembers
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Humans.Domain.Entities.AccountMergeRequest", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AdminNotes")
+ .HasMaxLength(4000)
+ .HasColumnType("character varying(4000)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("PendingEmailId")
+ .HasColumnType("uuid");
+
+ b.Property("ResolvedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ResolvedByUserId")
+ .HasColumnType("uuid");
+
+ b.Property("SourceUserId")
+ .HasColumnType("uuid");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("TargetUserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ResolvedByUserId");
+
+ b.HasIndex("SourceUserId");
+
+ b.HasIndex("Status");
+
+ b.HasIndex("TargetUserId");
+
+ b.ToTable("account_merge_requests", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.Application", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AdditionalInfo")
+ .HasMaxLength(4000)
+ .HasColumnType("character varying(4000)");
+
+ b.Property("BoardMeetingDate")
+ .HasColumnType("date");
+
+ b.Property("DecisionNote")
+ .HasMaxLength(4000)
+ .HasColumnType("character varying(4000)");
+
+ b.Property("Language")
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)");
+
+ b.Property("MembershipTier")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Motivation")
+ .IsRequired()
+ .HasMaxLength(4000)
+ .HasColumnType("character varying(4000)");
+
+ b.Property("RenewalReminderSentAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ResolvedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ReviewNotes")
+ .HasMaxLength(4000)
+ .HasColumnType("character varying(4000)");
+
+ b.Property("ReviewStartedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ReviewedByUserId")
+ .HasColumnType("uuid");
+
+ b.Property("RoleUnderstanding")
+ .HasColumnType("text");
+
+ b.Property("SignificantContribution")
+ .HasColumnType("text");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("SubmittedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("TermExpiresAt")
+ .HasColumnType("date");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MembershipTier");
+
+ b.HasIndex("ReviewedByUserId");
+
+ b.HasIndex("Status");
+
+ b.HasIndex("SubmittedAt");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("UserId", "Status");
+
+ b.ToTable("applications", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.ApplicationStateHistory", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ApplicationId")
+ .HasColumnType("uuid");
+
+ b.Property("ChangedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ChangedByUserId")
+ .HasColumnType("uuid");
+
+ b.Property("Notes")
+ .HasMaxLength(4000)
+ .HasColumnType("character varying(4000)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ApplicationId");
+
+ b.HasIndex("ChangedAt");
+
+ b.HasIndex("ChangedByUserId");
+
+ b.ToTable("application_state_history", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.AuditLogEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Action")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("ActorUserId")
+ .HasColumnType("uuid");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(4000)
+ .HasColumnType("character varying(4000)");
+
+ b.Property("EntityId")
+ .HasColumnType("uuid");
+
+ b.Property("EntityType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("ErrorMessage")
+ .HasMaxLength(4000)
+ .HasColumnType("character varying(4000)");
+
+ b.Property("OccurredAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("RelatedEntityId")
+ .HasColumnType("uuid");
+
+ b.Property("RelatedEntityType")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("ResourceId")
+ .HasColumnType("uuid");
+
+ b.Property("Role")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Success")
+ .HasColumnType("boolean");
+
+ b.Property("SyncSource")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("UserEmail")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Action");
+
+ b.HasIndex("ActorUserId");
+
+ b.HasIndex("OccurredAt");
+
+ b.HasIndex("ResourceId");
+
+ b.HasIndex("EntityType", "EntityId");
+
+ b.HasIndex("RelatedEntityType", "RelatedEntityId");
+
+ b.ToTable("audit_log", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.BoardVote", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ApplicationId")
+ .HasColumnType("uuid");
+
+ b.Property("BoardMemberUserId")
+ .HasColumnType("uuid");
+
+ b.Property("Note")
+ .HasMaxLength(4000)
+ .HasColumnType("character varying(4000)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Vote")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("VotedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ApplicationId");
+
+ b.HasIndex("BoardMemberUserId");
+
+ b.HasIndex("ApplicationId", "BoardMemberUserId")
+ .IsUnique();
+
+ b.ToTable("board_votes", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.BudgetAuditLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ActorUserId")
+ .HasColumnType("uuid");
+
+ b.Property("BudgetYearId")
+ .HasColumnType("uuid");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("EntityId")
+ .HasColumnType("uuid");
+
+ b.Property("EntityType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("FieldName")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("NewValue")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("OccurredAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("OldValue")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ActorUserId");
+
+ b.HasIndex("BudgetYearId");
+
+ b.HasIndex("OccurredAt");
+
+ b.HasIndex("EntityType", "EntityId");
+
+ b.ToTable("budget_audit_logs", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.BudgetCategory", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AllocatedAmount")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("BudgetGroupId")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpenditureType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.Property("TeamId")
+ .HasColumnType("uuid");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TeamId")
+ .HasFilter("\"TeamId\" IS NOT NULL");
+
+ b.HasIndex("BudgetGroupId", "SortOrder");
+
+ b.ToTable("budget_categories", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.BudgetGroup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("BudgetYearId")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsDepartmentGroup")
+ .HasColumnType("boolean");
+
+ b.Property("IsRestricted")
+ .HasColumnType("boolean");
+
+ b.Property("IsTicketingGroup")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BudgetYearId", "SortOrder");
+
+ b.ToTable("budget_groups", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.BudgetLineItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Amount")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("BudgetCategoryId")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("ExpectedDate")
+ .HasColumnType("date");
+
+ b.Property("IsAutoGenerated")
+ .HasColumnType("boolean");
+
+ b.Property("IsCashflowOnly")
+ .HasColumnType("boolean");
+
+ b.Property("Notes")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("ResponsibleTeamId")
+ .HasColumnType("uuid");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("VatRate")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ResponsibleTeamId")
+ .HasFilter("\"ResponsibleTeamId\" IS NOT NULL");
+
+ b.HasIndex("BudgetCategoryId", "SortOrder");
+
+ b.ToTable("budget_line_items", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.BudgetYear", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsDeleted")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Year")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Status");
+
+ b.HasIndex("Year")
+ .IsUnique();
+
+ b.ToTable("budget_years", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.Camp", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ContactEmail")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("ContactPhone")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedByUserId")
+ .HasColumnType("uuid");
+
+ b.Property("HideHistoricalNames")
+ .HasColumnType("boolean");
+
+ b.Property("IsSwissCamp")
+ .HasColumnType("boolean");
+
+ b.Property("Links")
+ .HasColumnType("jsonb");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("TimesAtNowhere")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("WebOrSocialUrl")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedByUserId");
+
+ b.HasIndex("Slug")
+ .IsUnique();
+
+ b.ToTable("camps", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.CampHistoricalName", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CampId")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("Source")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Year")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CampId");
+
+ b.ToTable("camp_historical_names", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.CampImage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CampId")
+ .HasColumnType("uuid");
+
+ b.Property("ContentType")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.Property("StoragePath")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("UploadedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CampId");
+
+ b.ToTable("camp_images", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.CampLead", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CampId")
+ .HasColumnType("uuid");
+
+ b.Property("JoinedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LeftAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Role")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("CampId", "UserId")
+ .IsUnique()
+ .HasDatabaseName("IX_camp_leads_active_unique")
+ .HasFilter("\"LeftAt\" IS NULL");
+
+ b.ToTable("camp_leads", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.CampMember", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CampSeasonId")
+ .HasColumnType("uuid");
+
+ b.Property("ConfirmedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ConfirmedByUserId")
+ .HasColumnType("uuid");
+
+ b.Property("RemovedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("RemovedByUserId")
+ .HasColumnType("uuid");
+
+ b.Property("RequestedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ConfirmedByUserId");
+
+ b.HasIndex("RemovedByUserId");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("CampSeasonId", "UserId")
+ .IsUnique()
+ .HasDatabaseName("IX_camp_members_active_unique")
+ .HasFilter("\"Status\" <> 'Removed'");
+
+ b.ToTable("camp_members", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.CampPolygon", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AreaSqm")
+ .HasColumnType("double precision");
+
+ b.Property("CampSeasonId")
+ .HasColumnType("uuid");
+
+ b.Property("GeoJson")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("LastModifiedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LastModifiedByUserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CampSeasonId")
+ .IsUnique();
+
+ b.HasIndex("LastModifiedByUserId");
+
+ b.ToTable("camp_polygons", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.CampPolygonHistory", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AreaSqm")
+ .HasColumnType("double precision");
+
+ b.Property("CampSeasonId")
+ .HasColumnType("uuid");
+
+ b.Property("GeoJson")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ModifiedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ModifiedByUserId")
+ .HasColumnType("uuid");
+
+ b.Property("Note")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ModifiedByUserId");
+
+ b.HasIndex("CampSeasonId", "ModifiedAt");
+
+ b.ToTable("camp_polygon_histories", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.CampSeason", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AcceptingMembers")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("AdultPlayspace")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("BlurbLong")
+ .IsRequired()
+ .HasMaxLength(4000)
+ .HasColumnType("character varying(4000)");
+
+ b.Property("BlurbShort")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("CampId")
+ .HasColumnType("uuid");
+
+ b.Property("ContainerCount")
+ .HasColumnType("integer");
+
+ b.Property("ContainerNotes")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ElectricalGrid")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("HasPerformanceSpace")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("KidsAreaDescription")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("KidsVisiting")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("KidsWelcome")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Languages")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("MemberCount")
+ .HasColumnType("integer");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NameLockDate")
+ .HasColumnType("date");
+
+ b.Property("NameLockedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("PerformanceTypes")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("ResolvedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ReviewNotes")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("ReviewedByUserId")
+ .HasColumnType("uuid");
+
+ b.Property("SoundZone")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("SpaceRequirement")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Vibes")
+ .IsRequired()
+ .HasColumnType("jsonb");
+
+ b.Property("Year")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ReviewedByUserId");
+
+ b.HasIndex("Status");
+
+ b.HasIndex("CampId", "Year")
+ .IsUnique();
+
+ b.ToTable("camp_seasons", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.CampSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("OpenSeasons")
+ .IsRequired()
+ .HasColumnType("jsonb");
+
+ b.Property("PublicYear")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("camp_settings", (string)null);
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0010-000000000001"),
+ OpenSeasons = "[2026]",
+ PublicYear = 2026
+ });
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.Campaign", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedByUserId")
+ .HasColumnType("uuid");
+
+ b.Property("Description")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("EmailBodyTemplate")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("EmailSubject")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("ReplyToAddress")
+ .HasMaxLength(320)
+ .HasColumnType("character varying(320)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedByUserId");
+
+ b.ToTable("campaigns", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.CampaignCode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CampaignId")
+ .HasColumnType("uuid");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("ImportOrder")
+ .HasColumnType("integer");
+
+ b.Property("ImportedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CampaignId", "Code")
+ .IsUnique();
+
+ b.ToTable("campaign_codes", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.CampaignGrant", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AssignedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CampaignCodeId")
+ .HasColumnType("uuid");
+
+ b.Property("CampaignId")
+ .HasColumnType("uuid");
+
+ b.Property("LatestEmailAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LatestEmailStatus")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("RedeemedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CampaignCodeId")
+ .IsUnique();
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("CampaignId", "UserId")
+ .IsUnique();
+
+ b.ToTable("campaign_grants", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.CityPlanningSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ClosedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsPlacementOpen")
+ .HasColumnType("boolean");
+
+ b.Property("LimitZoneGeoJson")
+ .HasColumnType("text");
+
+ b.Property("OfficialZonesGeoJson")
+ .HasColumnType("text");
+
+ b.Property("OpenedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("PlacementClosesAt")
+ .HasColumnType("timestamp without time zone");
+
+ b.Property("PlacementOpensAt")
+ .HasColumnType("timestamp without time zone");
+
+ b.Property("RegistrationInfo")
+ .HasColumnType("text");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Year")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Year")
+ .IsUnique();
+
+ b.ToTable("city_planning_settings", (string)null);
+ });
+
+ modelBuilder.Entity("Humans.Domain.Entities.CommunicationPreference", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Category")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("InboxEnabled")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("OptedOut")
+ .HasColumnType("boolean");
+
+ b.Property("UpdateSource")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property