From 67da921bce540d6341a162c88a3416351bdda3a1 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 19 Sep 2026 21:08:10 +0100 Subject: [PATCH 01/66] Blog Post Id --- Makefile | 4 + src/Yavsc.Org/Helpers/AsciiDocTagHelper.cs | 2 +- .../20260919195608_blogPostId.Designer.cs | 4714 +++++++++++++++++ .../Migrations/20260919195608_blogPostId.cs | 52 + .../ApplicationDbContextModelSnapshot.cs | 10 +- src/Yavsc.Org/Views/Blogspot/Details.cshtml | 20 +- src/Yavsc.Org/Views/Blogspot/Index.cshtml | 4 +- src/Yavsc.Org/Views/Blogspot/Title.cshtml | 8 +- src/Yavsc.Org/Views/HairCutCommand/CGV.cshtml | 12 +- src/Yavsc.Org/Views/Home/About.pt.cshtml | 32 +- src/Yavsc.Org/Views/Home/AboutAccess.cshtml | 10 +- src/Yavsc.Org/Views/Home/AboutMarkdown.cshtml | 4 +- src/Yavsc.Org/Views/Home/CGV.cshtml | 12 +- src/Yavsc.Org/Views/Home/Privacy.cshtml | 6 +- src/Yavsc.Org/Views/Home/Todo.cshtml | 8 +- .../Shared/Components/Comment/Default.cshtml | 8 +- src/Yavsc.Org/Views/Shared/_Layout.cshtml | 2 +- src/Yavsc.Server/Helpers/UserHelpers.cs | 6 +- src/Yavsc.Server/Models/Blog/BlogPost.cs | 5 + .../Models/Blog/BlogspotPublication.cs | 8 +- src/Yavsc.Server/Services/BlogSpotService.cs | 36 +- .../Services/PermissionHandler.cs | 2 +- 22 files changed, 4869 insertions(+), 96 deletions(-) create mode 100644 src/Yavsc.Org/Migrations/20260919195608_blogPostId.Designer.cs create mode 100644 src/Yavsc.Org/Migrations/20260919195608_blogPostId.cs diff --git a/Makefile b/Makefile index d99fe5e5e..e883088c2 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,10 @@ all: clean: dotnet clean -c $(CONFIG) +brutal_clean: + @find -name "obj" -exec rm -rf {} \; + @find -name "bin" -exec rm -rf {} \; + src/Yavsc/bin/output/wwwroot: dotnet --project src/Yavsc.Org/Yavsc.Org.csproj publish diff --git a/src/Yavsc.Org/Helpers/AsciiDocTagHelper.cs b/src/Yavsc.Org/Helpers/AsciiDocTagHelper.cs index 96391990a..778cf5cfe 100644 --- a/src/Yavsc.Org/Helpers/AsciiDocTagHelper.cs +++ b/src/Yavsc.Org/Helpers/AsciiDocTagHelper.cs @@ -17,8 +17,8 @@ namespace Yavsc.Helpers if (string.IsNullOrWhiteSpace(text)) return; - try + { if (context.AllAttributes.ContainsName("summary")) { diff --git a/src/Yavsc.Org/Migrations/20260919195608_blogPostId.Designer.cs b/src/Yavsc.Org/Migrations/20260919195608_blogPostId.Designer.cs new file mode 100644 index 000000000..903838f1d --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260919195608_blogPostId.Designer.cs @@ -0,0 +1,4714 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Yavsc.Models; + +#nullable disable + +namespace Yavsc.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260919195608_blogPostId")] + partial class blogPostId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("AllowedAccessTokenSigningAlgorithms") + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("LastAccessed") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ApiResources"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceSecrets"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Emphasize") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("ApiScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ScopeId") + .HasColumnType("integer"); + + b.Property("ScopeId1") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScopeId"); + + b.HasIndex("ScopeId1"); + + b.ToTable("ApiScopeClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("ScopeId") + .HasColumnType("integer"); + + b.Property("ScopeId1") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScopeId"); + + b.HasIndex("ScopeId1"); + + b.ToTable("ApiScopeProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("AbsoluteRefreshTokenLifetime") + .HasColumnType("integer"); + + b.Property("AccessTokenLifetime") + .HasColumnType("integer"); + + b.Property("AccessTokenType") + .HasColumnType("integer"); + + b.Property("AllowAccessTokensViaBrowser") + .HasColumnType("boolean"); + + b.Property("AllowOfflineAccess") + .HasColumnType("boolean"); + + b.Property("AllowPlainTextPkce") + .HasColumnType("boolean"); + + b.Property("AllowRememberConsent") + .HasColumnType("boolean"); + + b.Property("AllowedIdentityTokenSigningAlgorithms") + .HasColumnType("text"); + + b.Property("AlwaysIncludeUserClaimsInIdToken") + .HasColumnType("boolean"); + + b.Property("AlwaysSendClientClaims") + .HasColumnType("boolean"); + + b.Property("AuthorizationCodeLifetime") + .HasColumnType("integer"); + + b.Property("BackChannelLogoutSessionRequired") + .HasColumnType("boolean"); + + b.Property("BackChannelLogoutUri") + .HasColumnType("text"); + + b.Property("ClientClaimsPrefix") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ClientName") + .HasColumnType("text"); + + b.Property("ClientUri") + .HasColumnType("text"); + + b.Property("ConsentLifetime") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DeviceCodeLifetime") + .HasColumnType("integer"); + + b.Property("EnableLocalLogin") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("FrontChannelLogoutSessionRequired") + .HasColumnType("boolean"); + + b.Property("FrontChannelLogoutUri") + .HasColumnType("text"); + + b.Property("IdentityTokenLifetime") + .HasColumnType("integer"); + + b.Property("IncludeJwtId") + .HasColumnType("boolean"); + + b.Property("LastAccessed") + .HasColumnType("timestamp with time zone"); + + b.Property("LogoUri") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("PairWiseSubjectSalt") + .HasColumnType("text"); + + b.Property("ProtocolType") + .HasColumnType("text"); + + b.Property("RefreshTokenExpiration") + .HasColumnType("integer"); + + b.Property("RefreshTokenUsage") + .HasColumnType("integer"); + + b.Property("RequireClientSecret") + .HasColumnType("boolean"); + + b.Property("RequireConsent") + .HasColumnType("boolean"); + + b.Property("RequirePkce") + .HasColumnType("boolean"); + + b.Property("RequireRequestObject") + .HasColumnType("boolean"); + + b.Property("SlidingRefreshTokenLifetime") + .HasColumnType("integer"); + + b.Property("UpdateAccessTokenClaimsOnRefresh") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.Property("UserCodeType") + .HasColumnType("text"); + + b.Property("UserSsoLifetime") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Clients"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Origin") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientCorsOrigins"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("GrantType") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientGrantTypes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientIdPRestrictions"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("PostLogoutRedirectUri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientPostLogoutRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("RedirectUri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientSecrets"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.DeviceFlowCodes", b => + { + b.Property("UserCode") + .HasColumnType("text"); + + b.Property("DeviceCode") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("SubjectId") + .HasColumnType("text"); + + b.HasKey("UserCode", "DeviceCode"); + + b.ToTable("DeviceFlowCodes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Emphasize") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("IdentityResources"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.PersistedGrant", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ConsumedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("SubjectId") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Key"); + + b.ToTable("PersistedGrants"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + 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("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") + .IsRequired() + .HasColumnType("text"); + + 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") + .IsRequired() + .HasColumnType("text"); + + 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") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + 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("Yavsc.Abstract.Identity.ClientProviderInfo", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("BillingAddressId") + .HasColumnType("bigint"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("ClientProviderInfo"); + }); + + modelBuilder.Entity("Yavsc.Abstract.Models.Messaging.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Target") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("body") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("click_action") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("color") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("icon") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("exclam"); + + b.Property("sound") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("tag") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Notification"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TargetId"); + + b.ToTable("Ban"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("BlackListed"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.Property("CircleId") + .HasColumnType("bigint"); + + b.Property("BlogPostId") + .HasColumnType("bigint"); + + b.HasKey("CircleId", "BlogPostId"); + + b.HasIndex("BlogPostId"); + + b.ToTable("CircleAuthorizationToBlogPost"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToFile", b => + { + b.Property("CircleId") + .HasColumnType("bigint"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Access") + .HasColumnType("smallint"); + + b.HasKey("CircleId", "Path", "OwnerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("CircleAuthorizationToFile"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ContactCredits") + .HasColumnType("bigint"); + + b.Property("Credits") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.ToTable("BankStatus"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("AllowMonthlyEmail") + .HasColumnType("boolean"); + + b.Property("Avatar") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("/images/Users/icon_user.png"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("DedicatedGoogleCalendar") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("DiskQuota") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(524288000L); + + b.Property("DiskUsage") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FullName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxFileSize") + .HasColumnType("bigint"); + + 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("PostalAddressId") + .HasColumnType("bigint"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("PostalAddressId"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BalanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExecDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Impact") + .HasColumnType("numeric"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BalanceId"); + + b.ToTable("BalanceImpact"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("BIC") + .IsRequired() + .HasColumnType("text"); + + b.Property("BankCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("BankedKey") + .HasColumnType("integer"); + + b.Property("IBAN") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("WicketCode") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("BankIdentity"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("integer"); + + b.Property("Currency") + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("EstimateTemplateId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UnitaryCost") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EstimateId"); + + b.HasIndex("EstimateTemplateId"); + + b.ToTable("CommandLine"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttachedFilesString") + .IsRequired() + .HasColumnType("text"); + + b.Property("AttachedGraphicsString") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CommandId") + .HasColumnType("bigint"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProviderValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("CommandId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Estimates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EstimateTemplates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.ExceptionSIREN", b => + { + b.Property("SIREN") + .HasColumnType("text"); + + b.HasKey("SIREN"); + + b.ToTable("ExceptionsSIREN"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.NominativeServiceCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserModified") + .IsRequired() + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.ToTable("NominativeServiceCommands"); + + b.HasDiscriminator("Discriminator").HasValue("NominativeServiceCommand"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CapturedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CoordinateMax") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(10000); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignerId") + .IsRequired() + .HasColumnType("text"); + + b.PrimitiveCollection("Strokes") + .IsRequired() + .HasColumnType("integer[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SignerId"); + + b.HasIndex("EstimateId", "Type") + .IsUnique(); + + b.ToTable("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.Property("FileId") + .HasColumnType("bigint"); + + b.Property("PostId") + .HasColumnType("bigint"); + + b.HasKey("FileId", "PostId"); + + b.HasIndex("PostId"); + + b.ToTable("BlogAttachedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .HasMaxLength(56224) + .HasColumnType("character varying(56224)"); + + b.Property("AuthorId") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Photo") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("BlogSpot"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.Property("PostId") + .HasColumnType("bigint"); + + b.Property("TagId") + .HasColumnType("bigint"); + + b.HasKey("PostId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("BlogTag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ParentId") + .HasColumnType("bigint"); + + b.Property("ReceiverId") + .HasColumnType("bigint"); + + b.Property("UserCreated") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserModified") + .IsRequired() + .HasColumnType("text"); + + b.Property("Visible") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ParentId"); + + b.HasIndex("ReceiverId"); + + b.ToTable("Comment"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.UploadedFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("Length") + .HasColumnType("bigint"); + + b.Property("Path") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("UploadedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.Property("PostId") + .HasColumnType("bigint"); + + b.HasKey("PostId"); + + b.ToTable("blogSpotPublications"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.HasKey("OwnerId"); + + b.ToTable("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Reccurence") + .HasColumnType("integer"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScheduleOwnerId"); + + b.HasIndex("PeriodStart", "PeriodEnd"); + + b.ToTable("ScheduledEvent"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.Property("ConnectionId") + .HasColumnType("text"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Connected") + .HasColumnType("boolean"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("ConnectionId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("ChatConnection"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LatestJoinPart") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Name"); + + b.HasIndex("OwnerId"); + + b.ToTable("ChatRoom"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.Property("ChannelName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("ChannelName", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ChatRoomAccess"); + }); + + modelBuilder.Entity("Yavsc.Models.Cratie.Option", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("CodeScrutin") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code", "CodeScrutin"); + + b.ToTable("Option"); + }); + + modelBuilder.Entity("Yavsc.Models.Drawing.Color", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Blue") + .HasColumnType("smallint"); + + b.Property("Green") + .HasColumnType("smallint"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Red") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ActionDistance") + .HasColumnType("integer"); + + b.Property("CarePrice") + .HasColumnType("numeric"); + + b.Property("FlatFeeDiscount") + .HasColumnType("numeric"); + + b.Property("HalfBalayagePrice") + .HasColumnType("numeric"); + + b.Property("HalfBrushingPrice") + .HasColumnType("numeric"); + + b.Property("HalfColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfDefrisPrice") + .HasColumnType("numeric"); + + b.Property("HalfFoldingPrice") + .HasColumnType("numeric"); + + b.Property("HalfMechPrice") + .HasColumnType("numeric"); + + b.Property("HalfMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfPermanentPrice") + .HasColumnType("numeric"); + + b.Property("KidCutPrice") + .HasColumnType("numeric"); + + b.Property("LongBalayagePrice") + .HasColumnType("numeric"); + + b.Property("LongBrushingPrice") + .HasColumnType("numeric"); + + b.Property("LongColorPrice") + .HasColumnType("numeric"); + + b.Property("LongDefrisPrice") + .HasColumnType("numeric"); + + b.Property("LongFoldingPrice") + .HasColumnType("numeric"); + + b.Property("LongMechPrice") + .HasColumnType("numeric"); + + b.Property("LongMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("LongPermanentPrice") + .HasColumnType("numeric"); + + b.Property("ManBrushPrice") + .HasColumnType("numeric"); + + b.Property("ManCutPrice") + .HasColumnType("numeric"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.Property("ShampooPrice") + .HasColumnType("numeric"); + + b.Property("ShortBalayagePrice") + .HasColumnType("numeric"); + + b.Property("ShortBrushingPrice") + .HasColumnType("numeric"); + + b.Property("ShortColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortDefrisPrice") + .HasColumnType("numeric"); + + b.Property("ShortFoldingPrice") + .HasColumnType("numeric"); + + b.Property("ShortMechPrice") + .HasColumnType("numeric"); + + b.Property("ShortMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortPermanentPrice") + .HasColumnType("numeric"); + + b.Property("WomenHalfCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenLongCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenShortCutPrice") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.HasIndex("ScheduleOwnerId"); + + b.ToTable("BrusherProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Cares") + .HasColumnType("boolean"); + + b.Property("Cut") + .HasColumnType("boolean"); + + b.Property("Dressing") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Length") + .HasColumnType("integer"); + + b.Property("Shampoo") + .HasColumnType("boolean"); + + b.Property("Tech") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("HairPrestation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("QueryId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PrestationId"); + + b.HasIndex("QueryId"); + + b.ToTable("HairPrestationCollectionItem"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Brand") + .HasColumnType("text"); + + b.Property("ColorId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ColorId"); + + b.ToTable("HairTaint"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.Property("TaintId") + .HasColumnType("bigint"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.HasKey("TaintId", "PrestationId"); + + b.HasIndex("PrestationId"); + + b.ToTable("HairTaintInstance"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Evolution.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ShortName") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Feature"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(10240) + .HasColumnType("character varying(10240)"); + + b.Property("FeatureId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.ToTable("Bug"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("DeclarationDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("LOCALTIMESTAMP"); + + b.Property("DeviceOwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("LatestActivityUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Model") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DeviceId"); + + b.HasIndex("DeviceOwnerId"); + + b.ToTable("DeviceDeclaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("MatchExcerpt") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PatternId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("PatternId"); + + b.ToTable("DeclarationFlag"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("ModeratorId") + .HasColumnType("text"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("ModeratorId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ModerationLogs", t => + { + t.HasCheckConstraint("CK_ModerationLog_Immutable", "1=1"); + }); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.RegexAlertPattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Pattern") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("RegexAlertPatterns"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DeclarantTokenId") + .HasColumnType("uuid"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Sentiment") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrustTokenId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("SubmittedAt"); + + b.HasIndex("TrustTokenId"); + + b.ToTable("TrustDeclarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TokenSource") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TrustScore") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.ToTable("TrustTokens"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Depth") + .HasColumnType("numeric"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("numeric"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.Property("Weight") + .HasColumnType("numeric"); + + b.Property("Width") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContextId") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ContextId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("For") + .HasColumnType("smallint"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Sender") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Announce"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("NotificationId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "NotificationId"); + + b.HasIndex("NotificationId"); + + b.ToTable("DismissClicked"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Instrument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("Instrument"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("InstrumentId", "OwnerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("InstrumentRating"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.Property("OwnerProfileId") + .HasColumnType("text"); + + b.Property("DjSettingsUserId") + .HasColumnType("text"); + + b.Property("MusicLoverSettingsUserId") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("TendencyId") + .HasColumnType("bigint"); + + b.HasKey("OwnerProfileId"); + + b.HasIndex("DjSettingsUserId"); + + b.HasIndex("MusicLoverSettingsUserId"); + + b.HasIndex("TendencyId"); + + b.ToTable("MusicalPreference"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalTendency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SoundCloudId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("DjSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("InstrumentId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("Instrumentation"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("MusicLoverSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Property("CreationToken") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ExecutorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderReference") + .HasColumnType("text"); + + b.Property("PaypalPayerId") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("CreationToken"); + + b.HasIndex("ExecutorId"); + + b.ToTable("PayPalPayment"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Circle"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.Property("MemberId") + .HasColumnType("text"); + + b.Property("CircleId") + .HasColumnType("bigint"); + + b.HasKey("MemberId", "CircleId"); + + b.HasIndex("CircleId"); + + b.ToTable("CircleMembers"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("OwnerId", "UserId"); + + b.HasIndex("AddressId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Contact"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.Property("HRef") + .HasColumnType("text"); + + b.Property("Method") + .HasColumnType("text"); + + b.Property("BrusherProfileUserId") + .HasColumnType("text"); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("PayPalPaymentCreationToken") + .HasColumnType("text"); + + b.Property("Rel") + .HasColumnType("text"); + + b.HasKey("HRef", "Method"); + + b.HasIndex("BrusherProfileUserId"); + + b.HasIndex("PayPalPaymentCreationToken"); + + b.ToTable("HyperLink"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.PostalAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("Street1") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SiteSkills"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DifferedFileName") + .HasColumnType("text"); + + b.Property("MediaType") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Pitch") + .HasColumnType("text"); + + b.Property("SequenceNumber") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("LiveFlow"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Hidden") + .HasColumnType("boolean"); + + b.Property("Moderated") + .HasColumnType("boolean"); + + b.Property("ModeratorGroupName") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCode") + .HasColumnType("text"); + + b.Property("Photo") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SettingsClassName") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code"); + + b.HasIndex("ParentCode"); + + b.ToTable("Activities"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FormationSettingsUserId") + .HasColumnType("text"); + + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("WorkingForId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FormationSettingsUserId"); + + b.HasIndex("PerformerId"); + + b.HasIndex("WorkingForId"); + + b.ToTable("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionName") + .HasColumnType("text"); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.ToTable("CommandForm"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Country", b => + { + b.Property("Code") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Code"); + + b.ToTable("Countries"); + + b.HasData( + new + { + Code = "fr", + DisplayName = "France" + }, + new + { + Code = "en", + DisplayName = "England" + }, + new + { + Code = "pt", + DisplayName = "Portugal" + }); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.DictionnaireMetier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DomaineActiviteCode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Langue") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Nom") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("DomaineActiviteCode", "Langue", "Nom") + .IsUnique(); + + b.ToTable("DictionnaireMetier"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerCodeInputValidation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CountryCode") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("ErrorMessage") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RegularExpression") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("CountryCode"); + + b.ToTable("PerformerCodeInputValidations"); + + b.HasData( + new + { + Id = 1L, + CountryCode = "fr", + ErrorMessage = "Le code FR doit contenir entre 9 et 14 chiffres.", + RegularExpression = "^[0-9]{9,14}$" + }, + new + { + Id = 2L, + CountryCode = "en", + ErrorMessage = "Le code EN doit contenir entre 8 et 14 caracteres alphanumeriques.", + RegularExpression = "^[A-Za-z0-9]{8,14}$" + }, + new + { + Id = 3L, + CountryCode = "pt", + ErrorMessage = "Le code PT doit contenir exactement 9 chiffres.", + RegularExpression = "^[0-9]{9}$" + }); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("AcceptNotifications") + .HasColumnType("boolean"); + + b.Property("AcceptPublicContact") + .HasColumnType("boolean"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ExerciseCountryCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaxDailyCost") + .HasColumnType("integer"); + + b.Property("MinDailyCost") + .HasColumnType("integer"); + + b.Property("OrganizationAddressId") + .HasColumnType("bigint"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SIREN") + .IsRequired() + .HasColumnType("text"); + + b.Property("UseGeoLocalizationToReduceDistanceWithClients") + .HasColumnType("boolean"); + + b.Property("WebSite") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("PerformerId"); + + b.HasIndex("OrganizationAddressId"); + + b.ToTable("Performers"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("FormationSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.TermeMetier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateSoumission") + .HasColumnType("timestamp with time zone"); + + b.Property("DateValidation") + .HasColumnType("timestamp with time zone"); + + b.Property("Definition") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DictionnaireMetierId") + .HasColumnType("bigint"); + + b.Property("Langue") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Mot") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProposeParId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("StatutValidation") + .HasColumnType("integer"); + + b.Property("ValideParId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("ProposeParId"); + + b.HasIndex("ValideParId"); + + b.HasIndex("DictionnaireMetierId", "Langue", "Mot") + .IsUnique(); + + b.ToTable("TermeMetier"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.Property("DoesCode") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Weight") + .HasColumnType("integer"); + + b.HasKey("DoesCode", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("UserActivities"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.Calendar.Period", b => + { + b.Property("Start") + .HasColumnType("timestamp with time zone"); + + b.Property("End") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Start", "End"); + + b.ToTable("Period"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.EMailing.MailingTemplate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(65536) + .HasColumnType("character varying(65536)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplyToAddress") + .HasColumnType("text"); + + b.Property("ToSend") + .HasColumnType("integer"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("MailingTemplate"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProjectBuildConfiguration"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Branch") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("GitRepositoryReference"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("AdditionalInfo") + .IsRequired() + .HasColumnType("text"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("SelectedProfileUserId") + .HasColumnType("text"); + + b.HasIndex("LocationId"); + + b.HasIndex("PrestationId"); + + b.HasIndex("SelectedProfileUserId"); + + b.HasDiscriminator().HasValue("HairCutQuery"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.HasIndex("LocationId"); + + b.ToTable("NominativeServiceCommands", t => + { + t.Property("EventDate") + .HasColumnName("HairMultiCutQuery_EventDate"); + + t.Property("LocationId") + .HasColumnName("HairMultiCutQuery_LocationId"); + }); + + b.HasDiscriminator().HasValue("HairMultiCutQuery"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("LocationType") + .HasColumnType("integer"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasIndex("LocationId"); + + b.ToTable("NominativeServiceCommands", t => + { + t.Property("EventDate") + .HasColumnName("RdvQuery_EventDate"); + + t.Property("LocationId") + .HasColumnName("RdvQuery_LocationId"); + }); + + b.HasDiscriminator().HasValue("RdvQuery"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("GitId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasIndex("GitId"); + + b.HasDiscriminator().HasValue("Project"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("UserClaims") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Properties") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Scopes") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Secrets") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("UserClaims") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("Properties") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("Claims") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("AllowedCorsOrigins") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedGrantTypes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("IdentityProviderRestrictions") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("PostLogoutRedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("Properties") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("RedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedScopes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("ClientSecrets") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("UserClaims") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("Properties") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "TargetUser") + .WithMany() + .HasForeignKey("TargetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetUser"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("BlackList") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Target") + .WithMany("ACL") + .HasForeignKey("BlogPostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Circle", "Allowed") + .WithMany() + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Allowed"); + + b.Navigation("Target"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToFile", b => + { + b.HasOne("Yavsc.Models.Relationship.Circle", "Allowed") + .WithMany() + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Allowed"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithOne("AccountBalance") + .HasForeignKey("Yavsc.Models.AccountBalance", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "PostalAddress") + .WithMany() + .HasForeignKey("PostalAddressId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.HasOne("Yavsc.Models.AccountBalance", "Balance") + .WithMany() + .HasForeignKey("BalanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Balance"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("BankInfo") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", null) + .WithMany("Bill") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Billing.EstimateTemplate", null) + .WithMany("Bill") + .HasForeignKey("EstimateTemplateId"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Billing.NominativeServiceCommand", "Query") + .WithMany() + .HasForeignKey("CommandId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Owner"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.NominativeServiceCommand", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Regularization"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", "Estimate") + .WithMany("Signatures") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Signer") + .WithMany() + .HasForeignKey("SignerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Estimate"); + + b.Navigation("Signer"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.HasOne("Yavsc.Models.Blog.UploadedFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("Posts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Tags") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("BlogComments") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.Comment", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Comments") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Parent"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", null) + .WithMany("Events") + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Server.Models.Calendar.Period", "Period") + .WithMany() + .HasForeignKey("PeriodStart", "PeriodEnd"); + + b.Navigation("Period"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Connections") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Rooms") + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.HasOne("Yavsc.Models.Chat.ChatRoom", "Room") + .WithMany("Moderation") + .HasForeignKey("ChannelName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("RoomAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "BaseProfile") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseProfile"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairMultiCutQuery", "Query") + .WithMany("Prestations") + .HasForeignKey("QueryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.HasOne("Yavsc.Models.Drawing.Color", "Color") + .WithMany() + .HasForeignKey("ColorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany("Taints") + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairTaint", "Taint") + .WithMany() + .HasForeignKey("TaintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Taint"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.HasOne("Yavsc.Models.IT.Evolution.Feature", "False") + .WithMany() + .HasForeignKey("FeatureId"); + + b.Navigation("False"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "DeviceOwner") + .WithMany("DeviceDeclaration") + .HasForeignKey("DeviceOwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceOwner"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany("Flags") + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Kyc.RegexAlertPattern", "Pattern") + .WithMany() + .HasForeignKey("PatternId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + + b.Navigation("Pattern"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany() + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustToken", "Subject") + .WithMany("Declarations") + .HasForeignKey("TrustTokenId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Subject"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Services") + .HasForeignKey("ContextId"); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.HasOne("Yavsc.Abstract.Models.Messaging.Notification", "Notified") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notified"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Instrument") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Profile") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instrument"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.HasOne("Yavsc.Models.Musical.Profiles.DjSettings", null) + .WithMany("SoundColor") + .HasForeignKey("DjSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.Profiles.MusicLoverSettings", null) + .WithMany("SoundColor") + .HasForeignKey("MusicLoverSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.MusicalTendency", "MusicalTendency") + .WithMany() + .HasForeignKey("TendencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Tool") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tool"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Executor") + .WithMany() + .HasForeignKey("ExecutorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Executor"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Circles") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.HasOne("Yavsc.Models.Relationship.Circle", "Circle") + .WithMany("Members") + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Member") + .WithMany("Membership") + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Circle"); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.HasOne("Yavsc.Models.Relationship.PostalAddress", "PostalAddress") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Book") + .HasForeignKey("ApplicationUserId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", null) + .WithMany("Links") + .HasForeignKey("BrusherProfileUserId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", null) + .WithMany("Links") + .HasForeignKey("PayPalPaymentCreationToken"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentCode"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.HasOne("Yavsc.Models.Workflow.Profiles.FormationSettings", null) + .WithMany("CoWorking") + .HasForeignKey("FormationSettingsUserId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Performer") + .WithMany() + .HasForeignKey("PerformerId"); + + b.HasOne("Yavsc.Models.ApplicationUser", "WorkingFor") + .WithMany() + .HasForeignKey("WorkingForId"); + + b.Navigation("Performer"); + + b.Navigation("WorkingFor"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Forms") + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.DictionnaireMetier", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "DomaineActivite") + .WithMany() + .HasForeignKey("DomaineActiviteCode") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DomaineActivite"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerCodeInputValidation", b => + { + b.HasOne("Yavsc.Models.Workflow.Country", "Country") + .WithMany() + .HasForeignKey("CountryCode") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "OrganizationAddress") + .WithMany() + .HasForeignKey("OrganizationAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Performer") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationAddress"); + + b.Navigation("Performer"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.TermeMetier", b => + { + b.HasOne("Yavsc.Models.Workflow.DictionnaireMetier", "DictionnaireMetier") + .WithMany("Termes") + .HasForeignKey("DictionnaireMetierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "ProposePar") + .WithMany() + .HasForeignKey("ProposeParId"); + + b.HasOne("Yavsc.Models.ApplicationUser", "ValidePar") + .WithMany() + .HasForeignKey("ValideParId"); + + b.Navigation("DictionnaireMetier"); + + b.Navigation("ProposePar"); + + b.Navigation("ValidePar"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Does") + .WithMany() + .HasForeignKey("DoesCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany("Activity") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Does"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.HasOne("Yavsc.Server.Models.IT.Project", "TargetProject") + .WithMany("Configurations") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetProject"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId"); + + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", "SelectedProfile") + .WithMany() + .HasForeignKey("SelectedProfileUserId"); + + b.Navigation("Location"); + + b.Navigation("Prestation"); + + b.Navigation("SelectedProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.HasOne("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", "Repository") + .WithMany() + .HasForeignKey("GitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Repository"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b => + { + b.Navigation("Properties"); + + b.Navigation("Scopes"); + + b.Navigation("Secrets"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b => + { + b.Navigation("AllowedCorsOrigins"); + + b.Navigation("AllowedGrantTypes"); + + b.Navigation("AllowedScopes"); + + b.Navigation("Claims"); + + b.Navigation("ClientSecrets"); + + b.Navigation("IdentityProviderRestrictions"); + + b.Navigation("PostLogoutRedirectUris"); + + b.Navigation("Properties"); + + b.Navigation("RedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Navigation("AccountBalance"); + + b.Navigation("BankInfo"); + + b.Navigation("BlackList"); + + b.Navigation("BlogComments"); + + b.Navigation("Book"); + + b.Navigation("Circles"); + + b.Navigation("Connections"); + + b.Navigation("DeviceDeclaration"); + + b.Navigation("Membership"); + + b.Navigation("Posts"); + + b.Navigation("RoomAccess"); + + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Navigation("Bill"); + + b.Navigation("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Navigation("Bill"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Navigation("ACL"); + + b.Navigation("Comments"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Navigation("Moderation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Navigation("Taints"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Navigation("Declarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Navigation("Children"); + + b.Navigation("Forms"); + + b.Navigation("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.DictionnaireMetier", b => + { + b.Navigation("Termes"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Navigation("Activity"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Navigation("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.Navigation("Prestations"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.Navigation("Configurations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Yavsc.Org/Migrations/20260919195608_blogPostId.cs b/src/Yavsc.Org/Migrations/20260919195608_blogPostId.cs new file mode 100644 index 000000000..624571209 --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260919195608_blogPostId.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Yavsc.Migrations +{ + /// + public partial class blogPostId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_blogSpotPublications_BlogSpot_BlogpostId", + table: "blogSpotPublications"); + + migrationBuilder.RenameColumn( + name: "BlogpostId", + table: "blogSpotPublications", + newName: "PostId"); + + migrationBuilder.AddForeignKey( + name: "FK_blogSpotPublications_BlogSpot_PostId", + table: "blogSpotPublications", + column: "PostId", + principalTable: "BlogSpot", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_blogSpotPublications_BlogSpot_PostId", + table: "blogSpotPublications"); + + migrationBuilder.RenameColumn( + name: "PostId", + table: "blogSpotPublications", + newName: "BlogpostId"); + + migrationBuilder.AddForeignKey( + name: "FK_blogSpotPublications_BlogSpot_BlogpostId", + table: "blogSpotPublications", + column: "BlogpostId", + principalTable: "BlogSpot", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs index 84960c5bc..2ea15651e 100644 --- a/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1693,10 +1693,10 @@ namespace Yavsc.Migrations modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => { - b.Property("BlogpostId") + b.Property("PostId") .HasColumnType("bigint"); - b.HasKey("BlogpostId"); + b.HasKey("PostId"); b.ToTable("blogSpotPublications"); }); @@ -3958,13 +3958,13 @@ namespace Yavsc.Migrations modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => { - b.HasOne("Yavsc.Models.Blog.BlogPost", "BlogPost") + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") .WithMany() - .HasForeignKey("BlogpostId") + .HasForeignKey("PostId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("BlogPost"); + b.Navigation("Post"); }); modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => diff --git a/src/Yavsc.Org/Views/Blogspot/Details.cshtml b/src/Yavsc.Org/Views/Blogspot/Details.cshtml index dc0bea8a8..c41f6bac0 100644 --- a/src/Yavsc.Org/Views/Blogspot/Details.cshtml +++ b/src/Yavsc.Org/Views/Blogspot/Details.cshtml @@ -3,7 +3,7 @@ ViewBag.Title=Model.Title; } @section scripts { - + - - @{ await Html.RenderPartialAsync("_FSScriptsPartial"); } - -} - -

Blog post

- - -
- - -

@Model.Title

- -
@Model.Article
- -
-
-
- -
- -
- -
- - - -
-
- -
- -
- - - -
-
- -
- -
- - - -
-
- -
- -
- @await Component.InvokeAsync("CirclesControl", Model) -
-
- -
- -
- -
-
- -
-
- -
-
-
-
-@await Component.InvokeAsync(name: "Directory",arguments: new {subdir="."}) -
- @{ await Html.RenderPartialAsync("_PostFilesPartial"); } -
- diff --git a/src/Yavsc.Org/Views/Blogspot/Details.cshtml b/src/Yavsc.Org/Views/Blogspot/Details.cshtml index c41f6bac0..a78964c5e 100644 --- a/src/Yavsc.Org/Views/Blogspot/Details.cshtml +++ b/src/Yavsc.Org/Views/Blogspot/Details.cshtml @@ -98,8 +98,5 @@ $('#commentValidation').html(

Vous devez être identifié pour commenter.

} - @if ((await AuthorizationService.AuthorizeAsync(User, Model, new EditPermission())).Succeeded) { - Edit - } Back to List diff --git a/src/Yavsc.Org/Views/Blogspot/Edit.cshtml b/src/Yavsc.Org/Views/Blogspot/Edit.cshtml deleted file mode 100644 index 4645d45db..000000000 --- a/src/Yavsc.Org/Views/Blogspot/Edit.cshtml +++ /dev/null @@ -1,120 +0,0 @@ -@model BlogPostEditViewModel -@{ - ViewBag.Title = "Blog post edition"; -} - -@section header { - - - - - - - - @{ await Html.RenderPartialAsync("_FSScriptsPartial"); } - -} - -

Blog post

- - -
- - - - - -

@Model.Title

- -
-
- -
- @Html.HiddenFor(m=>m.Id) - -
- -
- - - -
-
- -
- -
- - - -
-
-
- -
- - - -
-
-
- -
- @await Component.InvokeAsync("CirclesControl", Model) -
-
- -
- -
- -
-
- -
-
- -
-
-
-
-@await Component.InvokeAsync("Directory","") - - -
- -
- @{ await Html.RenderPartialAsync("_PostFilesPartial"); } -
- diff --git a/src/Yavsc.Org/Views/Blogspot/Index.cshtml b/src/Yavsc.Org/Views/Blogspot/Index.cshtml index 9d27798ee..1179dba53 100644 --- a/src/Yavsc.Org/Views/Blogspot/Index.cshtml +++ b/src/Yavsc.Org/Views/Blogspot/Index.cshtml @@ -39,11 +39,6 @@ }

@ViewBag.Title

@ViewBag.StatusMessage

-@if (User.IsSignedIn()) { -

- Create a new article -

-}
@{ @@ -78,8 +73,6 @@ } @if ((await AuthorizationService.AuthorizeAsync(User, post, new EditPermission())).Succeeded) { - Edit - Delete } diff --git a/src/Yavsc.Org/Views/Blogspot/Title.cshtml b/src/Yavsc.Org/Views/Blogspot/Title.cshtml index 3a4dd8886..9421a5b01 100644 --- a/src/Yavsc.Org/Views/Blogspot/Title.cshtml +++ b/src/Yavsc.Org/Views/Blogspot/Title.cshtml @@ -3,10 +3,6 @@

@ViewBag.StatusMessage

-

- Poster au même titre -

- @foreach (var item in Model) { @@ -32,8 +28,7 @@ } @if ((await AuthorizationService.AuthorizeAsync(User, item, new EditPermission())).Succeeded) { -
  • Edit -
  • +
  • Delete
  • } diff --git a/src/Yavsc.Server/Models/Blog/BlogPostEditViewModel.cs b/src/Yavsc.Server/Models/Blog/BlogPostEditViewModel.cs index 152e2e9a6..5c3c1e225 100644 --- a/src/Yavsc.Server/Models/Blog/BlogPostEditViewModel.cs +++ b/src/Yavsc.Server/Models/Blog/BlogPostEditViewModel.cs @@ -6,7 +6,7 @@ namespace Yavsc.Models.Blog public BlogPostEditViewModel() { } - + public BlogPostEditViewModel(BlogPost post, bool publish) { Id = post.Id; From 605d8859178a8f34a660d7052eb1c448e91c1425 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 21 Sep 2026 11:12:20 +0100 Subject: [PATCH 13/66] Rewrite Blogspot Index view with skip/take pagination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renders Prev/Next pager from the Index(string id, int skip, int take) call profile, drops dead .sametitle JS, and replaces the redundant ReadPermission branch and Minutes>0 modifié check. Co-Authored-By: Claude Code --- src/Yavsc.Org/Views/Blogspot/Index.cshtml | 206 +++++++++++++++------- 1 file changed, 138 insertions(+), 68 deletions(-) diff --git a/src/Yavsc.Org/Views/Blogspot/Index.cshtml b/src/Yavsc.Org/Views/Blogspot/Index.cshtml index 1179dba53..9f29a9417 100644 --- a/src/Yavsc.Org/Views/Blogspot/Index.cshtml +++ b/src/Yavsc.Org/Views/Blogspot/Index.cshtml @@ -1,85 +1,155 @@ - @model IEnumerable @{ ViewBag.Title = "Blogs, l'index"; + + // Pagination reflects the Index(string id, int skip, int take) + // call profile. The service applies Skip/Take server-side, so + // the view only needs the current page params to build Prev/Next + // links — it does not receive a total count. + int currentSkip = Context.Request.Query.TryGetValue("skip", out var skipVal) && int.TryParse(skipVal, out var s) ? s : 0; + int currentTake = Context.Request.Query.TryGetValue("take", out var takeVal) && int.TryParse(takeVal, out var t) ? t : 25; + + var posts = Model.ToList(); + bool hasPrev = currentSkip > 0; + bool hasNext = posts.Count >= currentTake; + + int prevSkip = Math.Max(0, currentSkip - currentTake); + int nextSkip = currentSkip + currentTake; + + int maxSummaryLen = 256; } @section header { -} -@section scripts { - + .blog-index .post-meta { + font-size: .8rem; + color: var(--bs-secondary-color, #6c757d); + } + .blog-index .post-author img { + width: 1.25rem; + height: 1.25rem; + border-radius: 50%; + vertical-align: middle; + margin-right: .25rem; + } + .blog-index .actiongroup { + margin-top: .5rem; + display: flex; + gap: .5rem; + } + .blog-index .post-summary { + margin: .25rem 0; + } + .blog-index .post-summary > p:last-child { + margin-bottom: 0; + } + .blog-pager { + display: flex; + justify-content: space-between; + margin-top: 1.5rem; + } + }

    @ViewBag.Title

    @ViewBag.StatusMessage

    -
    -@{ - int maxTextLen = 75; - foreach (var post in Model) { -
    - - - -
    -

    @post.Title

    -
    -
    - - @post.Article - @Html.DisplayFor(m => post.Author) - - posté le @post.DateCreated.ToString("dddd d MMM yyyy à H:mm") - @if ((post.DateModified - post.DateCreated).Minutes > 0){ - @:- Modifié le @post.DateModified.ToString("dddd d MMM yyyy à H:mm") - } - -
    -
    - @if ((await AuthorizationService.AuthorizeAsync(User, post, new ReadPermission())).Succeeded) - { - Details - } - else - { - Details - } - @if ((await AuthorizationService.AuthorizeAsync(User, post, new EditPermission())).Succeeded) - { - Delete - - } -
    -
    - } +@if (!posts.Any()) +{ +

    Néant

    } -
    +else +{ +
    + @foreach (var post in posts) + { + var canEdit = (await AuthorizationService.AuthorizeAsync(User, post, new EditPermission())).Succeeded; +
    + @if (!string.IsNullOrEmpty(post.Photo)) + { + + @post.Title + + } +
    +

    + @post.Title +

    +
    + + @post.Article + +
    + +
    + Details + @if (canEdit) + { + Delete + } +
    +
    +
    + } +
    -@if(Model.Count()==0){

    Néant

    } + +} \ No newline at end of file From 1455ba5187b43862e8a0929078a252ef80ad8abe Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 21 Sep 2026 11:21:33 +0100 Subject: [PATCH 14/66] Restore tree-structured comments in Blog Details The $.psc.blogcomment widget (from the removed wwwroot/js/comment.js) provided reply-to-comment nesting. Rewrite Details.cshtml to drive the same /api/v1/blogcomments endpoint with ParentId, delegating reply clicks so they work across the whole nested tree. Render comment metadata server-side in the Comment view component template (the widget used to add it client-side) and Include Author so nested comments get their author instead of relying on the request-scoped identity map. Co-Authored-By: Claude Code --- .../ViewComponents/CommentViewComponent.cs | 5 +- src/Yavsc.Org/Views/Blogspot/Details.cshtml | 287 +++++++++++++----- .../Shared/Components/Comment/Default.cshtml | 38 ++- 3 files changed, 238 insertions(+), 92 deletions(-) diff --git a/src/Yavsc.Org/ViewComponents/CommentViewComponent.cs b/src/Yavsc.Org/ViewComponents/CommentViewComponent.cs index 6f85a953f..32b7a14b2 100644 --- a/src/Yavsc.Org/ViewComponents/CommentViewComponent.cs +++ b/src/Yavsc.Org/ViewComponents/CommentViewComponent.cs @@ -18,7 +18,10 @@ namespace Yavsc.ViewComponents } public async Task InvokeAsync(long id) { - var comment = await context.Comment.Include(c=>c.Children).FirstOrDefaultAsync(c => c.Id==id); + var comment = await context.Comment + .Include(c => c.Children) + .Include(c => c.Author) + .FirstOrDefaultAsync(c => c.Id==id); if (comment == null) throw new InvalidOperationException(); ViewBag.apictlr = "/api/v1/blogcomments"; diff --git a/src/Yavsc.Org/Views/Blogspot/Details.cshtml b/src/Yavsc.Org/Views/Blogspot/Details.cshtml index a78964c5e..3cf4ff9ee 100644 --- a/src/Yavsc.Org/Views/Blogspot/Details.cshtml +++ b/src/Yavsc.Org/Views/Blogspot/Details.cshtml @@ -1,102 +1,231 @@ @model BlogPost @{ - ViewBag.Title=Model.Title; + ViewBag.Title = Model.Title; } -@section scripts { - - - +@section header { } +@section scripts { + +} + +
    +
    + @if (!string.IsNullOrEmpty(Model.Photo)) + { + @Model.Title + } +
    +

    @Model.Title

    + +
    +
    + @Model.Article
    - -

    @Model.Title

    - - @Html.DisplayFor(m=>m.Author) - @Html.DisplayFor(m=>m.Article) -
    - @if (Model.Comments!=null) { - foreach (var comment in Model.Comments.Where(c=>c.ParentId==null)) { + @if (Model.Comments != null) + { + foreach (var comment in Model.Comments.Where(c => c.ParentId == null)) + { @await Component.InvokeAsync("Comment", new { id = comment.Id }) } }
    -@if (User.GetUserId()!=null) { -
    -
    - - - - - + + @if (User.GetUserId() != null) + { +
    +
    + + + + + +
    - -
    } - else { -

    Vous devez être identifié pour commenter.

    + else + { +

    Vous devez être identifié pour commenter.

    } Back to List - +
    \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Shared/Components/Comment/Default.cshtml b/src/Yavsc.Org/Views/Shared/Components/Comment/Default.cshtml index 4d9344eba..e83ea14c6 100644 --- a/src/Yavsc.Org/Views/Shared/Components/Comment/Default.cshtml +++ b/src/Yavsc.Org/Views/Shared/Components/Comment/Default.cshtml @@ -1,16 +1,30 @@ @model Comment -
    +
    +
    @Model.Article - - @if (Model.Children!=null) { -
    - @foreach (var comment in Model.Children) - { - @await this.Component.InvokeAsync("Comment", new { id = comment.Id}); - }
    -} -
    - + + — @(Model.Author?.UserName ?? "Anonyme"), + posté le @Model.DateCreated.ToString("dddd d MMM yyyy à H:mm") + + @if (User.GetUserId() != null) + { + + } +
    + @if (Model.Children != null) + { + foreach (var comment in Model.Children) + { + @await this.Component.InvokeAsync("Comment", new { id = comment.Id }) + } + } +
    +
    \ No newline at end of file From 625e062b4b6fa0635d8dd18c692d77fb7ba3e11e Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 21 Sep 2026 11:29:33 +0100 Subject: [PATCH 15/66] Fix white-on-white comment blocks in quartz theme Quartz sets white body text on a coloured page background, but --bs-tertiary-bg is near-white in its light variant. The .blogcomment background now uses a semi-transparent black overlay so white text stays legible, matching the dropdown hover fix. Co-Authored-By: Claude Code --- src/Yavsc.Org/Views/Blogspot/Details.cshtml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Yavsc.Org/Views/Blogspot/Details.cshtml b/src/Yavsc.Org/Views/Blogspot/Details.cshtml index 3cf4ff9ee..0b07b0d94 100644 --- a/src/Yavsc.Org/Views/Blogspot/Details.cshtml +++ b/src/Yavsc.Org/Views/Blogspot/Details.cshtml @@ -35,7 +35,11 @@ margin: .3em 0; padding: .5rem .75rem; border-left: 3px solid var(--bs-border-color, #dee2e6); - background: var(--bs-tertiary-bg, #f8f9fa); + /* Quartz ships white body text on a coloured page bg, so + use a semi-transparent black overlay rather than + --bs-tertiary-bg (which is near-white in the quartz + light variant and would render white-on-white). */ + background: rgba(0, 0, 0, 0.15); border-radius: .25rem; } .blogcomment .comment-body > :last-child { From 60c10781f05f42b2b104eecf1649b0aeda91eac9 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 21 Sep 2026 12:49:38 +0100 Subject: [PATCH 16/66] deploy in pre and prod : bug fixes --- contrib/Makefile | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/contrib/Makefile b/contrib/Makefile index 8f9beb420..4c7e0e6d6 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -7,8 +7,14 @@ APP_INSTALL_PREFIX=preprod- include .$(APP_INSTALL_ENV).env -generated/: - @mkdir -p $@ +reinstall: copy-binaries + @for project in $(APP_PROJECT_NAMES); do \ + echo "🚀 Starting $(APP_INSTALL_PREFIX)yavsc$${project}" \ + sudo systemctl start $(APP_INSTALL_PREFIX)yavsc$${project} ; done + @echo "💯 backend started" + +reinstall-prod: + make APP_INSTALL_ENV= APP_INSTALL_PREFIX= APP_INSTALL_ENV=production generated/$(APP_INSTALL_PREFIX)yavscOrg.service: generated/$(APP_INSTALL_PREFIX)yavscBlogs.service: @@ -62,7 +68,6 @@ copy-binaries: build_publish_Org build_publish_Blogs build_publish_Api stop-serv @sudo chown -R $(USER_AND_GROUP) $(DESTDIR)$(APPDIR) @sync - $(DESTDIR)/etc/systemd/system/$(APP_INSTALL_PREFIX)yavsc%.service: generated/$(APP_INSTALL_PREFIX)yavsc%.service sudo cp $^ $@ sudo chown root:root $@ @@ -81,21 +86,11 @@ install: copy-binaries copy-services sudo systemctl start yavsc$${project} ; \ done -reinstall: copy-binaries - @for project in $(APP_PROJECT_NAMES); do \ - sudo systemctl start yavsc$${project} ; done - -reinstall-prod: - make APP_INSTALL_ENV= APP_INSTALL_PREFIX= - -reinstall-pre: copy-binaries - @for project in $(APP_PROJECT_NAMES); do \ - sudo systemctl start yavscPre$${project} ; done stop-services: @for project in $(APP_PROJECT_NAMES); \ do \ - sudo systemctl stop yavsc$${project} ; \ + sudo systemctl stop $(APP_INSTALL_PREFIX)yavsc$${project} ; \ done $(SLNDIR)/src/Yavsc.Org/bin/$(CONFIGURATION)/$(DOTNET_FRAMEWORK)/publish: build_publish @@ -118,3 +113,5 @@ showBlogsLogs: clean: @rm -rf generated +generated/: + @mkdir -p $@ From 22c33c5388ba33c4338e1906ff65a256260bae00 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 21 Sep 2026 14:38:49 +0100 Subject: [PATCH 17/66] Hide inserted estimates from client list; redirect anon to /signin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Estimates with status Inserted (provider hasn't worked on them yet) were showing up in the client's "Mes devis à valider" list. The asclient endpoint now requires ProviderValidationDate to be set, so only estimates the provider has actually established and submitted are visible to the client. The asprovider endpoint is unchanged (providers still need to see incoming unprocessed requests). Also fix the anonymous-access redirect on Yavsc.Org: the application cookie's LoginPath defaulted to /Account/Login, which has no controller. Anonymous hits to a restricted page now redirect to the real /signin endpoint (with /signout and /Account/AccessDenied set to match the existing controller routes). Co-Authored-By: Claude Code --- .../EstimateApiControllerTests.cs | 28 +++++++++++++++++-- .../Business/EstimateApiController.cs | 8 +++++- .../Smoke/AccountSmokeTests.cs | 25 +++++++++++++++++ src/Yavsc.Org/Extensions/HostingExtensions.cs | 12 ++++++++ 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/Yavsc.Api.Test/EstimateApiControllerTests.cs b/src/Yavsc.Api.Test/EstimateApiControllerTests.cs index cb945f51a..d8cae0265 100644 --- a/src/Yavsc.Api.Test/EstimateApiControllerTests.cs +++ b/src/Yavsc.Api.Test/EstimateApiControllerTests.cs @@ -209,9 +209,12 @@ public sealed class EstimateApiControllerTests : IClassFixture /// Seeds an estimate owned by alice (the provider) for the given - /// client, optionally already validated by the client. + /// client, optionally already validated by the client. By default the + /// estimate is also provider-validated (i.e. the provider has submitted + /// it); pass = false to seed a + /// freshly inserted request the provider hasn't worked on yet. /// - private long SeedEstimate(string clientId, bool clientValidated) + private long SeedEstimate(string clientId, bool clientValidated, bool providerValidated = true) { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); @@ -229,6 +232,7 @@ public sealed class EstimateApiControllerTests : IClassFixture e.Bill) - .Where(e => e.ClientId == uid && e.ClientValidationDate == default)); + .Where(e => e.ClientId == uid + && e.ProviderValidationDate != default + && e.ClientValidationDate == default)); } // GET: api/estimate/asprovider diff --git a/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs b/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs index 61d774c57..5e7ab7776 100644 --- a/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs +++ b/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs @@ -1,4 +1,7 @@ +using System.Net; +using System.Net.Http; using IdentityServer8.Stores; +using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -40,4 +43,26 @@ public class AccountSmokeTests : IClassFixture Assert.Null(exception); } + + // Regression: anonymous access to a restricted page used to redirect to + // the ASP.NET Identity default LoginPath "/Account/Login", which has no + // controller here. The application cookie must redirect to the real + // sign-in endpoint at /signin instead. + [Fact] + public async Task Anonymous_access_to_protected_page_redirects_to_signin() + { + var client = _factory.CreateClient(new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + }); + + // EstimateController is [Authorize]; GET /Estimate hits Index(). + var response = await client.GetAsync("/Estimate", TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + var location = response.Headers.Location; + Assert.NotNull(location); + Assert.Equal("/signin", location.AbsolutePath); + Assert.Contains("ReturnUrl=%2FEstimate", location.Query); + } } diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index 8c1db2a73..ea035001c 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -215,6 +215,18 @@ public static class HostingExtensions services.AddScoped, UserClaimsPrincipalFactory>(); + // The default application cookie LoginPath is "/Account/Login", which + // has no controller here — anonymous access to a restricted page must + // redirect to the actual sign-in endpoint instead. The paths mirror + // the route attributes on AccountController (Constants.SigninPath etc., + // which carry a "~/" prefix that the cookie middleware does not want). + services.ConfigureApplicationCookie(options => + { + options.LoginPath = "/signin"; + options.LogoutPath = "/signout"; + options.AccessDeniedPath = "/Account/AccessDenied"; + }); + // Dev-only: Chromium rejects SameSite=None without Secure on http:// // (e.g. http://localhost:5000). The default Identity cookie policy // sets SameSite=None, which is invalid without Secure. Force Lax in From e28959b068f7457ac2cf1c98966bcfcba0897397 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 21 Sep 2026 14:39:25 +0100 Subject: [PATCH 18/66] asciidoc => markdown --- src/Yavsc.Org/Views/Home/Privacy.cshtml | 16 ++++++++-------- src/Yavsc.Server/Helpers/ServiceExtensions.cs | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Yavsc.Org/Views/Home/Privacy.cshtml b/src/Yavsc.Org/Views/Home/Privacy.cshtml index e5b285eb2..a9a6e7cf4 100644 --- a/src/Yavsc.Org/Views/Home/Privacy.cshtml +++ b/src/Yavsc.Org/Views/Home/Privacy.cshtml @@ -3,14 +3,14 @@ } -= La confidentialité -:hide-uri-scheme: +# La confidentialité -xref:Contact[Get Report] - -À aucun moment, aucune adresse postale, aucune adresse e-mail ni aucun numéro de téléphone -ne sont transmis à personne. Seul le système et son link:Contact[possesseur] ont accès à ces informations. - -De plus, le droit de retrait est permanent et sa mise en oeuvre link:/Account/Delete[immédiate]. +À aucun moment, aucune adresse postale, +aucune adresse e-mail ni aucun numéro de téléphone +ne sont transmis à personne. Seul le système + et son link:Contact[possesseur] ont accès + à ces informations. +De plus, le droit de retrait est permanent +et sa [mise en oeuvre est immédiate](/Account/Delete). diff --git a/src/Yavsc.Server/Helpers/ServiceExtensions.cs b/src/Yavsc.Server/Helpers/ServiceExtensions.cs index f20d50476..297084c28 100644 --- a/src/Yavsc.Server/Helpers/ServiceExtensions.cs +++ b/src/Yavsc.Server/Helpers/ServiceExtensions.cs @@ -113,6 +113,7 @@ public static class ServiceExtensions options.ClaimsIssuer = authority; options.Audience = audiences[0]; + // Dev: every Yavsc resource service (Yavsc.Api, Yavsc.Blogs, // Yavsc.Org itself) validates JWTs against the OP that runs // on https://localhost:5001 with a self-signed dev cert. From 9aef10f281f7e56b360b18c9289dc7b2ef7cffd5 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 21 Sep 2026 14:58:40 +0100 Subject: [PATCH 19/66] make reinstall, better --- contrib/Makefile | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/contrib/Makefile b/contrib/Makefile index 4c7e0e6d6..4ba6059df 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -9,12 +9,12 @@ include .$(APP_INSTALL_ENV).env reinstall: copy-binaries @for project in $(APP_PROJECT_NAMES); do \ - echo "🚀 Starting $(APP_INSTALL_PREFIX)yavsc$${project}" \ + echo "🚀 Starting $(APP_INSTALL_PREFIX)yavsc$${project}" ; \ sudo systemctl start $(APP_INSTALL_PREFIX)yavsc$${project} ; done @echo "💯 backend started" reinstall-prod: - make APP_INSTALL_ENV= APP_INSTALL_PREFIX= APP_INSTALL_ENV=production + make reinstall APP_INSTALL_ENV= APP_INSTALL_PREFIX= APP_INSTALL_ENV=production generated/$(APP_INSTALL_PREFIX)yavscOrg.service: generated/$(APP_INSTALL_PREFIX)yavscBlogs.service: @@ -82,14 +82,16 @@ install: copy-binaries copy-services @sudo systemctl daemon-reload @for project in $(APP_PROJECT_NAMES); \ do \ - sudo systemctl enable yavsc$${project} ; \ - sudo systemctl start yavsc$${project} ; \ + echo "🖒 enable $(APP_INSTALL_PREFIX)yavsc$${project}" ; \ + sudo systemctl enable $(APP_INSTALL_PREFIX)yavsc$${project} ; \ + echo "🚀 start $(APP_INSTALL_PREFIX)yavsc$${project}" ; \ + sudo systemctl start $(APP_INSTALL_PREFIX)yavsc$${project} ; \ done - stop-services: @for project in $(APP_PROJECT_NAMES); \ do \ + echo "🔥 Stopping $(APP_INSTALL_PREFIX)yavsc$${project}" ; \ sudo systemctl stop $(APP_INSTALL_PREFIX)yavsc$${project} ; \ done From 709846c58d1e9440fa9b4f249c0cae011d629bfd Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 21 Sep 2026 16:15:18 +0100 Subject: [PATCH 20/66] Finalize FrontOffice accept/reject: role-aware, authz, signature; drop Signature.FilePath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FrontOfficeApiController.AcceptQuery/RejectQuery now resolve the actor (provider vs client vs admin) from the query's PerformerId/ClientId and authorize accordingly — a non-party caller is forbidden. Either party may accept or reject. Acceptance may carry a PostIt signature, which is staged on the query's linked estimate (Estimate.CommandId == queryId) and sets the estimate's ProviderValidationDate / ClientValidationDate; a signature with no linked estimate, or a signature on a reject, returns 400. Admin bypasses authz but cannot sign. The shared signature-staging logic (find-or-add the Signature row by (EstimateId, Type), write Strokes/CoordinateMax/CapturedAtUtc) is extracted into EstimateSignaturePersister, and BillingController.Sign now calls it. The persister owns no disk I/O and no SaveChanges — the caller persists in its own transaction. Signature.Strokes is now the sole source of truth: drop the FilePath property, remove the on-disk EstimateSignatureFileHelper (and its tests), and add the DropSignatureFilePath migration. The legacy PNG prosign/clisign flow is independent of Signature.FilePath and is unchanged. Co-Authored-By: Claude Code --- .../FrontOfficeApiControllerTests.cs | 206 +- .../Controllers/Business/BillingController.cs | 83 +- .../Business/FrontOfficeApiController.cs | 209 +- .../EstimateSignatureFileHelperTests.cs | 136 - ...21145454_DropSignatureFilePath.Designer.cs | 4713 +++++++++++++++++ .../20260921145454_DropSignatureFilePath.cs | 29 + .../ApplicationDbContextModelSnapshot.cs | 13 +- .../Helpers/EstimateSignatureFileHelper.cs | 158 - .../Helpers/EstimateSignaturePersister.cs | 111 + src/Yavsc.Server/Models/Billing/Signature.cs | 31 +- 10 files changed, 5254 insertions(+), 435 deletions(-) delete mode 100644 src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs create mode 100644 src/Yavsc.Org/Migrations/20260921145454_DropSignatureFilePath.Designer.cs create mode 100644 src/Yavsc.Org/Migrations/20260921145454_DropSignatureFilePath.cs delete mode 100644 src/Yavsc.Server/Helpers/EstimateSignatureFileHelper.cs create mode 100644 src/Yavsc.Server/Helpers/EstimateSignaturePersister.cs diff --git a/src/Yavsc.Api.Test/FrontOfficeApiControllerTests.cs b/src/Yavsc.Api.Test/FrontOfficeApiControllerTests.cs index 97ed2e61c..55e830660 100644 --- a/src/Yavsc.Api.Test/FrontOfficeApiControllerTests.cs +++ b/src/Yavsc.Api.Test/FrontOfficeApiControllerTests.cs @@ -1,9 +1,12 @@ using System.Net; using System.Net.Http.Headers; +using System.Net.Http.Json; using Microsoft.Extensions.DependencyInjection; using Yavsc.Api.Test.Fixtures; using Yavsc.Helpers; using Yavsc.Models; +using Yavsc.Models.Billing; +using Yavsc.Models.Workflow; using Yavsc.Tests.Shared; namespace Yavsc.Api.Test; @@ -34,21 +37,88 @@ public sealed class FrontOfficeApiControllerTests : IClassFixture JsonBody(new { }); + + private static HttpContent WithSignature() => + JsonBody(new { strokes = SampleStrokes, coordinateMax = 10_000 }); + + private static JsonContent JsonBody(object value) => + JsonContent.Create(value, mediaType: new MediaTypeHeaderValue("application/json")); + + /// + /// Seeds an Rdv query with distinct parties (provider alice, + /// client bob) and, when is true, + /// an estimate linked to it via CommandId. Returns the query id + /// and (when seeded) the estimate id. + /// + private (long queryId, long estimateId) SeedQuery(bool withEstimate) + { + WorkflowHelpers.ConfigureBillingService(); + _fixture.ResetAndSeedActivityGraph(); + + using var scope = _fixture.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var location = db.Locations.Single(l => l.Address == "1 rue du Test"); + + var query = new RdvQuery + { + ActivityCode = "dev", + ClientId = "bob", + PerformerId = "alice", + Consent = true, + UserCreated = "alice", + UserModified = "alice", + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + EventDate = DateTime.UtcNow.AddDays(1), + Location = location, + Reason = "Initial rendez-vous", + Status = Yavsc.QueryStatus.Inserted, + }; + db.RdvQueries.Add(query); + db.SaveChanges(); + + long estimateId = 0; + if (withEstimate) + { + var estimate = new Estimate + { + CommandId = query.Id, + ClientId = "bob", + OwnerId = "alice", + CommandType = BillingCodes.Rdv, + Title = "Devis prestation", + Description = "Devis de test", + AttachedFiles = new List(), + AttachedGraphics = new List(), + Bill = new List + { + new() { Name = "Prestation", Description = "Prestation de base", Count = 1, UnitaryCost = 120m, Currency = "EUR" }, + }, + }; + db.Estimates.Add(estimate); + db.SaveChanges(); + estimateId = estimate.Id; + } + + return (query.Id, estimateId); + } + [Fact] public async Task Front_accept_query_updates_status_without_server_error() { - WorkflowHelpers.ConfigureBillingService(); - _fixture.ResetAndSeedRdvQueryGraph(); - - long queryId; - using (var scope = _fixture.Services.CreateScope()) - { - var db = scope.ServiceProvider.GetRequiredService(); - queryId = db.RdvQueries.Select(q => q.Id).Single(); - } + var (queryId, _) = SeedQuery(withEstimate: false); using var http = NewClient(); - var response = await http.PostAsync($"/api/v1/front/query/accept?billingCode=Rdv&queryId={queryId}", content: null, TestContext.Current.CancellationToken); + var response = await http.PostAsync( + $"/api/v1/front/query/accept?billingCode=Rdv&queryId={queryId}", + NoSignature(), + TestContext.Current.CancellationToken); var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}"); @@ -59,4 +129,118 @@ public sealed class FrontOfficeApiControllerTests : IClassFixture(); + + Assert.Equal(QueryStatus.Accepted, assertDb.RdvQueries.Single(q => q.Id == queryId).Status); + + var estimate = assertDb.Estimates.Single(e => e.Id == estimateId); + Assert.True(estimate.ProviderValidationDate != default); + Assert.True(estimate.ClientValidationDate == default); + + var signature = assertDb.Signatures.Single(s => s.EstimateId == estimateId); + Assert.Equal(SignatureType.Pro, signature.Type); + Assert.Equal("alice", signature.SignerId); + } + + [Fact] + public async Task Client_accept_with_signature_sets_ClientValidationDate_and_Client_signature() + { + var (queryId, estimateId) = SeedQuery(withEstimate: true); + + using var http = NewClient(subject: "bob"); + var response = await http.PostAsync( + $"/api/v1/front/query/accept?billingCode=Rdv&queryId={queryId}", + WithSignature(), + TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}"); + + using var assertScope = _fixture.Services.CreateScope(); + var assertDb = assertScope.ServiceProvider.GetRequiredService(); + + Assert.Equal(QueryStatus.Accepted, assertDb.RdvQueries.Single(q => q.Id == queryId).Status); + + var estimate = assertDb.Estimates.Single(e => e.Id == estimateId); + Assert.True(estimate.ClientValidationDate != default); + Assert.True(estimate.ProviderValidationDate == default); + + var signature = assertDb.Signatures.Single(s => s.EstimateId == estimateId); + Assert.Equal(SignatureType.Client, signature.Type); + Assert.Equal("bob", signature.SignerId); + } + + [Fact] + public async Task Reject_sets_status_Rejected() + { + var (queryId, _) = SeedQuery(withEstimate: false); + + using var http = NewClient(subject: "alice"); + var response = await http.PostAsync( + $"/api/v1/front/query/reject?billingCode=Rdv&queryId={queryId}", + NoSignature(), + TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}"); + + using var assertScope = _fixture.Services.CreateScope(); + var assertDb = assertScope.ServiceProvider.GetRequiredService(); + Assert.Equal(QueryStatus.Rejected, assertDb.RdvQueries.Single(q => q.Id == queryId).Status); + } + + [Fact] + public async Task Non_party_actor_is_forbidden() + { + var (queryId, _) = SeedQuery(withEstimate: false); + + using var http = NewClient(subject: "carol"); + var response = await http.PostAsync( + $"/api/v1/front/query/accept?billingCode=Rdv&queryId={queryId}", + NoSignature(), + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task Accept_with_signature_but_no_estimate_returns_400() + { + var (queryId, _) = SeedQuery(withEstimate: false); + + using var http = NewClient(subject: "alice"); + var response = await http.PostAsync( + $"/api/v1/front/query/accept?billingCode=Rdv&queryId={queryId}", + WithSignature(), + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task Reject_with_signature_returns_400() + { + var (queryId, _) = SeedQuery(withEstimate: true); + + using var http = NewClient(subject: "alice"); + var response = await http.PostAsync( + $"/api/v1/front/query/reject?billingCode=Rdv&queryId={queryId}", + WithSignature(), + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } +} \ No newline at end of file diff --git a/src/Yavsc.Api/Controllers/Business/BillingController.cs b/src/Yavsc.Api/Controllers/Business/BillingController.cs index ba099e7c7..ba43094a9 100644 --- a/src/Yavsc.Api/Controllers/Business/BillingController.cs +++ b/src/Yavsc.Api/Controllers/Business/BillingController.cs @@ -401,79 +401,12 @@ namespace Yavsc.ApiControllers Strokes = body.Strokes, }; - // Disk write first: a disk failure shouldn't leave - // a Signature row pointing at a file that doesn't - // exist. The file helper throws on filesystem - // problems and propagates here. - FileReceivedInfo fi; - try - { - fi = await User.ReceiveEstimateSignatureAsync(id, type, payload, - siteSettings, token); - } - catch (Exception ex) - { - logger.LogError(ex, "estimate {Id}: signature file write failed", id); - return BadRequest(new { Error = "file write failed", Detail = ex.Message }); - } - - // Find-or-add: the (EstimateId, Type) pair is - // unique, so a second POST for the same side of the - // estimate replaces the previous signature. EF - // translates this into a single UPDATE when the - // row exists and an INSERT otherwise; the unique - // index in ApplicationDbContext is the - // database-level guarantee that the contract - // holds if two requests race. - var signature = await dbContext.Signatures - .FirstOrDefaultAsync(s => s.EstimateId == id && s.Type == type, token); - - if (signature is null) - { - signature = new Signature - { - EstimateId = id, - SignerId = userId, - Type = type, - }; - dbContext.Signatures.Add(signature); - } - else - { - // Roll the signer's quota back by the size of - // the file we're about to orphan: the old - // FilePath is no longer referenced once we - // overwrite FilePath below. - try - { - var orphan = new FileInfo(signature.FilePath); - if (orphan.Exists) - { - var signerForOrphan = await dbContext.Users - .FirstOrDefaultAsync(u => u.Id == userId, token); - if (signerForOrphan is not null) - signerForOrphan.DiskUsage = - Math.Max(0, signerForOrphan.DiskUsage - orphan.Length); - } - } - catch { /* best effort — the file is being replaced anyway */ } - } - - signature.SignerId = userId; - signature.CoordinateMax = payload.CoordinateMax; - signature.Strokes = payload.Strokes; - signature.CapturedAtUtc = payload.CapturedAtUtc; - signature.FilePath = Path.Combine(fi.DestDir, fi.FileName); - - // Bump the signer's quota. The Signature row's - // SignerId is the IdentityUser.Id (a string), so we - // look up by Id and not by username. - var signer = await dbContext.Users - .FirstOrDefaultAsync(u => u.Id == userId, token); - if (signer is not null) - { - signer.DiskUsage += new FileInfo(signature.FilePath).Length; - } + // The Signature row is staged by the shared persister; we + // SaveChanges here so the signature write is one + // transaction. The Strokes column is the source of truth — + // there is no on-disk copy to roll back on failure. + var signature = await EstimateSignaturePersister.StageAsync( + dbContext, id, type, userId, payload, token); try { @@ -482,10 +415,6 @@ namespace Yavsc.ApiControllers catch (Exception ex) { logger.LogError(ex, "estimate {Id}: signature db write failed", id); - // Best-effort rollback: remove the file we wrote - // so disk and db don't disagree. - try { System.IO.File.Delete(signature.FilePath); } - catch { /* swallow — the row will be re-orphaned, the user re-signs */ } return BadRequest(new { Error = "db write failed", Detail = ex.Message }); } diff --git a/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs b/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs index 3adac5b03..5f328728c 100644 --- a/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs @@ -1,7 +1,10 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Yavsc.Abstract.Workflow; using Yavsc.Helpers; using Yavsc.Models; +using Yavsc.Models.Billing; using Yavsc.Services; using Yavsc.Server.Helpers; using Yavsc.ViewModels.FrontOffice; @@ -13,13 +16,17 @@ namespace Yavsc.ApiControllers public class FrontOfficeApiController : Controller { ApplicationDbContext dbContext; + private readonly IBillingService billing; + private readonly ILogger logger; - private IBillingService billing; - - public FrontOfficeApiController(ApplicationDbContext context, IBillingService billing = null) + public FrontOfficeApiController( + ApplicationDbContext context, + ILoggerFactory loggerFactory, + IBillingService billing = null) { dbContext = context; this.billing = billing ?? new BillingService(context); + logger = loggerFactory.CreateLogger(); } [HttpGet("profiles/{actCode}")] @@ -28,29 +35,183 @@ namespace Yavsc.ApiControllers return await dbContext.ListPerformersAsync(billing, actCode); } - [HttpPost("query/reject")] - public IActionResult RejectQuery(string billingCode, long queryId) - { - if (billingCode == null) return BadRequest("billingCode"); - if (queryId == 0) return BadRequest("queryId"); - var billing = BillingService.GetBillable(dbContext, billingCode, queryId); - if (billing == null) return BadRequest(); - - billing.Status = QueryStatus.Rejected; - dbContext.SaveChanges(User.GetUserId()); - return Ok(); - } - + // POST: api/front/query/accept + // + // Either party of the query may accept: the provider + // (PerformerId) accepts the client's request, or the client + // (ClientId) accepts the provider's estimate. An optional + // signature (the PostIt wire format) may be sent along; it is + // persisted on the estimate linked to the query + // (Estimate.CommandId == queryId), and the matching validation + // date is stamped — ProviderValidationDate when the provider + // accepts, ClientValidationDate when the client accepts. [HttpPost("query/accept")] - public IActionResult AcceptQuery(string billingCode, long queryId) + public async Task AcceptQuery( + string billingCode, + long queryId, + [FromBody] QueryAcceptanceRequest? body, + CancellationToken token) { - if (billingCode == null) return BadRequest("billingCode"); - if (queryId == 0) return BadRequest("queryId"); - var billing = BillingService.GetBillable(dbContext, billingCode, queryId); - if (billing == null) return BadRequest(); - billing.Status = QueryStatus.Accepted; - dbContext.SaveChanges(User.GetUserId()); - return Ok(); + var (query, role, error) = await ResolveAndAuthorizeAsync(billingCode, queryId, token); + if (error is not null) return error; + if (query is null) return BadRequest(new { Error = "query not found" }); + + Signature? signature = null; + if (HasSignature(body)) + { + // Admins bypass authorization but are neither party of + // the estimate, so they cannot sign. + if (role == ActorRole.Admin) + return BadRequest(new { Error = "admin cannot sign on behalf of a party" }); + + var estimate = await dbContext.Estimates + .Where(e => e.CommandId == queryId) + .OrderByDescending(e => e.Id) + .FirstOrDefaultAsync(token); + if (estimate is null) + return BadRequest(new { Error = "no estimate linked to this query" }); + + var type = role == ActorRole.Provider + ? SignatureType.Pro + : SignatureType.Client; + + var payload = new SignaturePadPayload + { + CoordinateMax = body!.CoordinateMax, + CapturedAtUtc = body.CapturedAtUtc ?? DateTime.UtcNow, + Strokes = body.Strokes!, + }; + + try + { + var uid = User.GetUserId(); + signature = await EstimateSignaturePersister.StageAsync( + dbContext, estimate.Id, type, uid, payload, token); + } + catch (Exception ex) + { + logger.LogError(ex, "query {QueryId}: signature stage failed", queryId); + return BadRequest(new { Error = "signature stage failed", Detail = ex.Message }); + } + + if (role == ActorRole.Provider) + estimate.ProviderValidationDate = DateTime.UtcNow; + else + estimate.ClientValidationDate = DateTime.UtcNow; + } + + query.Status = QueryStatus.Accepted; + + try + { + await dbContext.SaveChangesAsync(User.GetUserId(), token); + } + catch (Exception ex) + { + logger.LogError(ex, "query {QueryId}: accept db write failed", queryId); + return BadRequest(new { Error = "db write failed", Detail = ex.Message }); + } + + return Ok(new + { + queryId, + status = query.Status.ToString(), + signature = signature is null + ? null + : new + { + id = signature.Id, + estimateId = signature.EstimateId, + type = signature.Type.ToString(), + }, + }); } + + // POST: api/front/query/reject + // + // Either party may reject. A signature is not meaningful on a + // rejection, so one is refused with 400. + [HttpPost("query/reject")] + public async Task RejectQuery( + string billingCode, + long queryId, + [FromBody] QueryAcceptanceRequest? body, + CancellationToken token) + { + if (HasSignature(body)) + return BadRequest(new { Error = "signature not allowed on reject" }); + + var (query, role, error) = await ResolveAndAuthorizeAsync(billingCode, queryId, token); + if (error is not null) return error; + if (query is null) return BadRequest(new { Error = "query not found" }); + + query.Status = QueryStatus.Rejected; + await dbContext.SaveChangesAsync(User.GetUserId(), token); + + return Ok(new + { + queryId, + status = query.Status.ToString(), + }); + } + + // Loads the query and checks that the caller is a party to it + // (provider or client) or an admin. Returns the role the caller + // is acting in; Admin is only returned when the caller is + // neither party but is an admin. + private async Task<(IQuery? Query, ActorRole Role, IActionResult? Error)> + ResolveAndAuthorizeAsync(string billingCode, long queryId, CancellationToken token) + { + if (string.IsNullOrWhiteSpace(billingCode)) + return (null, ActorRole.None, BadRequest("billingCode")); + if (queryId == 0) + return (null, ActorRole.None, BadRequest("queryId")); + + var query = await billing.GetBillAsync(billingCode, queryId); + if (query is null) + return (null, ActorRole.None, BadRequest()); + + var uid = User.GetUserId(); + bool isProvider = query.PerformerId == uid; + bool isClient = query.ClientId == uid; + + if (isProvider) return (query, ActorRole.Provider, null); + if (isClient) return (query, ActorRole.Client, null); + + if (User.IsInRole(Constants.AdminGroupName)) + return (query, ActorRole.Admin, null); + + return (null, ActorRole.None, Forbid()); + } + + private static bool HasSignature(QueryAcceptanceRequest? body) + => body is not null && body.Strokes is not null && body.Strokes.Length > 0; } + + internal enum ActorRole { None, Provider, Client, Admin } } + +namespace Yavsc.ViewModels.FrontOffice +{ + /// + /// Body of POST /api/v1/front/query/accept and + /// .../reject. The field is optional: + /// when present (non-empty) on accept, it is the PostIt wire-format + /// signature captured with the acceptance. All fields are optional + /// so a caller may post {} to accept/reject without a + /// signature. + /// + public class QueryAcceptanceRequest + { + /// + /// Wire-format strokes. See + /// PostIt.Models.SignaturePadData. Null or empty means + /// no signature. + /// + public int[]? Strokes { get; set; } + + public int CoordinateMax { get; set; } = 10_000; + + public DateTime? CapturedAtUtc { get; set; } + } +} \ No newline at end of file diff --git a/src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs b/src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs deleted file mode 100644 index b7b4d3591..000000000 --- a/src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System.Security.Claims; -using System.Text.Json; -using Xunit; -using Yavsc.Models.Billing; -using Yavsc.Server.Helpers; -using Yavsc.Server.Models.FileSystem; - -namespace Yavsc.Org.Tests; - -/// -/// Tests for the static -/// helper. Scope is intentionally narrow: the file-naming format, -/// the strokes counter, and the on-disk write path. The controller -/// (authz, db persistence, signalR notification) is out of scope -/// for this commit and will get a dedicated integration test once -/// the Yavsc.Api test project is set up. -/// -public class EstimateSignatureFileHelperTests : IDisposable -{ - private readonly string _tempRoot; - private SiteSettings siteSettings; - - public EstimateSignatureFileHelperTests() - { - // UserFilesDirName is a process-wide static; we redirect - // it to a per-test temp dir so concurrent tests don't - // collide and the host filesystem is not littered. - _tempRoot = Path.Combine( - Path.GetTempPath(), - "yavsc-sig-tests-" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(_tempRoot); - this.siteSettings = new SiteSettings { Blog = _tempRoot }; - } - - public void Dispose() - { - try { Directory.Delete(_tempRoot, recursive: true); } - catch { /* best effort — the OS will clean Temp eventually */ } - } - - [Fact] - public void FileNameFormat_lowercases_type_and_includes_estimateId_and_ticks() - { - var name = EstimateSignatureFileHelper.FileNameFormat( - SignatureType.Pro, 42, 638_000_000_000_000_000L); - Assert.Equal("sign-pro-42-638000000000000000.json", name); - - var cli = EstimateSignatureFileHelper.FileNameFormat( - SignatureType.Client, 7, 1L); - Assert.Equal("sign-client-7-1.json", cli); - } - - [Theory] - [InlineData(new int[] { }, 0)] - [InlineData(new[] { 1, 100, 200 }, 1)] - [InlineData(new[] { 2, 1, 2, 3, 4 }, 1)] - [InlineData(new[] { 1, 1, 1, 2, 2, 3, 3 }, 2)] - [InlineData(new[] { 0, 1, 2, 3 }, 0)] // malformed k=0: short-circuit - public void ReceiveEstimateSignatureAsync_writes_a_v1_envelope(int[] strokes, int expectedStrokeCount) - { - // We don't read the count back from the helper (it's a - // private method), but the JSON envelope must reflect - // it; this verifies the public behaviour end-to-end. - _ = expectedStrokeCount; - // Arrange - var user = MakeUser("alice"); - var payload = new SignaturePadPayload - { - CoordinateMax = 10_000, - CapturedAtUtc = new DateTime(2026, 7, 4, 12, 0, 0, DateTimeKind.Utc), - Strokes = strokes, - }; - - // Act - var fi = Run(user, 123L, SignatureType.Pro, payload, this.siteSettings); - - // Assert: file exists, sits under the user's root, and - // parses as a yavsc.signature/v1 envelope. - var fullPath = Path.Combine(fi.DestDir, fi.FileName); - Assert.True(File.Exists(fullPath), $"missing: {fullPath}"); - - using var doc = JsonDocument.Parse(File.ReadAllText(fullPath)); - var root = doc.RootElement; - Assert.Equal("yavsc.signature/v1", root.GetProperty("format").GetString()); - Assert.Equal(10_000, root.GetProperty("coordinateMax").GetInt32()); - Assert.Equal(123L, root.GetProperty("estimateId").GetInt64()); - Assert.Equal("Pro", root.GetProperty("type").GetString()); - Assert.Equal("alice", root.GetProperty("signerName").GetString()); - Assert.Equal(expectedStrokeCount, root.GetProperty("strokeCount").GetInt32()); - } - - [Fact] - public async Task ReceiveEstimateSignatureAsync_rejects_null_payload() - { - var user = MakeUser("bob"); - // Capture TestContext.Current.CancellationToken outside the - // lambda so xUnit1051 sees a real CancellationToken argument - // (the lambda body runs on a different stack frame). - var ct = TestContext.Current.CancellationToken; - await Assert.ThrowsAsync(() => - EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync( - user, 1L, SignatureType.Pro, payload: null!, - siteSettings, token: ct)); - } - - [Fact] - public async Task ReceiveEstimateSignatureAsync_rejects_non_positive_estimateId() - { - var user = MakeUser("bob"); - var payload = new SignaturePadPayload { Strokes = new[] { 1, 100, 100 } }; - var ct = TestContext.Current.CancellationToken; - await Assert.ThrowsAsync(() => - EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync( - user, 0L, SignatureType.Pro, payload, this.siteSettings, token: ct)); - } - - // --- helpers ---------------------------------------------------- - - private static FileReceivedInfo Run( - ClaimsPrincipal user, long estimateId, SignatureType type, SignaturePadPayload payload, SiteSettings siteSettings) - { - // The helper is async; tests that don't care about the - // result can call it sync via .GetAwaiter().GetResult() - // because we know it never throws in the happy path. - return EstimateSignatureFileHelper - .ReceiveEstimateSignatureAsync(user, estimateId, type, payload, siteSettings, CancellationToken.None) - .GetAwaiter().GetResult(); - } - - private static ClaimsPrincipal MakeUser(string username) - { - return new ClaimsPrincipal(new ClaimsIdentity( - new[] { new Claim(ClaimTypes.Name, username) }, - authenticationType: "test")); - } -} diff --git a/src/Yavsc.Org/Migrations/20260921145454_DropSignatureFilePath.Designer.cs b/src/Yavsc.Org/Migrations/20260921145454_DropSignatureFilePath.Designer.cs new file mode 100644 index 000000000..04b9fdc68 --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260921145454_DropSignatureFilePath.Designer.cs @@ -0,0 +1,4713 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Yavsc.Models; + +#nullable disable + +namespace Yavsc.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260921145454_DropSignatureFilePath")] + partial class DropSignatureFilePath + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("AllowedAccessTokenSigningAlgorithms") + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("LastAccessed") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ApiResources"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceSecrets"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Emphasize") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("ApiScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ScopeId") + .HasColumnType("integer"); + + b.Property("ScopeId1") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScopeId"); + + b.HasIndex("ScopeId1"); + + b.ToTable("ApiScopeClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("ScopeId") + .HasColumnType("integer"); + + b.Property("ScopeId1") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScopeId"); + + b.HasIndex("ScopeId1"); + + b.ToTable("ApiScopeProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("AbsoluteRefreshTokenLifetime") + .HasColumnType("integer"); + + b.Property("AccessTokenLifetime") + .HasColumnType("integer"); + + b.Property("AccessTokenType") + .HasColumnType("integer"); + + b.Property("AllowAccessTokensViaBrowser") + .HasColumnType("boolean"); + + b.Property("AllowOfflineAccess") + .HasColumnType("boolean"); + + b.Property("AllowPlainTextPkce") + .HasColumnType("boolean"); + + b.Property("AllowRememberConsent") + .HasColumnType("boolean"); + + b.Property("AllowedIdentityTokenSigningAlgorithms") + .HasColumnType("text"); + + b.Property("AlwaysIncludeUserClaimsInIdToken") + .HasColumnType("boolean"); + + b.Property("AlwaysSendClientClaims") + .HasColumnType("boolean"); + + b.Property("AuthorizationCodeLifetime") + .HasColumnType("integer"); + + b.Property("BackChannelLogoutSessionRequired") + .HasColumnType("boolean"); + + b.Property("BackChannelLogoutUri") + .HasColumnType("text"); + + b.Property("ClientClaimsPrefix") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ClientName") + .HasColumnType("text"); + + b.Property("ClientUri") + .HasColumnType("text"); + + b.Property("ConsentLifetime") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DeviceCodeLifetime") + .HasColumnType("integer"); + + b.Property("EnableLocalLogin") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("FrontChannelLogoutSessionRequired") + .HasColumnType("boolean"); + + b.Property("FrontChannelLogoutUri") + .HasColumnType("text"); + + b.Property("IdentityTokenLifetime") + .HasColumnType("integer"); + + b.Property("IncludeJwtId") + .HasColumnType("boolean"); + + b.Property("LastAccessed") + .HasColumnType("timestamp with time zone"); + + b.Property("LogoUri") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("PairWiseSubjectSalt") + .HasColumnType("text"); + + b.Property("ProtocolType") + .HasColumnType("text"); + + b.Property("RefreshTokenExpiration") + .HasColumnType("integer"); + + b.Property("RefreshTokenUsage") + .HasColumnType("integer"); + + b.Property("RequireClientSecret") + .HasColumnType("boolean"); + + b.Property("RequireConsent") + .HasColumnType("boolean"); + + b.Property("RequirePkce") + .HasColumnType("boolean"); + + b.Property("RequireRequestObject") + .HasColumnType("boolean"); + + b.Property("SlidingRefreshTokenLifetime") + .HasColumnType("integer"); + + b.Property("UpdateAccessTokenClaimsOnRefresh") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.Property("UserCodeType") + .HasColumnType("text"); + + b.Property("UserSsoLifetime") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Clients"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Origin") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientCorsOrigins"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("GrantType") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientGrantTypes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientIdPRestrictions"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("PostLogoutRedirectUri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientPostLogoutRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("RedirectUri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientSecrets"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.DeviceFlowCodes", b => + { + b.Property("UserCode") + .HasColumnType("text"); + + b.Property("DeviceCode") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("SubjectId") + .HasColumnType("text"); + + b.HasKey("UserCode", "DeviceCode"); + + b.ToTable("DeviceFlowCodes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Emphasize") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("IdentityResources"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.PersistedGrant", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ConsumedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("SubjectId") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Key"); + + b.ToTable("PersistedGrants"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + 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("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") + .IsRequired() + .HasColumnType("text"); + + 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") + .IsRequired() + .HasColumnType("text"); + + 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") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + 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("Yavsc.Abstract.Identity.ClientProviderInfo", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("BillingAddressId") + .HasColumnType("bigint"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("ClientProviderInfo"); + }); + + modelBuilder.Entity("Yavsc.Abstract.Models.Messaging.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Target") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("body") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("click_action") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("color") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("icon") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("exclam"); + + b.Property("sound") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("tag") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Notification"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TargetId"); + + b.ToTable("Ban"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("BlackListed"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.Property("CircleId") + .HasColumnType("bigint"); + + b.Property("BlogPostId") + .HasColumnType("bigint"); + + b.HasKey("CircleId", "BlogPostId"); + + b.HasIndex("BlogPostId"); + + b.ToTable("CircleAuthorizationToBlogPost"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToFile", b => + { + b.Property("CircleId") + .HasColumnType("bigint"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Access") + .HasColumnType("smallint"); + + b.HasKey("CircleId", "Path", "OwnerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("CircleAuthorizationToFile"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ContactCredits") + .HasColumnType("bigint"); + + b.Property("Credits") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.ToTable("BankStatus"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("AllowMonthlyEmail") + .HasColumnType("boolean"); + + b.Property("Avatar") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("/images/Users/icon_user.png"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("DedicatedGoogleCalendar") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("DiskQuota") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(524288000L); + + b.Property("DiskUsage") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FullName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxFileSize") + .HasColumnType("bigint"); + + 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("PostalAddressId") + .HasColumnType("bigint"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("PostalAddressId"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BalanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExecDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Impact") + .HasColumnType("numeric"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BalanceId"); + + b.ToTable("BalanceImpact"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("BIC") + .IsRequired() + .HasColumnType("text"); + + b.Property("BankCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("BankedKey") + .HasColumnType("integer"); + + b.Property("IBAN") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("WicketCode") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("BankIdentity"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("integer"); + + b.Property("Currency") + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("EstimateTemplateId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UnitaryCost") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EstimateId"); + + b.HasIndex("EstimateTemplateId"); + + b.ToTable("CommandLine"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttachedFilesString") + .IsRequired() + .HasColumnType("text"); + + b.Property("AttachedGraphicsString") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CommandId") + .HasColumnType("bigint"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProviderValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("CommandId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Estimates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EstimateTemplates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.ExceptionSIREN", b => + { + b.Property("SIREN") + .HasColumnType("text"); + + b.HasKey("SIREN"); + + b.ToTable("ExceptionsSIREN"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.NominativeServiceCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserModified") + .IsRequired() + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.ToTable("NominativeServiceCommands"); + + b.HasDiscriminator("Discriminator").HasValue("NominativeServiceCommand"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CapturedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CoordinateMax") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(10000); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("SignerId") + .IsRequired() + .HasColumnType("text"); + + b.PrimitiveCollection("Strokes") + .IsRequired() + .HasColumnType("integer[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SignerId"); + + b.HasIndex("EstimateId", "Type") + .IsUnique(); + + b.ToTable("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.Property("FileId") + .HasColumnType("bigint"); + + b.Property("PostId") + .HasColumnType("bigint"); + + b.HasKey("FileId", "PostId"); + + b.HasIndex("PostId"); + + b.ToTable("BlogAttachedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .HasMaxLength(56224) + .HasColumnType("character varying(56224)"); + + b.Property("AuthorId") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Photo") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("BlogSpot"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.Property("PostId") + .HasColumnType("bigint"); + + b.Property("TagId") + .HasColumnType("bigint"); + + b.HasKey("PostId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("BlogTag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ParentId") + .HasColumnType("bigint"); + + b.Property("ReceiverId") + .HasColumnType("bigint"); + + b.Property("UserCreated") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserModified") + .IsRequired() + .HasColumnType("text"); + + b.Property("Visible") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ParentId"); + + b.HasIndex("ReceiverId"); + + b.ToTable("Comment"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.UploadedFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("Length") + .HasColumnType("bigint"); + + b.Property("Path") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("UploadedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.Property("PostId") + .HasColumnType("bigint"); + + b.HasKey("PostId"); + + b.ToTable("blogSpotPublications"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.HasKey("OwnerId"); + + b.ToTable("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Reccurence") + .HasColumnType("integer"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScheduleOwnerId"); + + b.HasIndex("PeriodStart", "PeriodEnd"); + + b.ToTable("ScheduledEvent"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.Property("ConnectionId") + .HasColumnType("text"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Connected") + .HasColumnType("boolean"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("ConnectionId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("ChatConnection"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LatestJoinPart") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Name"); + + b.HasIndex("OwnerId"); + + b.ToTable("ChatRoom"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.Property("ChannelName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("ChannelName", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ChatRoomAccess"); + }); + + modelBuilder.Entity("Yavsc.Models.Cratie.Option", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("CodeScrutin") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code", "CodeScrutin"); + + b.ToTable("Option"); + }); + + modelBuilder.Entity("Yavsc.Models.Drawing.Color", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Blue") + .HasColumnType("smallint"); + + b.Property("Green") + .HasColumnType("smallint"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Red") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ActionDistance") + .HasColumnType("integer"); + + b.Property("CarePrice") + .HasColumnType("numeric"); + + b.Property("FlatFeeDiscount") + .HasColumnType("numeric"); + + b.Property("HalfBalayagePrice") + .HasColumnType("numeric"); + + b.Property("HalfBrushingPrice") + .HasColumnType("numeric"); + + b.Property("HalfColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfDefrisPrice") + .HasColumnType("numeric"); + + b.Property("HalfFoldingPrice") + .HasColumnType("numeric"); + + b.Property("HalfMechPrice") + .HasColumnType("numeric"); + + b.Property("HalfMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfPermanentPrice") + .HasColumnType("numeric"); + + b.Property("KidCutPrice") + .HasColumnType("numeric"); + + b.Property("LongBalayagePrice") + .HasColumnType("numeric"); + + b.Property("LongBrushingPrice") + .HasColumnType("numeric"); + + b.Property("LongColorPrice") + .HasColumnType("numeric"); + + b.Property("LongDefrisPrice") + .HasColumnType("numeric"); + + b.Property("LongFoldingPrice") + .HasColumnType("numeric"); + + b.Property("LongMechPrice") + .HasColumnType("numeric"); + + b.Property("LongMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("LongPermanentPrice") + .HasColumnType("numeric"); + + b.Property("ManBrushPrice") + .HasColumnType("numeric"); + + b.Property("ManCutPrice") + .HasColumnType("numeric"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.Property("ShampooPrice") + .HasColumnType("numeric"); + + b.Property("ShortBalayagePrice") + .HasColumnType("numeric"); + + b.Property("ShortBrushingPrice") + .HasColumnType("numeric"); + + b.Property("ShortColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortDefrisPrice") + .HasColumnType("numeric"); + + b.Property("ShortFoldingPrice") + .HasColumnType("numeric"); + + b.Property("ShortMechPrice") + .HasColumnType("numeric"); + + b.Property("ShortMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortPermanentPrice") + .HasColumnType("numeric"); + + b.Property("WomenHalfCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenLongCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenShortCutPrice") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.HasIndex("ScheduleOwnerId"); + + b.ToTable("BrusherProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Cares") + .HasColumnType("boolean"); + + b.Property("Cut") + .HasColumnType("boolean"); + + b.Property("Dressing") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Length") + .HasColumnType("integer"); + + b.Property("Shampoo") + .HasColumnType("boolean"); + + b.Property("Tech") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("HairPrestation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("QueryId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PrestationId"); + + b.HasIndex("QueryId"); + + b.ToTable("HairPrestationCollectionItem"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Brand") + .HasColumnType("text"); + + b.Property("ColorId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ColorId"); + + b.ToTable("HairTaint"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.Property("TaintId") + .HasColumnType("bigint"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.HasKey("TaintId", "PrestationId"); + + b.HasIndex("PrestationId"); + + b.ToTable("HairTaintInstance"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Evolution.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ShortName") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Feature"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(10240) + .HasColumnType("character varying(10240)"); + + b.Property("FeatureId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.ToTable("Bug"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("DeclarationDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("LOCALTIMESTAMP"); + + b.Property("DeviceOwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("LatestActivityUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Model") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DeviceId"); + + b.HasIndex("DeviceOwnerId"); + + b.ToTable("DeviceDeclaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("MatchExcerpt") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PatternId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("PatternId"); + + b.ToTable("DeclarationFlag"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("ModeratorId") + .HasColumnType("text"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("ModeratorId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ModerationLogs", t => + { + t.HasCheckConstraint("CK_ModerationLog_Immutable", "1=1"); + }); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.RegexAlertPattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Pattern") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("RegexAlertPatterns"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DeclarantTokenId") + .HasColumnType("uuid"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Sentiment") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrustTokenId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("SubmittedAt"); + + b.HasIndex("TrustTokenId"); + + b.ToTable("TrustDeclarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TokenSource") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TrustScore") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.ToTable("TrustTokens"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Depth") + .HasColumnType("numeric"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("numeric"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.Property("Weight") + .HasColumnType("numeric"); + + b.Property("Width") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContextId") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ContextId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("For") + .HasColumnType("smallint"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Sender") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Announce"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("NotificationId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "NotificationId"); + + b.HasIndex("NotificationId"); + + b.ToTable("DismissClicked"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Instrument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("Instrument"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("InstrumentId", "OwnerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("InstrumentRating"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.Property("OwnerProfileId") + .HasColumnType("text"); + + b.Property("DjSettingsUserId") + .HasColumnType("text"); + + b.Property("MusicLoverSettingsUserId") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("TendencyId") + .HasColumnType("bigint"); + + b.HasKey("OwnerProfileId"); + + b.HasIndex("DjSettingsUserId"); + + b.HasIndex("MusicLoverSettingsUserId"); + + b.HasIndex("TendencyId"); + + b.ToTable("MusicalPreference"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalTendency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SoundCloudId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("DjSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("InstrumentId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("Instrumentation"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("MusicLoverSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Property("CreationToken") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ExecutorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderReference") + .HasColumnType("text"); + + b.Property("PaypalPayerId") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("CreationToken"); + + b.HasIndex("ExecutorId"); + + b.ToTable("PayPalPayment"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Circle"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.Property("MemberId") + .HasColumnType("text"); + + b.Property("CircleId") + .HasColumnType("bigint"); + + b.HasKey("MemberId", "CircleId"); + + b.HasIndex("CircleId"); + + b.ToTable("CircleMembers"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("OwnerId", "UserId"); + + b.HasIndex("AddressId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Contact"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.Property("HRef") + .HasColumnType("text"); + + b.Property("Method") + .HasColumnType("text"); + + b.Property("BrusherProfileUserId") + .HasColumnType("text"); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("PayPalPaymentCreationToken") + .HasColumnType("text"); + + b.Property("Rel") + .HasColumnType("text"); + + b.HasKey("HRef", "Method"); + + b.HasIndex("BrusherProfileUserId"); + + b.HasIndex("PayPalPaymentCreationToken"); + + b.ToTable("HyperLink"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.PostalAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("Street1") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SiteSkills"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DifferedFileName") + .HasColumnType("text"); + + b.Property("MediaType") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Pitch") + .HasColumnType("text"); + + b.Property("SequenceNumber") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("LiveFlow"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Hidden") + .HasColumnType("boolean"); + + b.Property("Moderated") + .HasColumnType("boolean"); + + b.Property("ModeratorGroupName") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCode") + .HasColumnType("text"); + + b.Property("Photo") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SettingsClassName") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code"); + + b.HasIndex("ParentCode"); + + b.ToTable("Activities"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FormationSettingsUserId") + .HasColumnType("text"); + + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("WorkingForId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FormationSettingsUserId"); + + b.HasIndex("PerformerId"); + + b.HasIndex("WorkingForId"); + + b.ToTable("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionName") + .HasColumnType("text"); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.ToTable("CommandForm"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Country", b => + { + b.Property("Code") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Code"); + + b.ToTable("Countries"); + + b.HasData( + new + { + Code = "fr", + DisplayName = "France" + }, + new + { + Code = "en", + DisplayName = "England" + }, + new + { + Code = "pt", + DisplayName = "Portugal" + }); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.DictionnaireMetier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DomaineActiviteCode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Langue") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Nom") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("DomaineActiviteCode", "Langue", "Nom") + .IsUnique(); + + b.ToTable("DictionnaireMetier"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerCodeInputValidation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CountryCode") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("ErrorMessage") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RegularExpression") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("CountryCode"); + + b.ToTable("PerformerCodeInputValidations"); + + b.HasData( + new + { + Id = 1L, + CountryCode = "fr", + ErrorMessage = "Le code FR doit contenir entre 9 et 14 chiffres.", + RegularExpression = "^[0-9]{9,14}$" + }, + new + { + Id = 2L, + CountryCode = "en", + ErrorMessage = "Le code EN doit contenir entre 8 et 14 caracteres alphanumeriques.", + RegularExpression = "^[A-Za-z0-9]{8,14}$" + }, + new + { + Id = 3L, + CountryCode = "pt", + ErrorMessage = "Le code PT doit contenir exactement 9 chiffres.", + RegularExpression = "^[0-9]{9}$" + }); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("AcceptNotifications") + .HasColumnType("boolean"); + + b.Property("AcceptPublicContact") + .HasColumnType("boolean"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ExerciseCountryCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaxDailyCost") + .HasColumnType("integer"); + + b.Property("MinDailyCost") + .HasColumnType("integer"); + + b.Property("OrganizationAddressId") + .HasColumnType("bigint"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SIREN") + .IsRequired() + .HasColumnType("text"); + + b.Property("UseGeoLocalizationToReduceDistanceWithClients") + .HasColumnType("boolean"); + + b.Property("WebSite") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("PerformerId"); + + b.HasIndex("OrganizationAddressId"); + + b.ToTable("Performers"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("FormationSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.TermeMetier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateSoumission") + .HasColumnType("timestamp with time zone"); + + b.Property("DateValidation") + .HasColumnType("timestamp with time zone"); + + b.Property("Definition") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DictionnaireMetierId") + .HasColumnType("bigint"); + + b.Property("Langue") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Mot") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProposeParId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("StatutValidation") + .HasColumnType("integer"); + + b.Property("ValideParId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("ProposeParId"); + + b.HasIndex("ValideParId"); + + b.HasIndex("DictionnaireMetierId", "Langue", "Mot") + .IsUnique(); + + b.ToTable("TermeMetier"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.Property("DoesCode") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Weight") + .HasColumnType("integer"); + + b.HasKey("DoesCode", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("UserActivities"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.Calendar.Period", b => + { + b.Property("Start") + .HasColumnType("timestamp with time zone"); + + b.Property("End") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Start", "End"); + + b.ToTable("Period"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.EMailing.MailingTemplate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(65536) + .HasColumnType("character varying(65536)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplyToAddress") + .HasColumnType("text"); + + b.Property("ToSend") + .HasColumnType("integer"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("MailingTemplate"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProjectBuildConfiguration"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Branch") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("GitRepositoryReference"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("AdditionalInfo") + .IsRequired() + .HasColumnType("text"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("SelectedProfileUserId") + .HasColumnType("text"); + + b.HasIndex("LocationId"); + + b.HasIndex("PrestationId"); + + b.HasIndex("SelectedProfileUserId"); + + b.HasDiscriminator().HasValue("HairCutQuery"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.HasIndex("LocationId"); + + b.ToTable("NominativeServiceCommands", t => + { + t.Property("EventDate") + .HasColumnName("HairMultiCutQuery_EventDate"); + + t.Property("LocationId") + .HasColumnName("HairMultiCutQuery_LocationId"); + }); + + b.HasDiscriminator().HasValue("HairMultiCutQuery"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("LocationType") + .HasColumnType("integer"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasIndex("LocationId"); + + b.ToTable("NominativeServiceCommands", t => + { + t.Property("EventDate") + .HasColumnName("RdvQuery_EventDate"); + + t.Property("LocationId") + .HasColumnName("RdvQuery_LocationId"); + }); + + b.HasDiscriminator().HasValue("RdvQuery"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("GitId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasIndex("GitId"); + + b.HasDiscriminator().HasValue("Project"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("UserClaims") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Properties") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Scopes") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Secrets") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("UserClaims") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("Properties") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("Claims") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("AllowedCorsOrigins") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedGrantTypes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("IdentityProviderRestrictions") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("PostLogoutRedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("Properties") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("RedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedScopes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("ClientSecrets") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("UserClaims") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("Properties") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "TargetUser") + .WithMany() + .HasForeignKey("TargetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetUser"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("BlackList") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Target") + .WithMany("ACL") + .HasForeignKey("BlogPostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Circle", "Allowed") + .WithMany() + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Allowed"); + + b.Navigation("Target"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToFile", b => + { + b.HasOne("Yavsc.Models.Relationship.Circle", "Allowed") + .WithMany() + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Allowed"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithOne("AccountBalance") + .HasForeignKey("Yavsc.Models.AccountBalance", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "PostalAddress") + .WithMany() + .HasForeignKey("PostalAddressId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.HasOne("Yavsc.Models.AccountBalance", "Balance") + .WithMany() + .HasForeignKey("BalanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Balance"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("BankInfo") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", null) + .WithMany("Bill") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Billing.EstimateTemplate", null) + .WithMany("Bill") + .HasForeignKey("EstimateTemplateId"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Billing.NominativeServiceCommand", "Query") + .WithMany() + .HasForeignKey("CommandId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Owner"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.NominativeServiceCommand", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Regularization"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", "Estimate") + .WithMany("Signatures") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Signer") + .WithMany() + .HasForeignKey("SignerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Estimate"); + + b.Navigation("Signer"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.HasOne("Yavsc.Models.Blog.UploadedFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("Posts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Tags") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("BlogComments") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.Comment", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Comments") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Parent"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithOne("Publication") + .HasForeignKey("Yavsc.Models.BlogSpotPublication", "PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", null) + .WithMany("Events") + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Server.Models.Calendar.Period", "Period") + .WithMany() + .HasForeignKey("PeriodStart", "PeriodEnd"); + + b.Navigation("Period"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Connections") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Rooms") + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.HasOne("Yavsc.Models.Chat.ChatRoom", "Room") + .WithMany("Moderation") + .HasForeignKey("ChannelName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("RoomAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "BaseProfile") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseProfile"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairMultiCutQuery", "Query") + .WithMany("Prestations") + .HasForeignKey("QueryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.HasOne("Yavsc.Models.Drawing.Color", "Color") + .WithMany() + .HasForeignKey("ColorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany("Taints") + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairTaint", "Taint") + .WithMany() + .HasForeignKey("TaintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Taint"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.HasOne("Yavsc.Models.IT.Evolution.Feature", "False") + .WithMany() + .HasForeignKey("FeatureId"); + + b.Navigation("False"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "DeviceOwner") + .WithMany("DeviceDeclaration") + .HasForeignKey("DeviceOwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceOwner"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany("Flags") + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Kyc.RegexAlertPattern", "Pattern") + .WithMany() + .HasForeignKey("PatternId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + + b.Navigation("Pattern"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany() + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustToken", "Subject") + .WithMany("Declarations") + .HasForeignKey("TrustTokenId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Subject"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Services") + .HasForeignKey("ContextId"); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.HasOne("Yavsc.Abstract.Models.Messaging.Notification", "Notified") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notified"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Instrument") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Profile") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instrument"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.HasOne("Yavsc.Models.Musical.Profiles.DjSettings", null) + .WithMany("SoundColor") + .HasForeignKey("DjSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.Profiles.MusicLoverSettings", null) + .WithMany("SoundColor") + .HasForeignKey("MusicLoverSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.MusicalTendency", "MusicalTendency") + .WithMany() + .HasForeignKey("TendencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Tool") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tool"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Executor") + .WithMany() + .HasForeignKey("ExecutorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Executor"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Circles") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.HasOne("Yavsc.Models.Relationship.Circle", "Circle") + .WithMany("Members") + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Member") + .WithMany("Membership") + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Circle"); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.HasOne("Yavsc.Models.Relationship.PostalAddress", "PostalAddress") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Book") + .HasForeignKey("ApplicationUserId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", null) + .WithMany("Links") + .HasForeignKey("BrusherProfileUserId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", null) + .WithMany("Links") + .HasForeignKey("PayPalPaymentCreationToken"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentCode"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.HasOne("Yavsc.Models.Workflow.Profiles.FormationSettings", null) + .WithMany("CoWorking") + .HasForeignKey("FormationSettingsUserId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Performer") + .WithMany() + .HasForeignKey("PerformerId"); + + b.HasOne("Yavsc.Models.ApplicationUser", "WorkingFor") + .WithMany() + .HasForeignKey("WorkingForId"); + + b.Navigation("Performer"); + + b.Navigation("WorkingFor"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Forms") + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.DictionnaireMetier", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "DomaineActivite") + .WithMany() + .HasForeignKey("DomaineActiviteCode") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DomaineActivite"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerCodeInputValidation", b => + { + b.HasOne("Yavsc.Models.Workflow.Country", "Country") + .WithMany() + .HasForeignKey("CountryCode") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "OrganizationAddress") + .WithMany() + .HasForeignKey("OrganizationAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Performer") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationAddress"); + + b.Navigation("Performer"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.TermeMetier", b => + { + b.HasOne("Yavsc.Models.Workflow.DictionnaireMetier", "DictionnaireMetier") + .WithMany("Termes") + .HasForeignKey("DictionnaireMetierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "ProposePar") + .WithMany() + .HasForeignKey("ProposeParId"); + + b.HasOne("Yavsc.Models.ApplicationUser", "ValidePar") + .WithMany() + .HasForeignKey("ValideParId"); + + b.Navigation("DictionnaireMetier"); + + b.Navigation("ProposePar"); + + b.Navigation("ValidePar"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Does") + .WithMany() + .HasForeignKey("DoesCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany("Activity") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Does"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.HasOne("Yavsc.Server.Models.IT.Project", "TargetProject") + .WithMany("Configurations") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetProject"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId"); + + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", "SelectedProfile") + .WithMany() + .HasForeignKey("SelectedProfileUserId"); + + b.Navigation("Location"); + + b.Navigation("Prestation"); + + b.Navigation("SelectedProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.HasOne("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", "Repository") + .WithMany() + .HasForeignKey("GitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Repository"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b => + { + b.Navigation("Properties"); + + b.Navigation("Scopes"); + + b.Navigation("Secrets"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b => + { + b.Navigation("AllowedCorsOrigins"); + + b.Navigation("AllowedGrantTypes"); + + b.Navigation("AllowedScopes"); + + b.Navigation("Claims"); + + b.Navigation("ClientSecrets"); + + b.Navigation("IdentityProviderRestrictions"); + + b.Navigation("PostLogoutRedirectUris"); + + b.Navigation("Properties"); + + b.Navigation("RedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Navigation("AccountBalance"); + + b.Navigation("BankInfo"); + + b.Navigation("BlackList"); + + b.Navigation("BlogComments"); + + b.Navigation("Book"); + + b.Navigation("Circles"); + + b.Navigation("Connections"); + + b.Navigation("DeviceDeclaration"); + + b.Navigation("Membership"); + + b.Navigation("Posts"); + + b.Navigation("RoomAccess"); + + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Navigation("Bill"); + + b.Navigation("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Navigation("Bill"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Navigation("ACL"); + + b.Navigation("Comments"); + + b.Navigation("Publication") + .IsRequired(); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Navigation("Moderation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Navigation("Taints"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Navigation("Declarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Navigation("Children"); + + b.Navigation("Forms"); + + b.Navigation("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.DictionnaireMetier", b => + { + b.Navigation("Termes"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Navigation("Activity"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Navigation("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.Navigation("Prestations"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.Navigation("Configurations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Yavsc.Org/Migrations/20260921145454_DropSignatureFilePath.cs b/src/Yavsc.Org/Migrations/20260921145454_DropSignatureFilePath.cs new file mode 100644 index 000000000..d29c153cb --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260921145454_DropSignatureFilePath.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Yavsc.Migrations +{ + /// + public partial class DropSignatureFilePath : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FilePath", + table: "Signatures"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FilePath", + table: "Signatures", + type: "text", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs index 2ea15651e..2b8b98354 100644 --- a/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace Yavsc.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("ProductVersion", "10.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -1521,10 +1521,6 @@ namespace Yavsc.Migrations b.Property("EstimateId") .HasColumnType("bigint"); - b.Property("FilePath") - .IsRequired() - .HasColumnType("text"); - b.Property("SignerId") .IsRequired() .HasColumnType("text"); @@ -3959,8 +3955,8 @@ namespace Yavsc.Migrations modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => { b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") - .WithMany() - .HasForeignKey("PostId") + .WithOne("Publication") + .HasForeignKey("Yavsc.Models.BlogSpotPublication", "PostId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -4614,6 +4610,9 @@ namespace Yavsc.Migrations b.Navigation("Comments"); + b.Navigation("Publication") + .IsRequired(); + b.Navigation("Tags"); }); diff --git a/src/Yavsc.Server/Helpers/EstimateSignatureFileHelper.cs b/src/Yavsc.Server/Helpers/EstimateSignatureFileHelper.cs deleted file mode 100644 index 29062bc3c..000000000 --- a/src/Yavsc.Server/Helpers/EstimateSignatureFileHelper.cs +++ /dev/null @@ -1,158 +0,0 @@ -#nullable enable annotations - -using System.Security.Claims; -using System.Text; -using System.Text.Json; -using Microsoft.AspNetCore.Http; -using Yavsc.Models.Billing; -using Yavsc.Server.Models.FileSystem; -namespace Yavsc.Server.Helpers; - -/// -/// Filesystem counterpart of : writes -/// the wire-format JSON payload next to the user's other files, -/// under signatures/, and updates the user's disk quota. -/// -/// This is the JSON counterpart of the legacy -/// ReceiveProSignatureAsync method, which stored -/// sign-{billingCode}-{signType}-{estimateId}.png blobs. -/// We don't reuse that helper because (a) the wire format is no -/// longer a binary image, (b) there's no billingCode on -/// a freshly signed estimate in our model, and (c) the legacy -/// helper takes an whereas our pipeline -/// decodes a JSON body upstream of the controller and passes -/// in directly. -/// -public static class EstimateSignatureFileHelper -{ - /// - /// Sub-directory under the user's root where signature - /// payloads live. Kept short to leave room in PATH_MAX on - /// legacy filesystems; the rest of the filename is - /// sign-{type}-{estimateId}-{utcTicks}.json. - /// - public const string SignaturesSubdir = "signatures"; - - /// - /// Format string for signature file names. Public so the - /// migration and the admin tools can list by pattern. - /// - public static string FileNameFormat(SignatureType type, long estimateId, long utcTicks) - => $"sign-{type.ToString().ToLowerInvariant()}-{estimateId}-{utcTicks}.json"; - - /// - /// Persist a signature wire payload to disk. Returns the - /// file info (relative path under the user's root) suitable - /// for storing in ; the - /// caller is responsible for the database write. - /// - /// Signed-in user. Their - /// Identity.Name locates the disk root via - /// . - /// - /// Estimate this signature - /// attaches to. Used in the file name for human inspection - /// and to support multiple versions over time. - /// Provider or client signature. - /// Decoded wire payload (strokes + - /// coordinateMax + capturedAtUtc). Already validated - /// upstream. - /// Cancellation token forwarded to - /// the file write. - public static async Task ReceiveEstimateSignatureAsync( - this ClaimsPrincipal user, - long estimateId, - SignatureType type, - SignaturePadPayload payload, - SiteSettings siteSettings, - CancellationToken token = default) - { - if (user is null) throw new ArgumentNullException(nameof(user)); - if (payload is null) throw new ArgumentNullException(nameof(payload)); - if (estimateId <= 0) throw new ArgumentOutOfRangeException(nameof(estimateId)); - - // Ensure the user has a /signatures/ sub-directory we can - // write to. EnsureDestinationDirectory throws on invalid - // paths and creates the directory on the way; the - // SignaturesSubdir constant is a server-controlled value - // (not user-derived), so we skip the IsValidYavscPath - // check that ReceiveUserFile performs on user-supplied - // subpaths. - var root = user.EnsureDestinationDirectory(SignaturesSubdir, siteSettings); - - var fileName = FileNameFormat(type, estimateId, DateTime.UtcNow.Ticks); - var fullPath = Path.Combine(root, fileName); - - var envelope = new - { - format = "yavsc.signature/v1", - coordinateMax = payload.CoordinateMax, - capturedAtUtc = payload.CapturedAtUtc, - estimateId, - type = type.ToString(), - // Identity.Name is the username; we keep the wire - // payload keyed on the username rather than the - // numeric/guid Id so disk-side human inspection - // (e.g. cat sign-pro-1234-...json) is self-evident. - signerName = user.Identity?.Name, - strokes = payload.Strokes, - strokeCount = CountStrokes(payload.Strokes), - }; - - var json = JsonSerializer.Serialize(envelope, new JsonSerializerOptions { WriteIndented = true }); - await File.WriteAllTextAsync(fullPath, json, Encoding.UTF8, token).ConfigureAwait(false); - - // Quota update is the controller's responsibility: the - // helper has no DbContext access, and a ClaimsPrincipal - // is not an ApplicationUser. The controller looks up - // the user by Identity.Name and bumps DiskUsage after - // a successful database write. - - return new FileReceivedInfo(root, fileName); - } - - private static int CountStrokes(int[] strokes) - { - int n = 0; - for (int i = 0; i < strokes.Length;) - { - int k = strokes[i]; - if (k <= 0) break; - n++; - i += 1 + 2 * k; - } - return n; - } -} - -/// -/// Wire payload accepted by the signature endpoint and -/// persisted by . -/// Mirrors PostIt.Models.SignaturePadData's JSON shape -/// (without the disk-only envelope fields) so the two sides -/// stay trivially compatible. -/// -public class SignaturePadPayload -{ - /// - /// Normalised coordinate upper bound. Must be - /// PostIt.Models.SignaturePadData.CoordinateMax - /// (10_000) today; declared as a property so a future - /// resolution change can be replayed against the same - /// wire format. - /// - public int CoordinateMax { get; set; } = 10_000; - - /// - /// Client-reported capture time. The server may ignore - /// this for ordering (UTC now is the truth) but keeps it - /// for round-trip display. - /// - public DateTime CapturedAtUtc { get; set; } - - /// - /// Wire strokes. See - /// PostIt.Models.SignaturePadData for the format. - /// - public int[] Strokes { get; set; } = Array.Empty(); -} diff --git a/src/Yavsc.Server/Helpers/EstimateSignaturePersister.cs b/src/Yavsc.Server/Helpers/EstimateSignaturePersister.cs new file mode 100644 index 000000000..be8848e11 --- /dev/null +++ b/src/Yavsc.Server/Helpers/EstimateSignaturePersister.cs @@ -0,0 +1,111 @@ +#nullable enable annotations + +using System.Security.Claims; +using Microsoft.EntityFrameworkCore; +using Yavsc.Models; +using Yavsc.Models.Billing; + +namespace Yavsc.Server.Helpers; + +/// +/// Shared signature-persistence logic for estimates, extracted from +/// BillingController.Sign. Stages the +/// row (find-or-add by (EstimateId, Type)) for the PostIt +/// wire-format payload — but does not call +/// SaveChanges. The caller owns the transaction so it can +/// combine the signature write with its own changes (e.g. setting a +/// validation date or a query status) in a single +/// SaveChanges. +/// +/// +/// The column is the source of +/// truth; there is no on-disk copy, so no disk-quota accounting is +/// involved. +/// +/// +public static class EstimateSignaturePersister +{ + /// + /// Stage a signature for signed by + /// as . Upserts + /// the row for the + /// (EstimateId, Type) pair, writing the wire payload + /// (Strokes, CoordinateMax, CapturedAtUtc) + /// and . Returns the staged + /// ; the caller persists with + /// SaveChanges. + /// + public static async Task StageAsync( + ApplicationDbContext db, + long estimateId, + SignatureType type, + string signerId, + SignaturePadPayload payload, + CancellationToken token = default) + { + if (db is null) throw new ArgumentNullException(nameof(db)); + if (payload is null) throw new ArgumentNullException(nameof(payload)); + if (estimateId <= 0) throw new ArgumentOutOfRangeException(nameof(estimateId)); + + // Find-or-add: the (EstimateId, Type) pair is unique, so a + // second submission for the same side of the estimate + // replaces the previous signature. EF translates this into a + // single UPDATE when the row exists and an INSERT otherwise; + // the unique index in ApplicationDbContext is the + // database-level guarantee that the contract holds if two + // requests race. + var signature = await db.Signatures + .FirstOrDefaultAsync(s => s.EstimateId == estimateId && s.Type == type, token) + .ConfigureAwait(false); + + if (signature is null) + { + signature = new Signature + { + EstimateId = estimateId, + SignerId = signerId, + Type = type, + }; + db.Signatures.Add(signature); + } + + signature.SignerId = signerId; + signature.CoordinateMax = payload.CoordinateMax; + signature.Strokes = payload.Strokes; + signature.CapturedAtUtc = payload.CapturedAtUtc; + + return signature; + } +} + +/// +/// Wire payload accepted by the signature path and staged by +/// . Mirrors +/// PostIt.Models.SignaturePadData's JSON shape (without the +/// disk-only envelope fields) so the two sides stay trivially +/// compatible. +/// +public class SignaturePadPayload +{ + /// + /// Normalised coordinate upper bound. Must be + /// PostIt.Models.SignaturePadData.CoordinateMax + /// (10_000) today; declared as a property so a future + /// resolution change can be replayed against the same + /// wire format. + /// + public int CoordinateMax { get; set; } = 10_000; + + /// + /// Client-reported capture time. The server may ignore + /// this for ordering (UTC now is the truth) but keeps it + /// for round-trip display. + /// + public DateTime CapturedAtUtc { get; set; } + + /// + /// Wire strokes. See + /// PostIt.Models.SignaturePadData for the format. + /// + public int[] Strokes { get; set; } = Array.Empty(); +} \ No newline at end of file diff --git a/src/Yavsc.Server/Models/Billing/Signature.cs b/src/Yavsc.Server/Models/Billing/Signature.cs index 10da97f1b..1aac0cd0e 100644 --- a/src/Yavsc.Server/Models/Billing/Signature.cs +++ b/src/Yavsc.Server/Models/Billing/Signature.cs @@ -7,8 +7,8 @@ namespace Yavsc.Models.Billing; /// /// One captured signature, attached to a single /// . Multiple versions are allowed per -/// (EstimateId, Type, SignerId) tuple — the controller reads -/// the most recent when asked. The wire-format payload is the +/// (EstimateId, Type, SignerId) tuple — the controller reads the +/// most recent when asked. The wire-format payload is the /// same int[] shape PostIt produces (see /// PostIt.Models.SignaturePadData): a length-prefixed /// sequence of strokes, each stroke being @@ -16,13 +16,12 @@ namespace Yavsc.Models.Billing; /// CoordinateMax]. /// /// -/// Why a separate table (instead of a JSON column on -/// ): the jalon 1 spec calls for at least -/// two distinct signatures per estimate cycle (provider -/// validation + client agreement) and the audit value of -/// preserving superseded versions. A dedicated table also keeps -/// the row narrow, which matters for -/// list views. +/// The column is the source of truth: the +/// signature is captured and stored entirely in-app (PostIt) and +/// round-tripped as JSON, with no on-disk copy. A dedicated table +/// (instead of a JSON column on ) keeps the +/// row narrow for list views and preserves +/// superseded versions for audit. /// /// public class Signature @@ -75,16 +74,4 @@ public class Signature public int[] Strokes { get; set; } = Array.Empty(); public DateTime CapturedAtUtc { get; set; } - - /// - /// Path to the JSON-serialised wire payload on disk, - /// relative to UserFilesDirName. The disk copy is the - /// source of truth for the wire bytes; the - /// column is a denormalised index for queries. They are - /// written together in the same transaction by the - /// controller; the migration should keep them in sync - /// through ApplicationDbContext.SaveChanges. - /// - [Required] - public string FilePath { get; set; } -} +} \ No newline at end of file From 2ad7a55e34da8a452d2d5ce58a6f5b3737a72ee8 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 21 Sep 2026 16:15:31 +0100 Subject: [PATCH 21/66] Add missing Account/Delete confirmation page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The POST Delete(UnregisterViewModel) action and the GET Delete() action both existed in AccountController, but Views/Account/Delete.cshtml was missing — so GET /Account/Delete had no page to render. Add the confirmation view (posts back to Delete with the user's UserId, with a "Confirmer la suppression" button and an "Annuler" cancel link). Add a smoke test: an authenticated GET /Account/Delete returns 200 and the confirmation page body. Co-Authored-By: Claude Code --- .../Smoke/AccountSmokeTests.cs | 23 +++++++++++++++++++ .../Accounting/AccountController.cs | 1 + src/Yavsc.Org/Views/Account/Delete.cshtml | 21 +++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 src/Yavsc.Org/Views/Account/Delete.cshtml diff --git a/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs b/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs index 5e7ab7776..4a7600596 100644 --- a/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs +++ b/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs @@ -3,6 +3,7 @@ using System.Net.Http; using IdentityServer8.Stores; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; +using Yavsc.Tests.Shared; using Xunit; namespace Yavsc.Org.Tests.Smoke; @@ -65,4 +66,26 @@ public class AccountSmokeTests : IClassFixture Assert.Equal("/signin", location.AbsolutePath); Assert.Contains("ReturnUrl=%2FEstimate", location.Query); } + + // Regression: the POST Delete action existed, but neither the GET + // Delete view nor a rendered confirmation page was reachable — + // Views/Account/Delete.cshtml was missing. An authenticated GET + // must now return the confirmation page (200). + [Fact] + public async Task Authenticated_get_account_delete_returns_confirmation_page() + { + var client = _factory.CreateClient(new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + }); + client.DefaultRequestHeaders.Add( + TestAuthPolicyProvider.HeaderName, TestAuthPolicyProvider.AdminRole); + + var response = await client.GetAsync("/Account/Delete", TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync( + TestContext.Current.CancellationToken); + Assert.Contains("Supprimer mon compte", body); + } } diff --git a/src/Yavsc.Org/Controllers/Accounting/AccountController.cs b/src/Yavsc.Org/Controllers/Accounting/AccountController.cs index 04f60aa1d..f8fa7f0d2 100644 --- a/src/Yavsc.Org/Controllers/Accounting/AccountController.cs +++ b/src/Yavsc.Org/Controllers/Accounting/AccountController.cs @@ -1061,6 +1061,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, } return View(); } + [HttpPost, Authorize] public async Task Delete(UnregisterViewModel model) { diff --git a/src/Yavsc.Org/Views/Account/Delete.cshtml b/src/Yavsc.Org/Views/Account/Delete.cshtml new file mode 100644 index 000000000..7443d3d2a --- /dev/null +++ b/src/Yavsc.Org/Views/Account/Delete.cshtml @@ -0,0 +1,21 @@ +@model UnregisterViewModel +@{ + ViewBag.Title = "Supprimer mon compte"; +} + +

    @ViewBag.Title

    + +
    +
    +

    + Cette action est immédiate et irréversible. Toutes les données + associées à votre compte seront supprimées, et vous serez déconnecté. +

    + +
    + @Html.Hidden("UserId") + + Annuler + +
    +
    \ No newline at end of file From 36ffe4683c42735a4cc4f3b6591e71ba65f127e8 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 21 Sep 2026 17:33:18 +0100 Subject: [PATCH 22/66] PostIt: validate/refuse an estimate with a signature (pro or client) Adds the client-side ability to open an estimate from the list, draw a signature on a reusable SignaturePadView control, and accept or reject it against /api/v1/front/query/accept|reject. The server resolves the actor role (provider vs client) from the caller's identity, so the same page works for either party. - Yavsc.Api.Client: QueryAcceptanceDto + FrontOfficeApiClient (accept/reject) - PostIt: extract SignaturePadView UserControl from SignaturePage; new EstimateValidationPage (VM + view) with Valider/Refuser/Effacer/Retour; wire OpenEstimate entry point from EstimateListPage; register client/page in DI + ViewLocator - Tests: FrontOfficeApiClient + EstimateValidationPageViewModel unit tests; update EstimateListPageViewModelTests ctor Co-Authored-By: Claude Code --- .../EstimateListPageViewModelTests.cs | 12 +- .../EstimateValidationPageViewModelTests.cs | 166 ++++++++++++ .../PostIt.Tests/FrontOfficeApiClientTests.cs | 121 +++++++++ .../PostIt/Controls/SignaturePadView.axaml | 28 ++ .../PostIt/Controls/SignaturePadView.axaml.cs | 168 ++++++++++++ .../Helpers/ServiceCollectionHelpers.cs | 3 + src/PostIt/PostIt/ViewLocator.cs | 1 + .../Activity/EstimateListPageViewModel.cs | 29 +- .../EstimateValidationPageViewModel.cs | 256 ++++++++++++++++++ .../ViewModels/Layout/HomePageViewModel.cs | 8 +- .../Signature/SignaturePageViewModel.cs | 10 +- .../Views/Activity/EstimateListPage.axaml | 60 ++-- .../Activity/EstimateValidationPage.axaml | 126 +++++++++ .../Activity/EstimateValidationPage.axaml.cs | 32 +++ .../Views/Signature/SignaturePage.axaml | 30 +- .../Views/Signature/SignaturePage.axaml.cs | 98 +------ .../Dtos/QueryAcceptanceDto.cs | 44 +++ src/Yavsc.Api.Client/FrontOfficeApiClient.cs | 108 ++++++++ 18 files changed, 1149 insertions(+), 151 deletions(-) create mode 100644 src/PostIt/PostIt.Tests/EstimateValidationPageViewModelTests.cs create mode 100644 src/PostIt/PostIt.Tests/FrontOfficeApiClientTests.cs create mode 100644 src/PostIt/PostIt/Controls/SignaturePadView.axaml create mode 100644 src/PostIt/PostIt/Controls/SignaturePadView.axaml.cs create mode 100644 src/PostIt/PostIt/ViewModels/Activity/EstimateValidationPageViewModel.cs create mode 100644 src/PostIt/PostIt/Views/Activity/EstimateValidationPage.axaml create mode 100644 src/PostIt/PostIt/Views/Activity/EstimateValidationPage.axaml.cs create mode 100644 src/Yavsc.Api.Client/Dtos/QueryAcceptanceDto.cs create mode 100644 src/Yavsc.Api.Client/FrontOfficeApiClient.cs diff --git a/src/PostIt/PostIt.Tests/EstimateListPageViewModelTests.cs b/src/PostIt/PostIt.Tests/EstimateListPageViewModelTests.cs index 6158b4132..ed66f11e4 100644 --- a/src/PostIt/PostIt.Tests/EstimateListPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/EstimateListPageViewModelTests.cs @@ -11,7 +11,8 @@ public class EstimateListPageViewModelTests { var api = new StubEstimateListApi(); var client = new EstimateApiClient(api, "https://business.example/api/v1/"); - var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Client); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Client, front); await vm.InitializeAsync(); @@ -28,7 +29,8 @@ public class EstimateListPageViewModelTests { var api = new StubEstimateListApi(); var client = new EstimateApiClient(api, "https://business.example/api/v1/"); - var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Provider); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Provider, front); await vm.InitializeAsync(); @@ -42,7 +44,8 @@ public class EstimateListPageViewModelTests { var api = new StubEstimateListApi(); var client = new EstimateApiClient(api, "https://business.example/api/v1/"); - var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Provider); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Provider, front); await vm.InitializeAsync(); @@ -63,7 +66,8 @@ public class EstimateListPageViewModelTests { var api = new StubEstimateListApi { Empty = true }; var client = new EstimateApiClient(api, "https://business.example/api/v1/"); - var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Client); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Client, front); await vm.InitializeAsync(); diff --git a/src/PostIt/PostIt.Tests/EstimateValidationPageViewModelTests.cs b/src/PostIt/PostIt.Tests/EstimateValidationPageViewModelTests.cs new file mode 100644 index 000000000..27e4456f7 --- /dev/null +++ b/src/PostIt/PostIt.Tests/EstimateValidationPageViewModelTests.cs @@ -0,0 +1,166 @@ +using System; +using System.Net.Http; +using PostIt.Controls; +using PostIt.ViewModels; +using Yavsc.Api.Client; + +namespace PostIt.Tests; + +/// +/// Verifies wires the +/// signature pad + perspective into the right +/// call. The view (XAML, rendering) +/// is not tested here; the VM is driven through the same +/// test hooks the signature tests use, +/// and the API client through a recording fake +/// . The test host's +/// TestApp makes GoBackAsync a no-op, so the success paths +/// run to completion without navigation side effects. +/// +public class EstimateValidationPageViewModelTests +{ + private static EstimateDto SampleEstimate(long? commandId = 42) => new() + { + Id = 7, + CommandId = commandId, + CommandType = "Rdv", + Title = "Devis plomberie", + Description = "Réparation", + ClientId = "bob", + OwnerId = "alice", + }; + + private static void DrawOneStroke(SignaturePadControl pad) + { + pad.AppendPointForTest(100, 200); + pad.AppendPointForTest(300, 400); + pad.SealStrokeForTest(); + } + + [Fact] + public async Task Validate_calls_accept_with_strokes_and_command_id() + { + var api = new RecordingApi(); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateValidationPageViewModel( + SampleEstimate(), EstimateListPerspective.Provider, front); + + var pad = new SignaturePadControl(); + vm.Attach(pad); + DrawOneStroke(pad); + + await vm.ValidateAsync(); + + Assert.Equal(HttpMethod.Post, api.LastMethod); + Assert.Contains("front/query/accept", api.LastPath); + Assert.Contains("billingCode=Rdv", api.LastPath); + Assert.Contains("queryId=42", api.LastPath); + var body = Assert.IsType(api.LastBody); + Assert.NotNull(body.Strokes); + Assert.NotEmpty(body.Strokes); + Assert.Equal(10_000, body.CoordinateMax); + } + + [Fact] + public async Task Reject_calls_reject_endpoint_with_no_body() + { + var api = new RecordingApi(); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateValidationPageViewModel( + SampleEstimate(), EstimateListPerspective.Client, front); + + vm.Attach(new SignaturePadControl()); + + await vm.RejectAsync(); + + Assert.Equal(HttpMethod.Post, api.LastMethod); + Assert.Contains("front/query/reject", api.LastPath); + Assert.Contains("queryId=42", api.LastPath); + Assert.Null(api.LastBody); + } + + [Fact] + public async Task Validate_without_signature_warns_and_does_not_call() + { + var api = new RecordingApi(); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateValidationPageViewModel( + SampleEstimate(), EstimateListPerspective.Provider, front); + + vm.Attach(new SignaturePadControl()); // empty pad + + await vm.ValidateAsync(); // no throw: returns before GoBack + + Assert.Null(api.LastMethod); + Assert.Contains("signer", vm.StatusMessage, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Validate_command_gates_on_signature_and_command() + { + var api = new RecordingApi(); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateValidationPageViewModel( + SampleEstimate(commandId: null), EstimateListPerspective.Provider, front); + + var pad = new SignaturePadControl(); + vm.Attach(pad); + + // No CommandId ⇒ cannot validate even after signing. + DrawOneStroke(pad); + Assert.False(vm.ValidateCommand.CanExecute(null)); + + var vm2 = new EstimateValidationPageViewModel( + SampleEstimate(), EstimateListPerspective.Provider, front); + var pad2 = new SignaturePadControl(); + vm2.Attach(pad2); + Assert.False(vm2.ValidateCommand.CanExecute(null)); // no signature yet + DrawOneStroke(pad2); + Assert.True(vm2.ValidateCommand.CanExecute(null)); + } + + [Fact] + public void RoleLabel_reflects_perspective() + { + var front = new FrontOfficeApiClient(new RecordingApi(), "https://business.example/api/v1/"); + var pro = new EstimateValidationPageViewModel( + SampleEstimate(), EstimateListPerspective.Provider, front); + var cli = new EstimateValidationPageViewModel( + SampleEstimate(), EstimateListPerspective.Client, front); + + Assert.Contains("prestataire", pro.RoleLabel, StringComparison.OrdinalIgnoreCase); + Assert.Contains("client", cli.RoleLabel, StringComparison.OrdinalIgnoreCase); + } + + private sealed class RecordingApi : IYavscApiClient + { + public HttpClient Http { get; } = new(); + public HttpMethod? LastMethod { get; private set; } + public string? LastPath { get; private set; } + public object? LastBody { get; private set; } + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + LastMethod = method; + LastPath = path; + LastBody = body; + return Task.FromResult(default(T)!); + } + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + LastMethod = method; + LastPath = path; + LastBody = body; + return Task.CompletedTask; + } + + public Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) + => CallAsync(method, path, (object?)null, ct); + + public Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) + => CallAsync(method, path, (object?)null, ct); + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/PostIt/PostIt.Tests/FrontOfficeApiClientTests.cs b/src/PostIt/PostIt.Tests/FrontOfficeApiClientTests.cs new file mode 100644 index 000000000..a3c1e326b --- /dev/null +++ b/src/PostIt/PostIt.Tests/FrontOfficeApiClientTests.cs @@ -0,0 +1,121 @@ +using System.Net.Http; +using Yavsc.Api.Client; + +namespace PostIt.Tests; + +/// +/// Verifies issues the right +/// api/v1/front/query/accept / .../reject requests with +/// the PostIt signature payload, using a recording fake +/// (no network). +/// +public class FrontOfficeApiClientTests +{ + private static QueryAcceptanceRequestDto SampleBody() => new() + { + Strokes = new[] { 2, 100, 200, 300, 400 }, + CoordinateMax = 10_000, + CapturedAtUtc = new DateTime(2026, 9, 21, 12, 0, 0, DateTimeKind.Utc), + }; + + [Fact] + public async Task Accept_posts_to_accept_endpoint_with_signature_body() + { + var api = new RecordingApi(); + var client = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + + var body = SampleBody(); + await client.AcceptQueryAsync("Rdv", 42, body); + + Assert.Equal(HttpMethod.Post, api.LastMethod); + Assert.Equal( + "https://business.example/api/v1/front/query/accept?billingCode=Rdv&queryId=42", + api.LastPath); + Assert.Same(body, api.LastBody); + } + + [Fact] + public async Task Reject_posts_to_reject_endpoint_with_no_body() + { + var api = new RecordingApi(); + var client = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + + await client.RejectQueryAsync("Rdv", 42); + + Assert.Equal(HttpMethod.Post, api.LastMethod); + Assert.Equal( + "https://business.example/api/v1/front/query/reject?billingCode=Rdv&queryId=42", + api.LastPath); + Assert.Null(api.LastBody); + } + + [Fact] + public async Task Accept_returns_deserialized_response() + { + var api = new RecordingApi + { + Response = new QueryAcceptanceResponseDto + { + QueryId = 42, + Status = "Accepted", + Signature = new QuerySignatureRefDto { Id = 9, EstimateId = 5, Type = "Pro" }, + }, + }; + var client = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + + var response = await client.AcceptQueryAsync("Rdv", 42, SampleBody()); + + Assert.Equal(42, response.QueryId); + Assert.Equal("Accepted", response.Status); + Assert.NotNull(response.Signature); + Assert.Equal("Pro", response.Signature!.Type); + } + + [Fact] + public async Task Accept_rejects_invalid_arguments() + { + var api = new RecordingApi(); + var client = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + + await Assert.ThrowsAsync(() => client.AcceptQueryAsync("", 42, SampleBody())); + await Assert.ThrowsAsync(() => client.AcceptQueryAsync("Rdv", 0, SampleBody())); + await Assert.ThrowsAsync(() => client.AcceptQueryAsync("Rdv", 42, null!)); + await Assert.ThrowsAsync(() => client.RejectQueryAsync("", 42)); + await Assert.ThrowsAsync(() => client.RejectQueryAsync("Rdv", 0)); + } + + private sealed class RecordingApi : IYavscApiClient + { + public HttpClient Http { get; } = new(); + public HttpMethod? LastMethod { get; private set; } + public string? LastPath { get; private set; } + public object? LastBody { get; private set; } + public QueryAcceptanceResponseDto? Response { get; set; } + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + LastMethod = method; + LastPath = path; + LastBody = body; + + var response = Response ?? new QueryAcceptanceResponseDto(); + return Task.FromResult((T)(object)response); + } + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + LastMethod = method; + LastPath = path; + LastBody = body; + return Task.CompletedTask; + } + + public Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) + => CallAsync(method, path, (object?)null, ct); + + public Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) + => CallAsync(method, path, (object?)null, ct); + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/PostIt/PostIt/Controls/SignaturePadView.axaml b/src/PostIt/PostIt/Controls/SignaturePadView.axaml new file mode 100644 index 000000000..cb0a99de1 --- /dev/null +++ b/src/PostIt/PostIt/Controls/SignaturePadView.axaml @@ -0,0 +1,28 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/PostIt/PostIt/Controls/SignaturePadView.axaml.cs b/src/PostIt/PostIt/Controls/SignaturePadView.axaml.cs new file mode 100644 index 000000000..c54954ca1 --- /dev/null +++ b/src/PostIt/PostIt/Controls/SignaturePadView.axaml.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using PostIt.Models; + +namespace PostIt.Controls; + +/// +/// Reusable signature-capture surface: wraps the render-agnostic +/// in a sized +/// (the hit-test region) with an overlay that +/// the code-behind repaints from the control's stroke buffer. Owns +/// the ink-rendering logic so every page that captures a signature +/// (the dev-only SignaturePage, the estimate validation page, +/// ...) shares one implementation. +/// +/// Hosts bind / +/// (DIPs, defaults 600×200) to size the surface. View models attach +/// to the inner control (exposed here) to +/// read snapshots, drive Clear, and subscribe to +/// / +/// — the same +/// contract the headless tests use, since a +/// needs its XAML loaded to be drivable. +/// +public partial class SignaturePadView : UserControl +{ + private static readonly IBrush StrokeBrush = new SolidColorBrush(Color.FromRgb(0x10, 0x10, 0x10)); + private const double StrokeThickness = 2.0; + private const double CoordinateMax = 10_000.0; + + public static readonly StyledProperty CaptureWidthProperty = + AvaloniaProperty.Register(nameof(CaptureWidth), 600); + + public static readonly StyledProperty CaptureHeightProperty = + AvaloniaProperty.Register(nameof(CaptureHeight), 200); + + public double CaptureWidth + { + get => GetValue(CaptureWidthProperty); + set => SetValue(CaptureWidthProperty, value); + } + + public double CaptureHeight + { + get => GetValue(CaptureHeightProperty); + set => SetValue(CaptureHeightProperty, value); + } + + /// + /// The underlying capture control. View models attach to this to + /// read / call + /// and subscribe to its + /// events; this view repaints the ink layer from the same buffer. + /// + public SignaturePadControl SignaturePad => InnerPad; + + public SignaturePadView() + { + InitializeComponent(); + + // Wire the capture area: the Pad is the control, the + // surrounding Border (PadFrame) is the hit-test region. For an + // inline control with no template, capture wiring happens on + // first measure, which is guaranteed before the user can + // interact, so attaching here is safe. + InnerPad.CaptureArea = PadFrame; + ApplyFrameSize(); + + InnerPad.RedrawRequested += OnPadRedrawRequested; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == CaptureWidthProperty || change.Property == CaptureHeightProperty) + { + ApplyFrameSize(); + Repaint(); + } + } + + private void ApplyFrameSize() + { + if (PadFrame is null) return; + PadFrame.Width = Math.Max(1, CaptureWidth); + PadFrame.Height = Math.Max(1, CaptureHeight); + } + + private void OnPadRedrawRequested(object? sender, EventArgs e) => Repaint(); + + /// Defensive copy of the current buffer. + public SignaturePadData Snapshot() => InnerPad.Snapshot(); + + /// Forget every captured stroke. + public void Clear() => InnerPad.Clear(); + + /// Wire-format strokes, forwarded from the inner control. + public IReadOnlyList Strokes => InnerPad.Strokes; + + private void Repaint() + { + if (InkLayer is null) return; + + InkLayer.Children.Clear(); + var w = PadFrame.Bounds.Width; + var h = PadFrame.Bounds.Height; + if (w <= 0 || h <= 0) return; + + var pending = InnerPad.PendingStroke; + var strokes = InnerPad.Strokes; + int sealedCount = strokes.Count - pending.Count; + if (sealedCount < 0) + { + sealedCount = 0; + } + + int i = 0; + while (i < sealedCount) + { + int k = strokes[i]; + if (k <= 0) break; + i++; // skip the length prefix + + var poly = new Polyline + { + Stroke = StrokeBrush, + StrokeThickness = StrokeThickness, + StrokeLineCap = PenLineCap.Round, + StrokeJoin = PenLineJoin.Round, + }; + var pts = new List(k); + for (int p = 0; p < k; p++) + { + int nx = strokes[i++]; + int ny = strokes[i++]; + pts.Add(new Point(nx / CoordinateMax * w, ny / CoordinateMax * h)); + } + poly.Points = pts; + InkLayer.Children.Add(poly); + } + + if (pending.Count > 0) + { + var poly = new Polyline + { + Stroke = StrokeBrush, + StrokeThickness = StrokeThickness, + StrokeLineCap = PenLineCap.Round, + StrokeJoin = PenLineJoin.Round, + }; + + var pts = new List(pending.Count / 2); + for (int p = 0; p < pending.Count; p += 2) + { + int nx = pending[p]; + int ny = pending[p + 1]; + pts.Add(new Point(nx / CoordinateMax * w, ny / CoordinateMax * h)); + } + + poly.Points = pts; + InkLayer.Children.Add(poly); + } + } +} \ No newline at end of file diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs index bc5e7298f..2b243f2d5 100644 --- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs +++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs @@ -31,6 +31,7 @@ public static class ServiceCollectionHelpers () => settings.Authentication?.Authority); var billingClient = new BillingApiClient(api, () => settings.ApiUrl); var estimateClient = new EstimateApiClient(api, () => settings.ApiUrl); + var frontClient = new FrontOfficeApiClient(api, () => settings.ApiUrl); var userDirectory = new UserDirectory(userSearchClient); var reverseGeocoding = new NominatimReverseGeocodingService(); @@ -63,6 +64,7 @@ public static class ServiceCollectionHelpers services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // ViewModels services.AddSingleton(settings); @@ -74,6 +76,7 @@ public static class ServiceCollectionHelpers services.AddSingleton(activityClient); services.AddSingleton(billingClient); services.AddSingleton(estimateClient); + services.AddSingleton(frontClient); services.AddSingleton(reverseGeocoding); services.AddSingleton(userDirectory); services.AddSingleton(); diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs index 11797b714..ca9a7ff65 100644 --- a/src/PostIt/PostIt/ViewLocator.cs +++ b/src/PostIt/PostIt/ViewLocator.cs @@ -55,6 +55,7 @@ public class ViewLocator : IDataTemplate ProviderOngoingRequestsPageViewModel => services.GetRequiredService(), EstimateEditionPageViewModel => services.GetRequiredService(), EstimateListPageViewModel => services.GetRequiredService(), + EstimateValidationPageViewModel => services.GetRequiredService(), null => new TextBlock { Text = "No view for " }, _ => new TextBlock { Text = $"No view for {data.GetType().Name}" } }; diff --git a/src/PostIt/PostIt/ViewModels/Activity/EstimateListPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/EstimateListPageViewModel.cs index e5c4d355f..64db04bd9 100644 --- a/src/PostIt/PostIt/ViewModels/Activity/EstimateListPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Activity/EstimateListPageViewModel.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; +using Avalonia; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using PostIt.Helpers; @@ -32,6 +33,7 @@ public enum EstimateListPerspective public partial class EstimateListPageViewModel : ViewModelBase, IActionStatusViewModel { private readonly EstimateApiClient _estimateClient; + private readonly FrontOfficeApiClient _frontClient; private readonly EstimateListPerspective _perspective; private List _allEstimates = new(); @@ -83,10 +85,14 @@ public partial class EstimateListPageViewModel : ViewModelBase, IActionStatusVie protected set { _ = value; } } - public EstimateListPageViewModel(EstimateApiClient estimateClient, EstimateListPerspective perspective) + public EstimateListPageViewModel( + EstimateApiClient estimateClient, + EstimateListPerspective perspective, + FrontOfficeApiClient frontClient) { _estimateClient = estimateClient ?? throw new ArgumentNullException(nameof(estimateClient)); _perspective = perspective; + _frontClient = frontClient ?? throw new ArgumentNullException(nameof(frontClient)); } public Task InitializeAsync() => RefreshAsync(); @@ -153,4 +159,25 @@ public partial class EstimateListPageViewModel : ViewModelBase, IActionStatusVie private static bool ContainsInsensitive(string? value, string query) => !string.IsNullOrWhiteSpace(value) && value.Contains(query, StringComparison.OrdinalIgnoreCase); + + /// + /// Open the estimate validation (accept/reject with signature) + /// page for a specific estimate. The perspective the list was + /// opened in is forwarded as the actor-role UI hint; the server + /// independently enforces the real authorization. + /// + [RelayCommand] + private async Task OpenEstimateAsync(EstimateDto? estimate) + { + if (estimate is null) return; + + var app = (App?)Application.Current; + if (app is null) + { + throw new InvalidOperationException("Application PostIt indisponible."); + } + + var vm = new EstimateValidationPageViewModel(estimate, _perspective, _frontClient); + await app.PushPageAsync(vm).ConfigureAwait(true); + } } diff --git a/src/PostIt/PostIt/ViewModels/Activity/EstimateValidationPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/EstimateValidationPageViewModel.cs new file mode 100644 index 000000000..3b80c051a --- /dev/null +++ b/src/PostIt/PostIt/ViewModels/Activity/EstimateValidationPageViewModel.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Avalonia; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using PostIt.Controls; +using PostIt.Helpers; +using PostIt.Models; +using Yavsc.Api.Client; + +namespace PostIt.ViewModels; + +/// +/// Validation (accept / reject) of a single estimate, with an optional +/// signature. Backs the estimate validation page opened from +/// for either perspective. +/// +/// The server (POST api/v1/front/query/accept) resolves the +/// caller's role from the authenticated user compared to the query's +/// PerformerId/ClientId; the perspective passed in here +/// is only a UI hint (the ). The billing code +/// sent to the server is the estimate's +/// and the query id is its . +/// +public partial class EstimateValidationPageViewModel : ViewModelBase, IActionStatusViewModel +{ + private readonly EstimateDto _estimate; + private readonly EstimateListPerspective _perspective; + private readonly FrontOfficeApiClient _frontClient; + private SignaturePadControl? _pad; + + public string Title => _estimate.Title; + public string Description => _estimate.Description; + public long EstimateId => _estimate.Id; + public string CommandType => _estimate.CommandType; + public IReadOnlyList Bill => _estimate.Bill; + public DateTime ProviderValidationDate => _estimate.ProviderValidationDate; + public DateTime ClientValidationDate => _estimate.ClientValidationDate; + + public string RoleLabel => _perspective == EstimateListPerspective.Provider + ? "Vous validez en tant que prestataire" + : "Vous validez en tant que client"; + + /// + /// Human-readable summary of who has already validated this + /// estimate, shown above the signature pad. + /// + public string ValidationStateText + { + get + { + var parts = new List(); + if (ProviderValidationDate != default) + parts.Add($"Prestataire validé le {ProviderValidationDate.ToLocalTime():g}"); + if (ClientValidationDate != default) + parts.Add($"Client validé le {ClientValidationDate.ToLocalTime():g}"); + return parts.Count == 0 ? "Devis en attente de validation." : string.Join(" · ", parts); + } + } + + /// + /// True when the estimate has a linked query to accept/reject. + /// Estimates without a are + /// templates and cannot be validated through the front-office + /// endpoint. + /// + public bool HasCommand => _estimate.CommandId is not null; + + [ObservableProperty] + public partial bool IsBusy { get; set; } + + [ObservableProperty] + public partial string StatusMessage { get; set; } = "Prêt."; + + [ObservableProperty] + public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Prêt."); + + [ObservableProperty] + public partial int StrokeCount { get; set; } + + [ObservableProperty] + public partial int PointCount { get; set; } + + [ObservableProperty] + public partial bool HasSignature { get; set; } + + public override bool CanNavigateNext + { + get => false; + protected set { _ = value; } + } + + public override bool CanNavigatePrevious + { + get => true; + protected set { _ = value; } + } + + public EstimateValidationPageViewModel( + EstimateDto estimate, + EstimateListPerspective perspective, + FrontOfficeApiClient frontClient) + { + _estimate = estimate ?? throw new ArgumentNullException(nameof(estimate)); + _perspective = perspective; + _frontClient = frontClient ?? throw new ArgumentNullException(nameof(frontClient)); + } + + /// + /// Bind the page's signature pad control (the inner control of the + /// page's ). Called from the view's + /// code-behind once the view is in the visual tree. + /// + public void Attach(SignaturePadControl pad) + { + if (pad is null) throw new ArgumentNullException(nameof(pad)); + Detach(); + _pad = pad; + _pad.RedrawRequested += OnPadRedraw; + RefreshFromPad(); + } + + public void Detach() + { + if (_pad is null) return; + _pad.RedrawRequested -= OnPadRedraw; + _pad = null; + } + + private void OnPadRedraw(object? sender, EventArgs e) => RefreshFromPad(); + + private void RefreshFromPad() + { + if (_pad is null) return; + var snap = _pad.Snapshot(); + StrokeCount = snap.StrokeCount; + PointCount = snap.PointCount; + HasSignature = !snap.IsEmpty; + } + + partial void OnIsBusyChanged(bool value) + { + ValidateCommand.NotifyCanExecuteChanged(); + RejectCommand.NotifyCanExecuteChanged(); + } + + partial void OnHasSignatureChanged(bool value) + => ValidateCommand.NotifyCanExecuteChanged(); + + private bool CanValidate => !IsBusy && HasSignature && HasCommand; + + private bool CanReject => !IsBusy && HasCommand; + + [RelayCommand(CanExecute = nameof(CanValidate))] + public async Task ValidateAsync() + { + if (_estimate.CommandId is not long queryId) + { + this.SetWarningStatus("Ce devis n'est pas lié à une demande et ne peut être validé."); + return; + } + if (_pad is null || _pad.Snapshot().IsEmpty) + { + this.SetWarningStatus("Veuillez signer avant de valider."); + return; + } + + var snap = _pad.Snapshot(); + IsBusy = true; + try + { + var body = new QueryAcceptanceRequestDto + { + Strokes = snap.Strokes, + CoordinateMax = SignaturePadData.CoordinateMax, + CapturedAtUtc = DateTime.UtcNow, + }; + + await _frontClient.AcceptQueryAsync(_estimate.CommandType, queryId, body) + .ConfigureAwait(true); + + this.SetInfoStatus("Devis validé."); + await GoBackAsync().ConfigureAwait(true); + } + catch (HttpRequestException ex) when (ex.StatusCode is System.Net.HttpStatusCode.Forbidden) + { + this.SetWarningStatus("Vous n'êtes pas partie à ce devis."); + } + catch (HttpRequestException ex) when (ex.StatusCode is System.Net.HttpStatusCode.BadRequest) + { + this.SetWarningStatus($"Validation refusée: {ex.Message}"); + } + catch (Exception ex) + { + this.SetErrorStatus($"Erreur: {ex.Message}"); + } + finally + { + IsBusy = false; + } + } + + [RelayCommand(CanExecute = nameof(CanReject))] + public async Task RejectAsync() + { + if (_estimate.CommandId is not long queryId) + { + this.SetWarningStatus("Ce devis n'est pas lié à une demande et ne peut être refusé."); + return; + } + + IsBusy = true; + try + { + await _frontClient.RejectQueryAsync(_estimate.CommandType, queryId) + .ConfigureAwait(true); + + this.SetInfoStatus("Devis refusé."); + await GoBackAsync().ConfigureAwait(true); + } + catch (HttpRequestException ex) when (ex.StatusCode is System.Net.HttpStatusCode.Forbidden) + { + this.SetWarningStatus("Vous n'êtes pas partie à ce devis."); + } + catch (Exception ex) + { + this.SetErrorStatus($"Erreur: {ex.Message}"); + } + finally + { + IsBusy = false; + } + } + + [RelayCommand] + public void ClearSignature() + { + _pad?.Clear(); + RefreshFromPad(); + } + + [RelayCommand] + private async Task BackAsync() => await GoBackAsync().ConfigureAwait(true); + + private static async Task GoBackAsync() + { + var app = (App?)Application.Current; + if (app is null) + { + throw new InvalidOperationException("Application PostIt indisponible."); + } + await app.GoBackAsync().ConfigureAwait(true); + } +} \ No newline at end of file diff --git a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs index ea92f1d0a..9d466b26c 100644 --- a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs @@ -90,7 +90,13 @@ public class HomePageViewModel : ViewModelBase throw new InvalidOperationException("Client devis indisponible."); } - var vm = new EstimateListPageViewModel(estimateClient, perspective); + var frontClient = app.ServiceProvider?.GetRequiredService(); + if (frontClient is null) + { + throw new InvalidOperationException("Client front-office indisponible."); + } + + var vm = new EstimateListPageViewModel(estimateClient, perspective, frontClient); await vm.InitializeAsync(); await app.PushPageAsync(vm); } diff --git a/src/PostIt/PostIt/ViewModels/Signature/SignaturePageViewModel.cs b/src/PostIt/PostIt/ViewModels/Signature/SignaturePageViewModel.cs index 23ba70cf9..1e7df9c60 100644 --- a/src/PostIt/PostIt/ViewModels/Signature/SignaturePageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Signature/SignaturePageViewModel.cs @@ -85,10 +85,12 @@ public partial class SignaturePageViewModel : ViewModelBase, IActionStatusViewMo } /// - /// Bind a freshly-constructed (or re-templated) control to this - /// VM. Called from the view's code-behind once the control has - /// been added to the visual tree and its template applied (so - /// is wired). + /// Bind the inner of the page's + /// to this VM. Called from the + /// page's code-behind once the view is in the visual tree (so the + /// control's is + /// wired). Attaching to the control directly (rather than the + /// view) keeps the same contract the headless tests use. /// public void Attach(SignaturePadControl control) { diff --git a/src/PostIt/PostIt/Views/Activity/EstimateListPage.axaml b/src/PostIt/PostIt/Views/Activity/EstimateListPage.axaml index d5055dd1f..3fdb4fc7b 100644 --- a/src/PostIt/PostIt/Views/Activity/EstimateListPage.axaml +++ b/src/PostIt/PostIt/Views/Activity/EstimateListPage.axaml @@ -29,34 +29,40 @@ CornerRadius="8" Padding="10" Margin="0,0,0,8"> - - - + + + + - - - + + + +