diff --git a/backend/src/Socialize.Api/Data/AppDbContext.cs b/backend/src/Socialize.Api/Data/AppDbContext.cs index e9160e95..989387f8 100644 --- a/backend/src/Socialize.Api/Data/AppDbContext.cs +++ b/backend/src/Socialize.Api/Data/AppDbContext.cs @@ -22,6 +22,8 @@ internal class AppDbContext( { public DbSet Organizations => Set(); public DbSet OrganizationMembershipTiers => Set(); + public DbSet OrganizationMembershipTierTranslations => + Set(); public DbSet OrganizationMemberships => Set(); public DbSet Workspaces => Set(); public DbSet WorkspaceInvites => Set(); diff --git a/backend/src/Socialize.Api/Migrations/20260508003746_LocalizeOrganizationMembershipTiers.Designer.cs b/backend/src/Socialize.Api/Migrations/20260508003746_LocalizeOrganizationMembershipTiers.Designer.cs new file mode 100644 index 00000000..a7a8422e --- /dev/null +++ b/backend/src/Socialize.Api/Migrations/20260508003746_LocalizeOrganizationMembershipTiers.Designer.cs @@ -0,0 +1,2382 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Socialize.Api.Data; + +#nullable disable + +namespace Socialize.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260508003746_LocalizeOrganizationMembershipTiers")] + partial class LocalizeOrganizationMembershipTiers + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalDecision", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApprovalRequestId") + .HasColumnType("uuid"); + + b.Property("Comment") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("DecidedByEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DecidedByName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DecidedByUserId") + .HasColumnType("uuid"); + + b.Property("Decision") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalRequestId"); + + b.ToTable("ApprovalDecisions", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessToken") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("DueAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RequestedByUserId") + .HasColumnType("uuid"); + + b.Property("ReviewerEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ReviewerName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Stage") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("State") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WorkflowInstanceId") + .HasColumnType("uuid"); + + b.Property("WorkflowStepRequiredApproverCount") + .HasColumnType("integer"); + + b.Property("WorkflowStepSortOrder") + .HasColumnType("integer"); + + b.Property("WorkflowStepTargetType") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("WorkflowStepTargetValue") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("ReviewerEmail"); + + b.HasIndex("WorkflowInstanceId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("ApprovalRequests", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalWorkflowInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApprovalMode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("StartedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("State") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("WorkspaceId"); + + b.HasIndex("ContentItemId", "State") + .IsUnique() + .HasFilter("\"State\" = 'Pending'"); + + b.ToTable("ApprovalWorkflowInstances", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.WorkspaceApprovalStepConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RequiredApproverCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TargetValue") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorkspaceId"); + + b.HasIndex("WorkspaceId", "SortOrder") + .IsUnique(); + + b.ToTable("WorkspaceApprovalStepConfigurations", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.Asset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssetType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CurrentRevisionNumber") + .HasColumnType("integer"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GoogleDriveFileId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GoogleDriveLink") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("PreviewUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("Assets", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.AssetRevision", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssetId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("PreviewUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("RevisionNumber") + .HasColumnType("integer"); + + b.Property("SourceReference") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.HasKey("Id"); + + b.HasIndex("AssetId"); + + b.HasIndex("AssetId", "RevisionNumber") + .IsUnique(); + + b.ToTable("AssetRevisions", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarCatalogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Country") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CultureOrReligion") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("DefaultColor") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Region") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("SourceUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TrustLevel") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("Country"); + + b.HasIndex("ProviderName"); + + b.ToTable("CalendarCatalogEntries", (string)null); + + b.HasData( + new + { + Id = new Guid("10000000-0000-0000-0000-000000000001"), + Category = "public-holiday", + Country = "US", + CreatedAt = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DefaultColor = "#2F80ED", + Description = "Federal public holiday calendar for the United States.", + Language = "en", + ProviderName = "Nager.Date", + SourceUrl = "https://date.nager.at/api/v3/PublicHolidays/2026/US", + Title = "United States Public Holidays", + TrustLevel = "Verified" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000002"), + Category = "public-holiday", + Country = "CA", + CreatedAt = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DefaultColor = "#2F80ED", + Description = "Public holiday calendar for Canada.", + Language = "en", + ProviderName = "Nager.Date", + SourceUrl = "https://date.nager.at/api/v3/PublicHolidays/2026/CA", + Title = "Canada Public Holidays", + TrustLevel = "Verified" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000003"), + Category = "marketing-moment", + CreatedAt = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DefaultColor = "#9B51E0", + Description = "Common retail, awareness, and social planning moments.", + Language = "en", + ProviderName = "Socialize", + SourceUrl = "https://example.com/socialize/marketing-moments.ics", + Title = "Common Marketing Moments", + TrustLevel = "Maintained" + }); + }); + + modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CalendarSourceId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("EndLocalDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EndUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ImportedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAllDay") + .HasColumnType("boolean"); + + b.Property("IsFloatingTime") + .HasColumnType("boolean"); + + b.Property("Location") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("RecurrenceId") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("SourceEventUid") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("SourceLastModifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("StartLocalDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeZoneId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("Id"); + + b.HasIndex("CalendarSourceId"); + + b.HasIndex("CalendarSourceId", "SourceEventUid", "StartDate") + .IsUnique(); + + b.ToTable("CalendarEvents", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CatalogSourceReference") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("DisplayTitle") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("InheritanceMode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LastAttemptedSyncAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSuccessfulSyncAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSyncError") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("SourceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Scope"); + + b.HasIndex("UserId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("CalendarSources", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.UserCalendarExportFeed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("TokenHash") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserCalendarExportFeeds", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Campaigns.Data.Campaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("WorkspaceId"); + + b.HasIndex("ClientId", "Name") + .IsUnique(); + + b.ToTable("Campaigns", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Channels.Data.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ExternalUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Handle") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Network") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorkspaceId"); + + b.HasIndex("WorkspaceId", "Network", "Name") + .IsUnique(); + + b.ToTable("Channels", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Clients.Data.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PortraitUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("PrimaryContactEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PrimaryContactName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PrimaryContactPortraitUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorkspaceId"); + + b.HasIndex("WorkspaceId", "Name") + .IsUnique(); + + b.ToTable("Clients", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Comments.Data.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttachmentBlobContainerName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("AttachmentBlobName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("AttachmentBlobUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("AttachmentContentType") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("AttachmentFileName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AttachmentSizeBytes") + .HasColumnType("bigint"); + + b.Property("AuthorDisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AuthorEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AuthorUserId") + .HasColumnType("uuid"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ParentCommentId") + .HasColumnType("uuid"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("Comments", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CurrentRevisionLabel") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CurrentRevisionNumber") + .HasColumnType("integer"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Hashtags") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("PublicationMessage") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PublicationTargets") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("ClientId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("ContentItems", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemActivityEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActorEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ActorUserId") + .HasColumnType("uuid"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("EntityId") + .HasColumnType("uuid"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("MetadataJson") + .HasColumnType("jsonb"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("WorkspaceId"); + + b.HasIndex("ContentItemId", "CreatedAt"); + + b.ToTable("ContentItemActivityEntries", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemRevision", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChangeSummary") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("Hashtags") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("PublicationMessage") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PublicationTargets") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("RevisionLabel") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("RevisionNumber") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("ContentItemId", "RevisionNumber") + .IsUnique(); + + b.ToTable("ContentItemRevisions", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackActivityEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActivityType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ActorDisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ActorEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ActorUserId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("FeedbackReportId") + .HasColumnType("uuid"); + + b.Property("FromValue") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Note") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ToValue") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("Id"); + + b.HasIndex("ActorUserId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("FeedbackReportId"); + + b.ToTable("FeedbackActivityEntries", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorDisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AuthorEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AuthorRole") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("AuthorUserId") + .HasColumnType("uuid"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("FeedbackReportId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AuthorUserId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("FeedbackReportId"); + + b.ToTable("FeedbackComments", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppVersion") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("BrowserUserAgent") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("CampaignName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CancellationReason") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CancelledByUserId") + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("ContentItemTitle") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("LastActivityAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReporterDisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ReporterEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ReporterUserId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("SubmittedPath") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ViewportHeight") + .HasColumnType("integer"); + + b.Property("ViewportWidth") + .HasColumnType("integer"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.Property("WorkspaceName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("LastActivityAt"); + + b.HasIndex("ReporterUserId"); + + b.HasIndex("Status"); + + b.HasIndex("Type"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("FeedbackReports", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackScreenshot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BlobContainerName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("BlobName") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("FeedbackReportId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("FeedbackReportId") + .IsUnique(); + + b.ToTable("FeedbackScreenshots", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FeedbackReportId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName"); + + b.HasIndex("FeedbackReportId", "NormalizedName") + .IsUnique(); + + b.ToTable("FeedbackTags", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Identity.Data.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Identity.Data.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Alias") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BirthDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FacebookId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Firstname") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GoogleId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Lastname") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PortraitUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("RefreshToken") + .HasMaxLength(44) + .HasColumnType("character varying(44)"); + + b.Property("RefreshTokenExpiryTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Notifications.Data.NotificationEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("EntityId") + .HasColumnType("uuid"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("MetadataJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RecipientEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RecipientUserId") + .HasColumnType("uuid"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("RecipientUserId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("NotificationEvents", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LogoUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("MembershipTierId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValue(new Guid("20000000-0000-0000-0000-000000000001")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnerUserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MembershipTierId"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("Organizations", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique(); + + b.ToTable("OrganizationMemberships", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActiveContentLimit") + .HasColumnType("integer"); + + b.Property("ExternalReviewerLimit") + .HasColumnType("integer"); + + b.Property("IsCustom") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MemberLimit") + .HasColumnType("integer"); + + b.Property("MonthlyPriceCents") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("WorkspaceLimit") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.HasIndex("SortOrder"); + + b.ToTable("OrganizationMembershipTiers", (string)null); + + b.HasData( + new + { + Id = new Guid("20000000-0000-0000-0000-000000000001"), + ActiveContentLimit = 3, + ExternalReviewerLimit = 1, + IsCustom = false, + Key = "free", + MemberLimit = 2, + MonthlyPriceCents = 0, + SortOrder = 10, + WorkspaceLimit = 1 + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000002"), + ActiveContentLimit = 25, + ExternalReviewerLimit = 10, + IsCustom = false, + Key = "freelance", + MemberLimit = 5, + MonthlyPriceCents = 1900, + SortOrder = 20, + WorkspaceLimit = 3 + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000003"), + ActiveContentLimit = 250, + IsCustom = false, + Key = "agency", + MemberLimit = 25, + MonthlyPriceCents = 7900, + SortOrder = 30, + WorkspaceLimit = 15 + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000004"), + IsCustom = true, + Key = "enterprise", + SortOrder = 40 + }); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTierTranslation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("MembershipTierId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("MembershipTierId", "Culture") + .IsUnique(); + + b.ToTable("OrganizationMembershipTierTranslations", (string)null); + + b.HasData( + new + { + Id = new Guid("20000000-0000-0001-0000-000000000001"), + Culture = "en", + Description = "For trying Socialize on one real approval workflow.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000001"), + Name = "Free" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000002"), + Culture = "fr", + Description = "Pour essayer Socialize sur un vrai workflow d'approbation.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000001"), + Name = "Free" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000003"), + Culture = "en", + Description = "For solo operators managing recurring client reviews.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000002"), + Name = "Freelance" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000004"), + Culture = "fr", + Description = "Pour les independants qui gerent des revisions client recurrentes.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000002"), + Name = "Freelance" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000005"), + Culture = "en", + Description = "For agencies that need repeatable client approval operations.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000003"), + Name = "Agency" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000006"), + Culture = "fr", + Description = "Pour les agences qui veulent des operations d'approbation client repetables.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000003"), + Name = "Agency" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000007"), + Culture = "en", + Description = "For larger organizations with governance and access needs.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000004"), + Name = "Enterprise" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000008"), + Culture = "fr", + Description = "Pour les grandes organisations avec des besoins de gouvernance et d'acces.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000004"), + Name = "Enterprise" + }); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApprovalMode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasDefaultValue("Required"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LockContentAfterApproval") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LogoUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OwnerUserId") + .HasColumnType("uuid"); + + b.Property("SchedulePostsAutomaticallyOnApproval") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("SendAutomaticApprovalReminders") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("TimeZone") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("Workspaces", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.WorkspaceInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("InvitedByUserId") + .HasColumnType("uuid"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorkspaceId"); + + b.HasIndex("WorkspaceId", "Email", "Status"); + + b.ToTable("WorkspaceInvites", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Socialize.Api.Modules.Identity.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Socialize.Api.Modules.Identity.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Socialize.Api.Modules.Identity.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Socialize.Api.Modules.Identity.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Identity.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Socialize.Api.Modules.Identity.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalDecision", b => + { + b.HasOne("Socialize.Api.Modules.Approvals.Data.ApprovalRequest", null) + .WithMany() + .HasForeignKey("ApprovalRequestId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalRequest", b => + { + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Approvals.Data.ApprovalWorkflowInstance", null) + .WithMany() + .HasForeignKey("WorkflowInstanceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalWorkflowInstance", b => + { + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.WorkspaceApprovalStepConfiguration", b => + { + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.Asset", b => + { + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.AssetRevision", b => + { + b.HasOne("Socialize.Api.Modules.Assets.Data.Asset", null) + .WithMany() + .HasForeignKey("AssetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarEvent", b => + { + b.HasOne("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarSource", null) + .WithMany() + .HasForeignKey("CalendarSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Campaigns.Data.Campaign", b => + { + b.HasOne("Socialize.Api.Modules.Clients.Data.Client", null) + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Channels.Data.Channel", b => + { + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Clients.Data.Client", b => + { + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Comments.Data.Comment", b => + { + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Comments.Data.Comment", null) + .WithMany() + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItem", b => + { + b.HasOne("Socialize.Api.Modules.Campaigns.Data.Campaign", null) + .WithMany() + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Clients.Data.Client", null) + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemActivityEntry", b => + { + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemRevision", b => + { + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackActivityEntry", b => + { + b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport") + .WithMany("ActivityEntries") + .HasForeignKey("FeedbackReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FeedbackReport"); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackComment", b => + { + b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport") + .WithMany("Comments") + .HasForeignKey("FeedbackReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FeedbackReport"); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b => + { + b.HasOne("Socialize.Api.Modules.Campaigns.Data.Campaign", null) + .WithMany() + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Socialize.Api.Modules.Clients.Data.Client", null) + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackScreenshot", b => + { + b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport") + .WithOne("Screenshot") + .HasForeignKey("Socialize.Api.Modules.Feedback.Data.FeedbackScreenshot", "FeedbackReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FeedbackReport"); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackTag", b => + { + b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport") + .WithMany("Tags") + .HasForeignKey("FeedbackReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FeedbackReport"); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Notifications.Data.NotificationEvent", b => + { + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.Organization", b => + { + b.HasOne("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTier", null) + .WithMany() + .HasForeignKey("MembershipTierId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembership", b => + { + b.HasOne("Socialize.Api.Modules.Organizations.Data.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTierTranslation", b => + { + b.HasOne("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTier", null) + .WithMany() + .HasForeignKey("MembershipTierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b => + { + b.HasOne("Socialize.Api.Modules.Organizations.Data.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.WorkspaceInvite", b => + { + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b => + { + b.Navigation("ActivityEntries"); + + b.Navigation("Comments"); + + b.Navigation("Screenshot"); + + b.Navigation("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Socialize.Api/Migrations/20260508003746_LocalizeOrganizationMembershipTiers.cs b/backend/src/Socialize.Api/Migrations/20260508003746_LocalizeOrganizationMembershipTiers.cs new file mode 100644 index 00000000..b51c36c8 --- /dev/null +++ b/backend/src/Socialize.Api/Migrations/20260508003746_LocalizeOrganizationMembershipTiers.cs @@ -0,0 +1,137 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Socialize.Api.Migrations +{ + /// + internal partial class LocalizeOrganizationMembershipTiers : Migration + { + private static readonly string[] MembershipTierTranslationSeedColumns = + [ + "Id", + "Culture", + "Description", + "MembershipTierId", + "Name" + ]; + + private static readonly string[] MembershipTierColumnsToRestore = + [ + "Description", + "Name" + ]; + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "Description", + table: "OrganizationMembershipTiers"); + + migrationBuilder.DropColumn( + name: "Name", + table: "OrganizationMembershipTiers"); + + migrationBuilder.CreateTable( + name: "OrganizationMembershipTierTranslations", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + MembershipTierId = table.Column(type: "uuid", nullable: false), + Culture = table.Column(type: "character varying(16)", maxLength: 16, nullable: false), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationMembershipTierTranslations", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationMembershipTierTranslations_OrganizationMembersh~", + column: x => x.MembershipTierId, + principalTable: "OrganizationMembershipTiers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "OrganizationMembershipTierTranslations", + columns: MembershipTierTranslationSeedColumns, + values: new object[,] + { + { new Guid("20000000-0000-0001-0000-000000000001"), "en", "For trying Socialize on one real approval workflow.", new Guid("20000000-0000-0000-0000-000000000001"), "Free" }, + { new Guid("20000000-0000-0001-0000-000000000002"), "fr", "Pour essayer Socialize sur un vrai workflow d'approbation.", new Guid("20000000-0000-0000-0000-000000000001"), "Free" }, + { new Guid("20000000-0000-0001-0000-000000000003"), "en", "For solo operators managing recurring client reviews.", new Guid("20000000-0000-0000-0000-000000000002"), "Freelance" }, + { new Guid("20000000-0000-0001-0000-000000000004"), "fr", "Pour les independants qui gerent des revisions client recurrentes.", new Guid("20000000-0000-0000-0000-000000000002"), "Freelance" }, + { new Guid("20000000-0000-0001-0000-000000000005"), "en", "For agencies that need repeatable client approval operations.", new Guid("20000000-0000-0000-0000-000000000003"), "Agency" }, + { new Guid("20000000-0000-0001-0000-000000000006"), "fr", "Pour les agences qui veulent des operations d'approbation client repetables.", new Guid("20000000-0000-0000-0000-000000000003"), "Agency" }, + { new Guid("20000000-0000-0001-0000-000000000007"), "en", "For larger organizations with governance and access needs.", new Guid("20000000-0000-0000-0000-000000000004"), "Enterprise" }, + { new Guid("20000000-0000-0001-0000-000000000008"), "fr", "Pour les grandes organisations avec des besoins de gouvernance et d'acces.", new Guid("20000000-0000-0000-0000-000000000004"), "Enterprise" } + }); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationMembershipTierTranslations_MembershipTierId_Cul~", + table: "OrganizationMembershipTierTranslations", + columns: ["MembershipTierId", "Culture"], + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropTable( + name: "OrganizationMembershipTierTranslations"); + + migrationBuilder.AddColumn( + name: "Description", + table: "OrganizationMembershipTiers", + type: "character varying(512)", + maxLength: 512, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Name", + table: "OrganizationMembershipTiers", + type: "character varying(128)", + maxLength: 128, + nullable: false, + defaultValue: ""); + + migrationBuilder.UpdateData( + table: "OrganizationMembershipTiers", + keyColumn: "Id", + keyValue: new Guid("20000000-0000-0000-0000-000000000001"), + columns: MembershipTierColumnsToRestore, + values: new object[] { "For trying Socialize on one real approval workflow.", "Free" }); + + migrationBuilder.UpdateData( + table: "OrganizationMembershipTiers", + keyColumn: "Id", + keyValue: new Guid("20000000-0000-0000-0000-000000000002"), + columns: MembershipTierColumnsToRestore, + values: new object[] { "For solo operators managing recurring client reviews.", "Freelance" }); + + migrationBuilder.UpdateData( + table: "OrganizationMembershipTiers", + keyColumn: "Id", + keyValue: new Guid("20000000-0000-0000-0000-000000000003"), + columns: MembershipTierColumnsToRestore, + values: new object[] { "For agencies that need repeatable client approval operations.", "Agency" }); + + migrationBuilder.UpdateData( + table: "OrganizationMembershipTiers", + keyColumn: "Id", + keyValue: new Guid("20000000-0000-0000-0000-000000000004"), + columns: MembershipTierColumnsToRestore, + values: new object[] { "For larger organizations with governance and access needs.", "Enterprise" }); + } + } +} diff --git a/backend/src/Socialize.Api/Migrations/AppDbContextModelSnapshot.cs b/backend/src/Socialize.Api/Migrations/AppDbContextModelSnapshot.cs index 694e1e77..d8497086 100644 --- a/backend/src/Socialize.Api/Migrations/AppDbContextModelSnapshot.cs +++ b/backend/src/Socialize.Api/Migrations/AppDbContextModelSnapshot.cs @@ -1724,11 +1724,6 @@ namespace Socialize.Api.Migrations b.Property("ActiveContentLimit") .HasColumnType("integer"); - b.Property("Description") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - b.Property("ExternalReviewerLimit") .HasColumnType("integer"); @@ -1746,11 +1741,6 @@ namespace Socialize.Api.Migrations b.Property("MonthlyPriceCents") .HasColumnType("integer"); - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - b.Property("SortOrder") .HasColumnType("integer"); @@ -1771,13 +1761,11 @@ namespace Socialize.Api.Migrations { Id = new Guid("20000000-0000-0000-0000-000000000001"), ActiveContentLimit = 3, - Description = "For trying Socialize on one real approval workflow.", ExternalReviewerLimit = 1, IsCustom = false, Key = "free", MemberLimit = 2, MonthlyPriceCents = 0, - Name = "Free", SortOrder = 10, WorkspaceLimit = 1 }, @@ -1785,13 +1773,11 @@ namespace Socialize.Api.Migrations { Id = new Guid("20000000-0000-0000-0000-000000000002"), ActiveContentLimit = 25, - Description = "For solo operators managing recurring client reviews.", ExternalReviewerLimit = 10, IsCustom = false, Key = "freelance", MemberLimit = 5, MonthlyPriceCents = 1900, - Name = "Freelance", SortOrder = 20, WorkspaceLimit = 3 }, @@ -1799,26 +1785,120 @@ namespace Socialize.Api.Migrations { Id = new Guid("20000000-0000-0000-0000-000000000003"), ActiveContentLimit = 250, - Description = "For agencies that need repeatable client approval operations.", IsCustom = false, Key = "agency", MemberLimit = 25, MonthlyPriceCents = 7900, - Name = "Agency", SortOrder = 30, WorkspaceLimit = 15 }, new { Id = new Guid("20000000-0000-0000-0000-000000000004"), - Description = "For larger organizations with governance and access needs.", IsCustom = true, Key = "enterprise", - Name = "Enterprise", SortOrder = 40 }); }); + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTierTranslation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("MembershipTierId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("MembershipTierId", "Culture") + .IsUnique(); + + b.ToTable("OrganizationMembershipTierTranslations", (string)null); + + b.HasData( + new + { + Id = new Guid("20000000-0000-0001-0000-000000000001"), + Culture = "en", + Description = "For trying Socialize on one real approval workflow.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000001"), + Name = "Free" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000002"), + Culture = "fr", + Description = "Pour essayer Socialize sur un vrai workflow d'approbation.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000001"), + Name = "Free" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000003"), + Culture = "en", + Description = "For solo operators managing recurring client reviews.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000002"), + Name = "Freelance" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000004"), + Culture = "fr", + Description = "Pour les independants qui gerent des revisions client recurrentes.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000002"), + Name = "Freelance" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000005"), + Culture = "en", + Description = "For agencies that need repeatable client approval operations.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000003"), + Name = "Agency" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000006"), + Culture = "fr", + Description = "Pour les agences qui veulent des operations d'approbation client repetables.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000003"), + Name = "Agency" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000007"), + Culture = "en", + Description = "For larger organizations with governance and access needs.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000004"), + Name = "Enterprise" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000008"), + Culture = "fr", + Description = "Pour les grandes organisations avec des besoins de gouvernance et d'acces.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000004"), + Name = "Enterprise" + }); + }); + modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b => { b.Property("Id") @@ -2256,6 +2336,15 @@ namespace Socialize.Api.Migrations .IsRequired(); }); + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTierTranslation", b => + { + b.HasOne("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTier", null) + .WithMany() + .HasForeignKey("MembershipTierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b => { b.HasOne("Socialize.Api.Modules.Organizations.Data.Organization", null) diff --git a/backend/src/Socialize.Api/Modules/Organizations/Data/OrganizationMembershipTier.cs b/backend/src/Socialize.Api/Modules/Organizations/Data/OrganizationMembershipTier.cs index 49db436d..a9f8f723 100644 --- a/backend/src/Socialize.Api/Modules/Organizations/Data/OrganizationMembershipTier.cs +++ b/backend/src/Socialize.Api/Modules/Organizations/Data/OrganizationMembershipTier.cs @@ -4,8 +4,6 @@ internal class OrganizationMembershipTier { public Guid Id { get; init; } public required string Key { get; set; } - public required string Name { get; set; } - public required string Description { get; set; } public int? MonthlyPriceCents { get; set; } public int? WorkspaceLimit { get; set; } public int? ActiveContentLimit { get; set; } diff --git a/backend/src/Socialize.Api/Modules/Organizations/Data/OrganizationMembershipTierSeed.cs b/backend/src/Socialize.Api/Modules/Organizations/Data/OrganizationMembershipTierSeed.cs index f53faa25..b8e48eaf 100644 --- a/backend/src/Socialize.Api/Modules/Organizations/Data/OrganizationMembershipTierSeed.cs +++ b/backend/src/Socialize.Api/Modules/Organizations/Data/OrganizationMembershipTierSeed.cs @@ -13,8 +13,6 @@ internal static class OrganizationMembershipTierSeed { Id = FreeId, Key = "free", - Name = "Free", - Description = "For trying Socialize on one real approval workflow.", MonthlyPriceCents = 0, WorkspaceLimit = 1, ActiveContentLimit = 3, @@ -26,8 +24,6 @@ internal static class OrganizationMembershipTierSeed { Id = FreelanceId, Key = "freelance", - Name = "Freelance", - Description = "For solo operators managing recurring client reviews.", MonthlyPriceCents = 1900, WorkspaceLimit = 3, ActiveContentLimit = 25, @@ -39,8 +35,6 @@ internal static class OrganizationMembershipTierSeed { Id = AgencyId, Key = "agency", - Name = "Agency", - Description = "For agencies that need repeatable client approval operations.", MonthlyPriceCents = 7900, WorkspaceLimit = 15, ActiveContentLimit = 250, @@ -52,8 +46,6 @@ internal static class OrganizationMembershipTierSeed { Id = EnterpriseId, Key = "enterprise", - Name = "Enterprise", - Description = "For larger organizations with governance and access needs.", MonthlyPriceCents = null, WorkspaceLimit = null, ActiveContentLimit = null, @@ -63,4 +55,72 @@ internal static class OrganizationMembershipTierSeed SortOrder = 40, }, ]; + + public static readonly OrganizationMembershipTierTranslation[] Translations = + [ + new() + { + Id = Guid.Parse("20000000-0000-0001-0000-000000000001"), + MembershipTierId = FreeId, + Culture = "en", + Name = "Free", + Description = "For trying Socialize on one real approval workflow.", + }, + new() + { + Id = Guid.Parse("20000000-0000-0001-0000-000000000002"), + MembershipTierId = FreeId, + Culture = "fr", + Name = "Free", + Description = "Pour essayer Socialize sur un vrai workflow d'approbation.", + }, + new() + { + Id = Guid.Parse("20000000-0000-0001-0000-000000000003"), + MembershipTierId = FreelanceId, + Culture = "en", + Name = "Freelance", + Description = "For solo operators managing recurring client reviews.", + }, + new() + { + Id = Guid.Parse("20000000-0000-0001-0000-000000000004"), + MembershipTierId = FreelanceId, + Culture = "fr", + Name = "Freelance", + Description = "Pour les independants qui gerent des revisions client recurrentes.", + }, + new() + { + Id = Guid.Parse("20000000-0000-0001-0000-000000000005"), + MembershipTierId = AgencyId, + Culture = "en", + Name = "Agency", + Description = "For agencies that need repeatable client approval operations.", + }, + new() + { + Id = Guid.Parse("20000000-0000-0001-0000-000000000006"), + MembershipTierId = AgencyId, + Culture = "fr", + Name = "Agency", + Description = "Pour les agences qui veulent des operations d'approbation client repetables.", + }, + new() + { + Id = Guid.Parse("20000000-0000-0001-0000-000000000007"), + MembershipTierId = EnterpriseId, + Culture = "en", + Name = "Enterprise", + Description = "For larger organizations with governance and access needs.", + }, + new() + { + Id = Guid.Parse("20000000-0000-0001-0000-000000000008"), + MembershipTierId = EnterpriseId, + Culture = "fr", + Name = "Enterprise", + Description = "Pour les grandes organisations avec des besoins de gouvernance et d'acces.", + }, + ]; } diff --git a/backend/src/Socialize.Api/Modules/Organizations/Data/OrganizationMembershipTierTranslation.cs b/backend/src/Socialize.Api/Modules/Organizations/Data/OrganizationMembershipTierTranslation.cs new file mode 100644 index 00000000..04405e92 --- /dev/null +++ b/backend/src/Socialize.Api/Modules/Organizations/Data/OrganizationMembershipTierTranslation.cs @@ -0,0 +1,10 @@ +namespace Socialize.Api.Modules.Organizations.Data; + +internal class OrganizationMembershipTierTranslation +{ + public Guid Id { get; init; } + public Guid MembershipTierId { get; set; } + public required string Culture { get; set; } + public required string Name { get; set; } + public required string Description { get; set; } +} diff --git a/backend/src/Socialize.Api/Modules/Organizations/Data/OrganizationModelConfiguration.cs b/backend/src/Socialize.Api/Modules/Organizations/Data/OrganizationModelConfiguration.cs index 57801dad..92cdaa45 100644 --- a/backend/src/Socialize.Api/Modules/Organizations/Data/OrganizationModelConfiguration.cs +++ b/backend/src/Socialize.Api/Modules/Organizations/Data/OrganizationModelConfiguration.cs @@ -30,13 +30,26 @@ internal static class OrganizationModelConfiguration tier.ToTable("OrganizationMembershipTiers"); tier.HasKey(x => x.Id); tier.Property(x => x.Key).HasMaxLength(64).IsRequired(); - tier.Property(x => x.Name).HasMaxLength(128).IsRequired(); - tier.Property(x => x.Description).HasMaxLength(512).IsRequired(); tier.HasIndex(x => x.Key).IsUnique(); tier.HasIndex(x => x.SortOrder); tier.HasData(OrganizationMembershipTierSeed.Tiers); }); + modelBuilder.Entity(translation => + { + translation.ToTable("OrganizationMembershipTierTranslations"); + translation.HasKey(x => x.Id); + translation.Property(x => x.Culture).HasMaxLength(16).IsRequired(); + translation.Property(x => x.Name).HasMaxLength(128).IsRequired(); + translation.Property(x => x.Description).HasMaxLength(512).IsRequired(); + translation.HasIndex(x => new { x.MembershipTierId, x.Culture }).IsUnique(); + translation.HasOne() + .WithMany() + .HasForeignKey(x => x.MembershipTierId) + .OnDelete(DeleteBehavior.Cascade); + translation.HasData(OrganizationMembershipTierSeed.Translations); + }); + modelBuilder.Entity(membership => { membership.ToTable("OrganizationMemberships"); diff --git a/backend/src/Socialize.Api/Modules/Organizations/Handlers/CreateOrganization.cs b/backend/src/Socialize.Api/Modules/Organizations/Handlers/CreateOrganization.cs index bacc6641..ee930a3e 100644 --- a/backend/src/Socialize.Api/Modules/Organizations/Handlers/CreateOrganization.cs +++ b/backend/src/Socialize.Api/Modules/Organizations/Handlers/CreateOrganization.cs @@ -71,7 +71,11 @@ internal class CreateOrganizationHandler( OrganizationDto.FromOrganization( organization, OrganizationPermissionRules.GetPermissionsForRole(OrganizationRoles.Owner), - OrganizationMembershipTierDto.FromTier(membershipTier)), + await OrganizationMembershipTierLocalization.GetLocalizedTierDtoAsync( + dbContext, + membershipTier, + HttpContext, + ct)), StatusCodes.Status201Created, ct); } diff --git a/backend/src/Socialize.Api/Modules/Organizations/Handlers/GetOrganization.cs b/backend/src/Socialize.Api/Modules/Organizations/Handlers/GetOrganization.cs index 0bf9466e..efe86982 100644 --- a/backend/src/Socialize.Api/Modules/Organizations/Handlers/GetOrganization.cs +++ b/backend/src/Socialize.Api/Modules/Organizations/Handlers/GetOrganization.cs @@ -47,14 +47,17 @@ internal class GetOrganizationHandler( IReadOnlyCollection members = await GetMembersAsync(organizationId, ct); IReadOnlyCollection workspaces = await GetWorkspacesAsync(organizationId, ct); OrganizationMembershipTier membershipTier = await GetMembershipTierAsync(organization.MembershipTierId, ct); - OrganizationUsageDto usage = await GetUsageAsync(organization, membershipTier, ct); - IReadOnlyCollection availableMembershipTiers = await GetAvailableMembershipTiersAsync(ct); + OrganizationMembershipTierDto membershipTierDto = + await OrganizationMembershipTierLocalization.GetLocalizedTierDtoAsync(dbContext, membershipTier, HttpContext, ct); + OrganizationUsageDto usage = await GetUsageAsync(organization, membershipTier, membershipTierDto, ct); + IReadOnlyCollection availableMembershipTiers = + await OrganizationMembershipTierLocalization.GetLocalizedTierDtosAsync(dbContext, HttpContext, ct); await SendOkAsync( OrganizationDto.FromOrganization( organization, currentUserPermissions, - OrganizationMembershipTierDto.FromTier(membershipTier), + membershipTierDto, members, workspaces, usage, @@ -107,6 +110,7 @@ internal class GetOrganizationHandler( private async Task GetUsageAsync( Organization organization, OrganizationMembershipTier membershipTier, + OrganizationMembershipTierDto membershipTierDto, CancellationToken ct) { Guid[] workspaceIds = await dbContext.Workspaces @@ -138,7 +142,7 @@ internal class GetOrganizationHandler( return new OrganizationUsageDto( membershipTier.Key, - membershipTier.Name, + membershipTierDto.Name, [ new OrganizationUsageItemDto("users", userCount, membershipTier.MemberLimit), new OrganizationUsageItemDto("workspaces", workspaceIds.Length, membershipTier.WorkspaceLimit), @@ -155,18 +159,6 @@ internal class GetOrganizationHandler( .SingleAsync(tier => tier.Id == OrganizationMembershipTierSeed.FreeId, ct); } - private async Task> GetAvailableMembershipTiersAsync(CancellationToken ct) - { - List tiers = await dbContext.OrganizationMembershipTiers - .OrderBy(tier => tier.SortOrder) - .ThenBy(tier => tier.Name) - .ToListAsync(ct); - - return tiers - .Select(OrganizationMembershipTierDto.FromTier) - .ToArray(); - } - private async Task GetExternalReviewerCountAsync( IReadOnlyCollection workspaceIds, IReadOnlyCollection organizationMemberUserIds, diff --git a/backend/src/Socialize.Api/Modules/Organizations/Handlers/GetOrganizations.cs b/backend/src/Socialize.Api/Modules/Organizations/Handlers/GetOrganizations.cs index 81573aa2..782d0dac 100644 --- a/backend/src/Socialize.Api/Modules/Organizations/Handlers/GetOrganizations.cs +++ b/backend/src/Socialize.Api/Modules/Organizations/Handlers/GetOrganizations.cs @@ -26,17 +26,11 @@ internal class GetOrganizationsHandler( .OrderBy(organization => organization.Name) .ToListAsync(ct); - Guid[] membershipTierIds = organizations - .Select(organization => organization.MembershipTierId) - .Distinct() - .ToArray(); - List membershipTierModels = await dbContext.OrganizationMembershipTiers - .Where(tier => membershipTierIds.Contains(tier.Id)) - .ToListAsync(ct); - Dictionary membershipTiersById = membershipTierModels + Dictionary membershipTiersById = + (await OrganizationMembershipTierLocalization.GetLocalizedTierDtosAsync(dbContext, HttpContext, ct)) .ToDictionary( tier => tier.Id, - OrganizationMembershipTierDto.FromTier); + tier => tier); List response = []; foreach (Organization organization in organizations) diff --git a/backend/src/Socialize.Api/Modules/Organizations/Handlers/ListOrganizationMembershipTiers.cs b/backend/src/Socialize.Api/Modules/Organizations/Handlers/ListOrganizationMembershipTiers.cs index f97b8dd3..a6c7805f 100644 --- a/backend/src/Socialize.Api/Modules/Organizations/Handlers/ListOrganizationMembershipTiers.cs +++ b/backend/src/Socialize.Api/Modules/Organizations/Handlers/ListOrganizationMembershipTiers.cs @@ -1,7 +1,6 @@ using FastEndpoints; -using Microsoft.EntityFrameworkCore; using Socialize.Api.Data; -using Socialize.Api.Modules.Organizations.Data; +using Socialize.Api.Modules.Organizations.Services; namespace Socialize.Api.Modules.Organizations.Handlers; @@ -16,15 +15,8 @@ internal class ListOrganizationMembershipTiersHandler(AppDbContext dbContext) public override async Task HandleAsync(CancellationToken ct) { - List tierModels = await dbContext.OrganizationMembershipTiers - .OrderBy(tier => tier.SortOrder) - .ThenBy(tier => tier.Name) - .ToListAsync(ct); - - OrganizationMembershipTierDto[] tiers = tierModels - .Select(OrganizationMembershipTierDto.FromTier) - .ToArray(); - - await SendOkAsync(tiers, ct); + await SendOkAsync( + await OrganizationMembershipTierLocalization.GetLocalizedTierDtosAsync(dbContext, HttpContext, ct), + ct); } } diff --git a/backend/src/Socialize.Api/Modules/Organizations/Handlers/OrganizationDtos.cs b/backend/src/Socialize.Api/Modules/Organizations/Handlers/OrganizationDtos.cs index 06ca7131..1296abcc 100644 --- a/backend/src/Socialize.Api/Modules/Organizations/Handlers/OrganizationDtos.cs +++ b/backend/src/Socialize.Api/Modules/Organizations/Handlers/OrganizationDtos.cs @@ -62,13 +62,15 @@ internal record OrganizationMembershipTierDto( bool IsCustom, int SortOrder) { - public static OrganizationMembershipTierDto FromTier(OrganizationMembershipTier tier) + public static OrganizationMembershipTierDto FromTier( + OrganizationMembershipTier tier, + OrganizationMembershipTierTranslation translation) { return new OrganizationMembershipTierDto( tier.Id, tier.Key, - tier.Name, - tier.Description, + translation.Name, + translation.Description, tier.MonthlyPriceCents, tier.WorkspaceLimit, tier.ActiveContentLimit, diff --git a/backend/src/Socialize.Api/Modules/Organizations/Handlers/UpdateOrganizationMembershipTier.cs b/backend/src/Socialize.Api/Modules/Organizations/Handlers/UpdateOrganizationMembershipTier.cs index d10b23c4..20a0482e 100644 --- a/backend/src/Socialize.Api/Modules/Organizations/Handlers/UpdateOrganizationMembershipTier.cs +++ b/backend/src/Socialize.Api/Modules/Organizations/Handlers/UpdateOrganizationMembershipTier.cs @@ -74,7 +74,11 @@ internal class UpdateOrganizationMembershipTierHandler( OrganizationDto.FromOrganization( organization, currentUserPermissions, - OrganizationMembershipTierDto.FromTier(membershipTier)), + await OrganizationMembershipTierLocalization.GetLocalizedTierDtoAsync( + dbContext, + membershipTier, + HttpContext, + ct)), ct); } } diff --git a/backend/src/Socialize.Api/Modules/Organizations/Services/OrganizationMembershipTierLocalization.cs b/backend/src/Socialize.Api/Modules/Organizations/Services/OrganizationMembershipTierLocalization.cs new file mode 100644 index 00000000..ad4853d2 --- /dev/null +++ b/backend/src/Socialize.Api/Modules/Organizations/Services/OrganizationMembershipTierLocalization.cs @@ -0,0 +1,142 @@ +using Microsoft.EntityFrameworkCore; +using System.Globalization; +using Socialize.Api.Data; +using Socialize.Api.Modules.Organizations.Data; +using Socialize.Api.Modules.Organizations.Handlers; + +namespace Socialize.Api.Modules.Organizations.Services; + +internal static class OrganizationMembershipTierLocalization +{ + private const string DefaultCulture = "en"; + + private static readonly HashSet SupportedCultures = new(StringComparer.OrdinalIgnoreCase) + { + "en", + "fr", + }; + + public static string GetRequestedCulture(HttpContext httpContext) + { + ArgumentNullException.ThrowIfNull(httpContext); + + string header = httpContext.Request.Headers.AcceptLanguage.ToString(); + if (string.IsNullOrWhiteSpace(header)) + { + return DefaultCulture; + } + + return header + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(ParseLanguagePreference) + .Where(preference => preference.Culture is not null) + .OrderByDescending(preference => preference.Quality) + .Select(preference => preference.Culture!) + .FirstOrDefault(IsSupportedCulture) + ?? DefaultCulture; + } + + public static async Task> GetLocalizedTierDtosAsync( + AppDbContext dbContext, + HttpContext httpContext, + CancellationToken ct) + { + ArgumentNullException.ThrowIfNull(dbContext); + ArgumentNullException.ThrowIfNull(httpContext); + + string culture = GetRequestedCulture(httpContext); + + List tiers = await dbContext.OrganizationMembershipTiers + .OrderBy(tier => tier.SortOrder) + .ThenBy(tier => tier.Key) + .ToListAsync(ct); + + Guid[] tierIds = tiers + .Select(tier => tier.Id) + .ToArray(); + + List translations = await dbContext.OrganizationMembershipTierTranslations + .Where(translation => + tierIds.Contains(translation.MembershipTierId) && + (translation.Culture == culture || translation.Culture == DefaultCulture)) + .ToListAsync(ct); + + return tiers + .Select(tier => OrganizationMembershipTierDto.FromTier( + tier, + SelectTranslation(translations, tier.Id, culture))) + .ToArray(); + } + + public static async Task GetLocalizedTierDtoAsync( + AppDbContext dbContext, + OrganizationMembershipTier tier, + HttpContext httpContext, + CancellationToken ct) + { + ArgumentNullException.ThrowIfNull(dbContext); + ArgumentNullException.ThrowIfNull(tier); + ArgumentNullException.ThrowIfNull(httpContext); + + string culture = GetRequestedCulture(httpContext); + + List translations = await dbContext.OrganizationMembershipTierTranslations + .Where(translation => + translation.MembershipTierId == tier.Id && + (translation.Culture == culture || translation.Culture == DefaultCulture)) + .ToListAsync(ct); + + return OrganizationMembershipTierDto.FromTier(tier, SelectTranslation(translations, tier.Id, culture)); + } + + private static OrganizationMembershipTierTranslation SelectTranslation( + IReadOnlyCollection translations, + Guid membershipTierId, + string culture) + { + return translations.FirstOrDefault(translation => + translation.MembershipTierId == membershipTierId && + string.Equals(translation.Culture, culture, StringComparison.OrdinalIgnoreCase)) + ?? translations.First(translation => + translation.MembershipTierId == membershipTierId && + string.Equals(translation.Culture, DefaultCulture, StringComparison.OrdinalIgnoreCase)); + } + + private static LanguagePreference ParseLanguagePreference(string value) + { + string[] parts = value.Split(';', StringSplitOptions.TrimEntries); + string? culture = NormalizeCulture(parts[0]); + decimal quality = 1m; + + foreach (string part in parts.Skip(1)) + { + if (part.StartsWith("q=", StringComparison.OrdinalIgnoreCase) && + decimal.TryParse(part[2..], NumberStyles.Number, CultureInfo.InvariantCulture, out decimal parsedQuality)) + { + quality = parsedQuality; + } + } + + return new LanguagePreference(culture, quality); + } + + private static string? NormalizeCulture(string value) + { + if (string.IsNullOrWhiteSpace(value) || value == "*") + { + return null; + } + + string neutralCulture = value.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)[0]; + + return SupportedCultures.FirstOrDefault(culture => + string.Equals(culture, neutralCulture, StringComparison.OrdinalIgnoreCase)); + } + + private static bool IsSupportedCulture(string culture) + { + return SupportedCultures.Contains(culture); + } + + private sealed record LanguagePreference(string? Culture, decimal Quality); +} diff --git a/docs/TASKS/organizations/009-localized-membership-tier-display.md b/docs/TASKS/organizations/009-localized-membership-tier-display.md new file mode 100644 index 00000000..f0676d60 --- /dev/null +++ b/docs/TASKS/organizations/009-localized-membership-tier-display.md @@ -0,0 +1,41 @@ +# Task: Localized organization membership tier display + +## Feature Spec + +`docs/FEATURES/organizations.md` + +## Goal + +Move membership tier display names and descriptions into localized database rows so API consumers receive tier copy in the requested language with English fallback. + +## Scope + +- Add membership tier translation persistence for `en` and `fr`. +- Remove tier `Name` and `Description` from the base tier model. +- Select tier translation from `Accept-Language`, fallback to English. +- Send the active frontend locale as `Accept-Language` on API requests. +- Regenerate EF migration, OpenAPI, and frontend schema. +- Audit similar database-backed display strings that are currently unlocalized. + +## Validation + +```bash +dotnet ef migrations add LocalizeOrganizationMembershipTiers --project backend/src/Socialize.Api/Socialize.Api.csproj --startup-project backend/src/Socialize.Api/Socialize.Api.csproj +dotnet test backend/Socialize.slnx +cd frontend && npm run build +./scripts/update-openapi.sh +``` + +## Done When + +- [x] Tier display names and descriptions are stored per culture. +- [x] Tier APIs return localized `name` and `description`. +- [x] English is used when the requested locale is missing. +- [x] Frontend API requests include the current locale. +- [x] Similar unlocalized persisted display strings are identified. + +## Localization Audit + +- Calendar catalog seed rows still store curated `Title` and `Description` as single-language strings. This looks similar to membership tiers because the copy is product-owned catalog metadata, not user-authored content. +- Backend-generated notification/activity messages are persisted as English sentences. They are event history, but they are still user-visible generated copy. +- Most other `Name`, `Title`, and `Description` fields found in modules are user-authored domain content, imported calendar event data, identifiers, roles, statuses, or test data and should not be translated by the platform. diff --git a/frontend/src/plugins/api.js b/frontend/src/plugins/api.js index 84783e2d..d5684469 100644 --- a/frontend/src/plugins/api.js +++ b/frontend/src/plugins/api.js @@ -1,6 +1,7 @@ import axios from 'axios'; import { useAuthStore } from '@/features/auth/stores/authStore.js'; import config from '@/config.js'; +import { i18n } from '@/plugins/i18n.js'; export function useClient() { const client = axios.create({ @@ -36,6 +37,8 @@ export function useClient() { config.headers.Authorization = `Bearer ${authStore.accessToken}`; } + config.headers['Accept-Language'] = i18n.global.locale.value ?? 'en'; + if (config.data instanceof FormData) { console.log(`Data is FormData, removing explicit Content-Type header for: ${config.method?.toUpperCase()} ${config.url}`); delete config.headers['Content-Type'];