feat: localize membership tier display
This commit is contained in:
@@ -22,6 +22,8 @@ internal class AppDbContext(
|
||||
{
|
||||
public DbSet<Organization> Organizations => Set<Organization>();
|
||||
public DbSet<OrganizationMembershipTier> OrganizationMembershipTiers => Set<OrganizationMembershipTier>();
|
||||
public DbSet<OrganizationMembershipTierTranslation> OrganizationMembershipTierTranslations =>
|
||||
Set<OrganizationMembershipTierTranslation>();
|
||||
public DbSet<OrganizationMembership> OrganizationMemberships => Set<OrganizationMembership>();
|
||||
public DbSet<Workspace> Workspaces => Set<Workspace>();
|
||||
public DbSet<WorkspaceInvite> WorkspaceInvites => Set<WorkspaceInvite>();
|
||||
|
||||
2382
backend/src/Socialize.Api/Migrations/20260508003746_LocalizeOrganizationMembershipTiers.Designer.cs
generated
Normal file
2382
backend/src/Socialize.Api/Migrations/20260508003746_LocalizeOrganizationMembershipTiers.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
internal partial class LocalizeOrganizationMembershipTiers : Migration
|
||||
{
|
||||
private static readonly string[] MembershipTierTranslationSeedColumns =
|
||||
[
|
||||
"Id",
|
||||
"Culture",
|
||||
"Description",
|
||||
"MembershipTierId",
|
||||
"Name"
|
||||
];
|
||||
|
||||
private static readonly string[] MembershipTierColumnsToRestore =
|
||||
[
|
||||
"Description",
|
||||
"Name"
|
||||
];
|
||||
|
||||
/// <inheritdoc />
|
||||
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<Guid>(type: "uuid", nullable: false),
|
||||
MembershipTierId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Culture = table.Column<string>(type: "character varying(16)", maxLength: 16, nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
Description = table.Column<string>(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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OrganizationMembershipTierTranslations");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Description",
|
||||
table: "OrganizationMembershipTiers",
|
||||
type: "character varying(512)",
|
||||
maxLength: 512,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1724,11 +1724,6 @@ namespace Socialize.Api.Migrations
|
||||
b.Property<int?>("ActiveContentLimit")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<int?>("ExternalReviewerLimit")
|
||||
.HasColumnType("integer");
|
||||
|
||||
@@ -1746,11 +1741,6 @@ namespace Socialize.Api.Migrations
|
||||
b.Property<int?>("MonthlyPriceCents")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<int>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Culture")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<Guid>("MembershipTierId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("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<Guid>("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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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<OrganizationMembershipTierTranslation>(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<OrganizationMembershipTier>()
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.MembershipTierId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
translation.HasData(OrganizationMembershipTierSeed.Translations);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<OrganizationMembership>(membership =>
|
||||
{
|
||||
membership.ToTable("OrganizationMemberships");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -47,14 +47,17 @@ internal class GetOrganizationHandler(
|
||||
IReadOnlyCollection<OrganizationMemberDto> members = await GetMembersAsync(organizationId, ct);
|
||||
IReadOnlyCollection<WorkspaceDto> workspaces = await GetWorkspacesAsync(organizationId, ct);
|
||||
OrganizationMembershipTier membershipTier = await GetMembershipTierAsync(organization.MembershipTierId, ct);
|
||||
OrganizationUsageDto usage = await GetUsageAsync(organization, membershipTier, ct);
|
||||
IReadOnlyCollection<OrganizationMembershipTierDto> availableMembershipTiers = await GetAvailableMembershipTiersAsync(ct);
|
||||
OrganizationMembershipTierDto membershipTierDto =
|
||||
await OrganizationMembershipTierLocalization.GetLocalizedTierDtoAsync(dbContext, membershipTier, HttpContext, ct);
|
||||
OrganizationUsageDto usage = await GetUsageAsync(organization, membershipTier, membershipTierDto, ct);
|
||||
IReadOnlyCollection<OrganizationMembershipTierDto> 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<OrganizationUsageDto> 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<IReadOnlyCollection<OrganizationMembershipTierDto>> GetAvailableMembershipTiersAsync(CancellationToken ct)
|
||||
{
|
||||
List<OrganizationMembershipTier> tiers = await dbContext.OrganizationMembershipTiers
|
||||
.OrderBy(tier => tier.SortOrder)
|
||||
.ThenBy(tier => tier.Name)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return tiers
|
||||
.Select(OrganizationMembershipTierDto.FromTier)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private async Task<int> GetExternalReviewerCountAsync(
|
||||
IReadOnlyCollection<Guid> workspaceIds,
|
||||
IReadOnlyCollection<Guid> organizationMemberUserIds,
|
||||
|
||||
@@ -26,17 +26,11 @@ internal class GetOrganizationsHandler(
|
||||
.OrderBy(organization => organization.Name)
|
||||
.ToListAsync(ct);
|
||||
|
||||
Guid[] membershipTierIds = organizations
|
||||
.Select(organization => organization.MembershipTierId)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
List<OrganizationMembershipTier> membershipTierModels = await dbContext.OrganizationMembershipTiers
|
||||
.Where(tier => membershipTierIds.Contains(tier.Id))
|
||||
.ToListAsync(ct);
|
||||
Dictionary<Guid, OrganizationMembershipTierDto> membershipTiersById = membershipTierModels
|
||||
Dictionary<Guid, OrganizationMembershipTierDto> membershipTiersById =
|
||||
(await OrganizationMembershipTierLocalization.GetLocalizedTierDtosAsync(dbContext, HttpContext, ct))
|
||||
.ToDictionary(
|
||||
tier => tier.Id,
|
||||
OrganizationMembershipTierDto.FromTier);
|
||||
tier => tier);
|
||||
|
||||
List<OrganizationDto> response = [];
|
||||
foreach (Organization organization in organizations)
|
||||
|
||||
@@ -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<OrganizationMembershipTier> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -74,7 +74,11 @@ internal class UpdateOrganizationMembershipTierHandler(
|
||||
OrganizationDto.FromOrganization(
|
||||
organization,
|
||||
currentUserPermissions,
|
||||
OrganizationMembershipTierDto.FromTier(membershipTier)),
|
||||
await OrganizationMembershipTierLocalization.GetLocalizedTierDtoAsync(
|
||||
dbContext,
|
||||
membershipTier,
|
||||
HttpContext,
|
||||
ct)),
|
||||
ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string> 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<IReadOnlyCollection<OrganizationMembershipTierDto>> GetLocalizedTierDtosAsync(
|
||||
AppDbContext dbContext,
|
||||
HttpContext httpContext,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dbContext);
|
||||
ArgumentNullException.ThrowIfNull(httpContext);
|
||||
|
||||
string culture = GetRequestedCulture(httpContext);
|
||||
|
||||
List<OrganizationMembershipTier> tiers = await dbContext.OrganizationMembershipTiers
|
||||
.OrderBy(tier => tier.SortOrder)
|
||||
.ThenBy(tier => tier.Key)
|
||||
.ToListAsync(ct);
|
||||
|
||||
Guid[] tierIds = tiers
|
||||
.Select(tier => tier.Id)
|
||||
.ToArray();
|
||||
|
||||
List<OrganizationMembershipTierTranslation> 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<OrganizationMembershipTierDto> GetLocalizedTierDtoAsync(
|
||||
AppDbContext dbContext,
|
||||
OrganizationMembershipTier tier,
|
||||
HttpContext httpContext,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dbContext);
|
||||
ArgumentNullException.ThrowIfNull(tier);
|
||||
ArgumentNullException.ThrowIfNull(httpContext);
|
||||
|
||||
string culture = GetRequestedCulture(httpContext);
|
||||
|
||||
List<OrganizationMembershipTierTranslation> 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<OrganizationMembershipTierTranslation> 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);
|
||||
}
|
||||
Reference in New Issue
Block a user