diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7fd31746e..4c2d00997 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,11 @@
### Added
+* [Yavsc.Abstract, Yavsc.Server] Modele de solde des commandes facturables :
+ montants dus et payes persistants, reste a payer et etats non paye,
+ partiellement paye ou acquitte. L'acquittement depend des montants,
+ et non du seul statut PayPal ; migration sans montant historique invente.
+
* [PostIt.Desktop, Yavsc.Api] Podcasts en direct avec replay HLS conserve :
capture camera et microphone par LibVLC (H.264/AAC, segments MPEG-TS),
envoi ordonne et idempotent, catalogue et lecture authentifiee dans PostIt.
diff --git a/doc/invoicing.md b/doc/invoicing.md
new file mode 100644
index 000000000..3e23b4f68
--- /dev/null
+++ b/doc/invoicing.md
@@ -0,0 +1,189 @@
+# Facturation et paiement après devis signé
+
+## Objectif
+
+Ce document fixe le plan métier et technique pour le flux qui suit la validation du devis :
+
+- le devis est signé par les deux parties,
+- le prestataire déclenche la création d'une facture non acquittée,
+- la facture contient la ligne associée au mode de paiement choisi par le prestataire,
+- le client règle le montant partiel ou total,
+- la facture reste non acquittée tant que le total n'est pas intégralement payé.
+
+## Règle métier de base
+
+1. Le devis n'est considéré comme valide que lorsqu'il est signé par le fournisseur et par le client.
+2. Une facture ne peut être générée qu'après cette double validation.
+3. Le type de contrat choisi dans les settings du prestataire détermine le mode de règlement :
+ - arrhes,
+ - avance,
+ - ou autre mode de facturation supporté par le système.
+4. Le montant payé partiellement est enregistré comme paiement partiel, sans transformer la facture en facture entièrement payée.
+5. La facture est marquée comme acquittée uniquement lorsque le montant total est effectivement réglé.
+6. L'addition / le relevé de paiement doit mentionner le paiement effectué, sans prétendre que la facture est déjà entièrement acquittée.
+
+## Flux attendu
+
+### 1. Devis signé par les deux parties
+
+Le flux contractuel suit la séquence :
+
+- commande / demande,
+- devis préparé,
+- validation prestataire,
+- validation client,
+- double signature persistée.
+
+Une fois cette étape passée, le devis est un contrat exploitable pour la facture.
+
+### 2. Génération de la facture non acquittée
+
+Le prestataire déclenche la création de la facture depuis le contexte du devis validé.
+
+Le système doit alors :
+
+- identifier le devis validé,
+- récupérer le client et le prestataire,
+- sélectionner le mode de contrat défini dans les settings du prestataire,
+- ajouter la ligne de frais correspondante à la facture,
+- créer la facture dans un état "non acquittée".
+
+### 3. Paiement partiel
+
+Le client peut payer en premier :
+
+- l'avance,
+- les arrhes,
+- ou une tranche partielle.
+
+Dans ce cas :
+
+- le paiement est enregistré,
+- le montant payé est ajouté au total enregistré,
+- la facture reste "non acquittée" tant qu'elle n'a pas été réglée intégralement,
+- l'addition doit refléter la transaction effectuée sans annoncer un paiement total abusif.
+
+### 4. Acquittement total
+
+Lorsque le client a payé la somme totale due :
+
+- le statut de la facture passe à "payée" / "acquittée",
+- la facture peut être considérée comme totalement soldée,
+- la génération de documents ou notifications finales peut s'appuyer sur cet état.
+
+## Règles de codage à respecter
+
+### Règle 1 — ne pas confondre paiement partiel et paiement total
+
+Le modèle de paiement ne doit pas se limiter à un booléen "payé / non payé".
+
+Le bon niveau de modélisation est :
+
+- montant dû,
+- montant payé,
+- montant restant,
+- statut de paiement partiel / total,
+- état d'acquittement final.
+
+### Règle 2 — la facture ne doit pas être "payée" trop tôt
+
+Le système ne doit pas marquer la facture comme entièrement payée à la première transaction.
+
+L'état métier doit être calculé de manière explicite :
+
+- `montantPaye < montantTotal` => facture non acquittée,
+- `montantPaye >= montantTotal` => facture acquittée (reste à payer borné à zéro).
+
+### Règle 3 — les arrhes / avances doivent rester cohérents avec le contexte contractuel
+
+Le mode choisi par le prestataire (arrhes ou avance) doit être représenté de façon explicite dans le flux de génération de facture et dans l'addition.
+
+Le comportement attendu est :
+
+- la ligne de frais correspondante est ajoutée à la facture,
+- le paiement initial est comptabilisé correctement,
+- le solde restant reste visible et exploitable.
+
+## Plan d'implémentation proposé
+
+### Étape 1 — modéliser les règles de paiement
+
+Implémentée sur le point d'entrée existant `IBillable` /
+`NominativeServiceCommand`, sans créer encore d'entité Facture distincte :
+
+- `AmountDue` et `AmountPaid` sont persistés sur la commande ; le montant dû
+ reste `null` tant que le total de facture n'a pas été établi.
+- `SetPaymentAmounts(amountDue, amountPaid)` définit un instantané validé
+ des montants (décimaux non négatifs), côté métier uniquement. Les setters
+ privés empêchent leur alimentation par les requêtes de modification de commande.
+- `PaymentSummary` expose les montants dû, payé et restant, ainsi que
+ `Unpaid`, `PartiallyPaid` ou `Paid` ; il est absent si le montant dû est inconnu.
+- Le restant est borné à zéro en cas de trop-perçu, sans perdre le montant payé.
+ L'acquittement est atteint si `AmountPaid >= AmountDue`. Une facture
+ explicitement établie à zéro est soldée ; un montant inconnu ne l'est pas.
+- `GetIsAcquitted()` et le suffixe documentaire `-ack` utilisent ce calcul,
+ jamais le seul statut PayPal `SUCCESS`.
+
+La migration ajoute les montants sans reconstituer de paiements historiques :
+les anciennes commandes ont un dû inconnu et un payé nul. Un rapprochement
+explicite est nécessaire avant de les considérer comme acquittées.
+La génération depuis le devis, la fixation du dû et l'alimentation du payé
+par des transactions confirmées restent à brancher aux étapes suivantes.
+L'appel `SetPaymentAmounts` remplace un total cumulé ; il n'enregistre pas
+une transaction et ne doit pas être utilisé pour additionner un retour PayPal
+sans contrôle d'idempotence.
+
+Objectifs couverts :
+
+- identifier le point d'entrée métier de la facture,
+- ajouter un modèle de montant dû / montant payé / montant restant,
+- distinguer clairement l'état partiel du statut de paiement total,
+- éviter toute logique qui force un booléen `IsPaid = true` dès le premier paiement.
+
+### Étape 2 — brancher la génération de facture sur le devis signé
+
+- vérifier que le devis est bien doublement validé,
+- déclencher la facture depuis le contexte du devis,
+- rattacher la facture au bon client / prestataire.
+
+### Étape 3 — intégrer le mode de contrat du prestataire
+
+- lire le setting du prestataire qui indique le type de contrat,
+- associer ce mode à la ligne de frais ajoutée sur la facture,
+- garantir que la ligne peut être différenciée selon arrhes ou avance.
+
+### Étape 4 — enregistrer les paiements sans surinterpréter le statut
+
+- enregister le paiement effectué,
+- alimenter les montants payés et restants,
+- ne pas considérer la facture comme totalement acquittée tant que le total n'est pas atteint.
+
+### Étape 5 — finaliser l'état acquitté
+
+- calculer le total payé,
+- si `totalPaye >= totalDu`, alors passer la facture en état acquittée,
+- envoyer les notifications / documents associés au cas échéant.
+
+## Tests à couvrir
+
+Le flux doit être validé par des tests ciblés sur :
+
+- devis signé par les deux parties → génération d'une facture non acquittée,
+- mode arrhes / avance → ligne de frais correctement ajoutée,
+- paiement partiel → montant enregistré sans statut payé complet,
+- paiement total → facture marquée acquittée,
+- addition / message de paiement → mention du montant réellement versé sans faux état d'acquittement complet.
+
+## Critère de sortie fonctionnel
+
+Le comportement attendu est considéré comme atteint si le scénario suivant est vérifié :
+
+1. le devis est doublé signé,
+2. le prestataire génère une facture non acquittée,
+3. le client paye une avance ou des arrhes,
+4. la facture reste non acquittée tant qu'elle n'est pas totalement soldée,
+5. la facture devient acquittée uniquement lorsque le total est réglé.
+
+## Note de contexte
+
+Ce document consolide la décision métier issue du workflow “Prestation / devis” et est volontairement limité au périmètre de la facturation et du paiement après signature du devis. Il ne remplace pas les règles plus larges de gestion des litiges, workflows d'annulation ou paiements transverses déjà documentés ailleurs.
diff --git a/src/Yavsc.Abstract/Billing/IBillable.cs b/src/Yavsc.Abstract/Billing/IBillable.cs
index f936a57df..7d0fe3040 100644
--- a/src/Yavsc.Abstract/Billing/IBillable.cs
+++ b/src/Yavsc.Abstract/Billing/IBillable.cs
@@ -20,6 +20,8 @@ namespace Yavsc.Billing
DateTime? ValidationDate { get; }
+ PaymentSummary? PaymentSummary { get; }
+
bool GetIsAcquitted ();
string GetFileBaseName (IBillingService service);
diff --git a/src/Yavsc.Abstract/Billing/PaymentStatus.cs b/src/Yavsc.Abstract/Billing/PaymentStatus.cs
new file mode 100644
index 000000000..50bb2a921
--- /dev/null
+++ b/src/Yavsc.Abstract/Billing/PaymentStatus.cs
@@ -0,0 +1,8 @@
+namespace Yavsc.Billing;
+
+public enum PaymentStatus
+{
+ Unpaid,
+ PartiallyPaid,
+ Paid
+}
diff --git a/src/Yavsc.Abstract/Billing/PaymentSummary.cs b/src/Yavsc.Abstract/Billing/PaymentSummary.cs
new file mode 100644
index 000000000..b1f780fcd
--- /dev/null
+++ b/src/Yavsc.Abstract/Billing/PaymentSummary.cs
@@ -0,0 +1,21 @@
+namespace Yavsc.Billing;
+
+public sealed class PaymentSummary
+{
+ public PaymentSummary(decimal amountDue, decimal amountPaid)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(amountDue);
+ ArgumentOutOfRangeException.ThrowIfNegative(amountPaid);
+
+ AmountDue = amountDue;
+ AmountPaid = amountPaid;
+ }
+
+ public decimal AmountDue { get; }
+ public decimal AmountPaid { get; }
+ public decimal AmountRemaining => Math.Max(0m, AmountDue - AmountPaid);
+ public bool IsAcquitted => AmountPaid >= AmountDue;
+ public PaymentStatus Status => IsAcquitted
+ ? PaymentStatus.Paid
+ : AmountPaid > 0m ? PaymentStatus.PartiallyPaid : PaymentStatus.Unpaid;
+}
diff --git a/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs b/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs
index bf27d1d96..cdffae4a3 100644
--- a/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs
+++ b/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs
@@ -89,6 +89,8 @@ namespace Yavsc.ApiControllers
}
_context.Entry(hairCutQuery).State = EntityState.Modified;
+ _context.Entry(hairCutQuery).Property(q => q.AmountDue).IsModified = false;
+ _context.Entry(hairCutQuery).Property(q => q.AmountPaid).IsModified = false;
try
{
diff --git a/src/Yavsc.Org/Migrations/20261019182518_InvoicePaymentAmounts.Designer.cs b/src/Yavsc.Org/Migrations/20261019182518_InvoicePaymentAmounts.Designer.cs
new file mode 100644
index 000000000..c299307af
--- /dev/null
+++ b/src/Yavsc.Org/Migrations/20261019182518_InvoicePaymentAmounts.Designer.cs
@@ -0,0 +1,4867 @@
+//
+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("20261019182518_InvoicePaymentAmounts")]
+ partial class InvoicePaymentAmounts
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.13")
+ .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