diff --git a/CHANGELOG.md b/CHANGELOG.md index d33b5a9e1..3ee921d5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,13 @@ ### Added +* [PostIt/API/Yavsc.Org] Expose the private PayPal recipient setting in PostIt's performer editor and authenticated profile API; validate addresses, allow disabling with an empty value and preserve the setting for older clients. Make Web donation settings discoverable from the performer profile and podcast catalogue. +* [API] Initialize the required address field when creating a performer profile so a first configuration can be saved. +* [Yavsc.Org] Add an authenticated Razor podcast catalogue with same-origin API-backed HLS/MP4 playback and replay byte-range support. Reuse Site.ApiUrl and the authority's signing credentials to issue short-lived user tokens, without a separate OAuth client or PodcastWeb configuration. Distinguish an empty catalogue from API/configuration failures. +* [Yavsc.Org] Integrate direct PayPal donations to performers: self-service recipient settings, Yavsc donation pages/history, server-verified checkout and capture, antiforgery/concurrency protection, and validated IPN reconciliation. Reuse the existing merchant settings and persist donation receipts in a dedicated migration; keep uncertain and pending payments distinct from completed donations. +* [PostIt] Open the Yavsc.Org podcast catalogue in the browser instead of the authenticated API playlist; preserve embedded playback and external blog-video links. * [PostIt.Android/Desktop] Extend LAN/VPN SRTP calls to Android 8+ using the existing front-camera/microphone Podcast capture, rotated native preview and embedded LibVLC remote video, without libwebrtc or new dependencies. Pass the live MediaRecorder pipe directly to the shared SRTP engine without HLS files or transcoding; add a call-specific foreground notification, stop on backgrounding, and allow incoming callers to configure devices before acceptance. Use separate RTP/RTCP UDP ports to avoid a diagnosed Android VLC `rtcp-mux` SDP-generation crash. Add debug-only physical camera/SRTP checks and shared control/pipe-input regressions; Internet/TURN/group calls remain outside this transport. + * [PostIt.Desktop/API] Add opt-in Linux LAN/VPN peer calls through the existing ChatHub, reusing Podcast LibVLC camera/microphone capture and H.264/AAC encoding with direct SRTP instead of HLS. Add consent, embedded local/remote video, hangup/interruption cleanup, private IPv4 validation and per-direction ephemeral keys over HTTPS-only signaling. Reject IP-masking accounts rather than silently bypassing TURN; keep this transport disabled by default and separate from the future Internet/group-call design. Test real bidirectional encrypted audio/video, wrong-key rejection, preview output, signaling and UI lifecycle; physical camera/two-machine rendering remains to validate. * [PostIt/API] Begin Android/Linux call integration on the existing authenticated ChatHub: typed invitation, single-device consent, bounded SDP/ICE exchange and hangup; derive relay policy from account preferences and issue temporary coturn credentials. Attach the shared PostIt signaling client to the chat connection, end calls on disconnect/room departure/privacy activation, and add targeted regression tests. Calls remain disabled by default; native media adapters and Android/Linux end-to-end validation are not implemented yet. * [PostIt] Clarify reuse of the existing ChatHub for call signaling and document the inconclusive Android native WebRTC experiment; discard the isolated laboratory without changing production chat or media behavior. diff --git a/doc/Architecture.md b/doc/Architecture.md index d48dff22b..d8f1406d7 100644 --- a/doc/Architecture.md +++ b/doc/Architecture.md @@ -57,6 +57,91 @@ sudo adduser --ingroup yavsc --add-extra-groups www-data \ --disabled-password --system yavsc ``` +## Catalogue Web de podcasts + +`/Podcast` (vue `src/Yavsc.Org/Views/Podcast/Index.cshtml`) exige une +session Identity Yavsc.Org, y compris pour chaque requête de lecture. +Yavsc.Org interroge Yavsc.Api côté serveur ; aucun jeton n'est transmis +au navigateur et aucun second catalogue de stockage n'est créé. +Les replays MP4 prêts prennent en charge GET, HEAD et les plages +d'octets. Les directs et replays non encore convertis utilisent HLS +natif ou hls.js (servi localement, installé par `npm install` et copié par +`npm run build:podcasts`, également inclus dans la copie d'assets de +`npm run build:js`). Sans support vidéo compatible, la page affiche un +avertissement et invite à utiliser PostIt. Cette page ne crée ni ne +supprime de podcast. +Depuis le lecteur PostIt, « Ouvrir dans le navigateur » ouvre `/Podcast` +sur l'hôte `Authentication.Authority`, pas la playlist API authentifiée. +La session Web reste distincte de celle de PostIt : le navigateur peut +donc demander une connexion. + +Il n'y a pas de section de configuration `PodcastWeb`, ni de client OAuth +technique à enregistrer. L'adresse API est celle de `Site:ApiUrl`, en HTTPS +(avec ou sans suffixe `/api/v1`). Yavsc.Org est l'autorité : comme pour +l'envoi d'avatar, `IdentityServerTools.IssueJwtAsync` utilise sa signature +existante pour émettre un jeton de cinq minutes au nom de l'utilisateur +connecté (`sub`, audience `api`, scope `api` attendu par Yavsc.Api). +Ce jeton reste côté serveur. Une adresse API manquante renvoie 503 ; +une panne de l'API renvoie une erreur explicite, pas un catalogue vide. + +### Dons PayPal aux performers + +Les dons sont gérés par Yavsc.Org, pas par un lien de paiement externe +configuré par l'opérateur : + +1. Depuis **Mon compte > Dons PayPal**, le performer renseigne l'adresse + de son propre compte PayPal (`/Donation/Settings`). Un champ vide + désactive les nouveaux dons. Seuls les performers actifs ayant + configuré leur compte proposent un bouton de don dans le catalogue. + Le formulaire est aussi accessible depuis le catalogue de podcasts et + le profil professionnel Web. Dans PostIt : **Mon profil > Gérer mon + activité professionnelle > Dons PayPal**, puis **Enregistrer**. + L'API privée `GET/PUT /api/v1/account/performer-profile` transporte + `DonationPayPalEmail` uniquement pour le compte connecté ; une chaîne + vide désactive les dons, une propriété omise ou nulle préserve + l'adresse existante pour les anciens clients. +2. Le donateur connecté choisit un montant en euros (`/Donation/Create`). + Yavsc conserve le don et son bénéficiaire avant de créer le checkout. +3. Yavsc utilise les paramètres marchands existants + `Authentication:PayPal` (mode sandbox/production et identifiants NVP). + Express Checkout désigne le compte du performer par + `PAYMENTREQUEST_0_SELLERPAYPALACCOUNTID` : le paiement va directement + au performer ; Yavsc ne collecte pas les fonds et ne les reverse pas. +4. PayPal recueille l'accord du donateur puis le renvoie dans Yavsc. + Une confirmation POST avec protection antiforgery déclenche le + paiement, après vérification côté serveur du token, du montant, + de la devise, de la référence et du bénéficiaire. Un simple retour + du navigateur n'est jamais considéré comme une preuve de paiement. +5. Seule une réponse PayPal vérifiée permet d'afficher le don comme payé. + Les notifications IPN reçues sur `/Donation/Notify` sont revalidées + auprès de PayPal, puis rapprochées du bénéficiaire, du montant, de la + devise et de la transaction. Elles permettent aussi de confirmer + ultérieurement un paiement Pending ou une réponse de capture perdue. + +`Site:ExternalUrl` doit être l'URL HTTPS publique de Yavsc.Org, utilisée +pour les retours et notifications (jamais le Host fourni par le client). +PayPal doit pouvoir joindre `/Donation/Notify`. Le compte marchand +doit être autorisé par PayPal pour les paiements directs à un tiers ; +valider ce parcours en sandbox avec deux comptes distincts avant mise +en production. Les tests locaux utilisent un serveur PayPal simulé, +pas de véritables transactions. + +`PerformerDonations` conserve la référence, le donateur, le performer, +le bénéficiaire figé à la création, le montant et l'état du paiement. +La migration `PerformerPayPalDonations` ajoute ce suivi et l'adresse +PayPal optionnelle du performer. Les profils ayant un historique de +dons ne peuvent pas être supprimés. Les rééditions du profil général +ne remplacent pas la configuration du bénéficiaire. + +Le paiement n'est capturé qu'une fois : contrôle de concurrence, +référence de déduplication PayPal (`MSGSUBID`) et transaction unique. +En cas de réponse ambiguë, le don reste en vérification sans nouvelle +capture automatique. Le donateur retrouve son suivi dans **Mon compte > +Mes dons** (`/Donation`). Si l'IPN ne réconcilie pas cet état, l'opérateur +doit vérifier la transaction auprès de PayPal avant toute nouvelle +tentative. Les dons sont facultatifs et n'ouvrent pas droit à un reçu +fiscal émis par cette page. + ## À documenter ensuite - Modèle `ProjetMusical` diff --git a/doc/testing.md b/doc/testing.md index fafd89068..7444d3273 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -78,12 +78,15 @@ pour éviter de monter un host complet. Avec le runner Microsoft.Testing.Platform du depot (.NET 10, xUnit v3) : ```bash +dotnet test --project src/test/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj \ + --filter-class '*PodcastWebTests' --filter-class '*PerformerDonationTests' dotnet test --project src/test/Yavsc.Api.Tests/Yavsc.Api.Tests.csproj \ --filter-class '*LiveProcessorTests' --filter-class '*PodcastControllerTests' \ - --filter-class '*PodcastReplayTests' + --filter-class '*PodcastReplayTests' --filter-class '*PerformerDonationSettingsTests' dotnet test --project src/PostIt/PostIt.Tests/PostIt.Tests.csproj \ --filter-class '*PodcastTests' --filter-class '*PodcastReplayTests' \ - --filter-class '*YavscApiClientTests' --filter-class '*PodcastNativeCaptureTests' + --filter-class '*YavscApiClientTests' --filter-class '*PodcastNativeCaptureTests' \ + --filter-class '*PerformerDonationSettingsTests' dotnet test --project src/PostIt/PostIt.Tests/PostIt.Tests.csproj \ --filter-method '*Shared_page_exposes_running_capture_preview_and_clears_it_before_disposal' ``` @@ -91,6 +94,9 @@ dotnet test --project src/PostIt/PostIt.Tests/PostIt.Tests.csproj \ Les tests serveur couvrent HTTP/authentification, proprietaire, sequences, reprises idempotentes, limites exactes, concurrence et persistance des replays apres interruption/redemarrage, dans des dossiers temporaires isoles. +Les tests de configuration PayPal couvrent la lecture privee, la creation, +la validation avant toute sauvegarde, l'effacement, la compatibilite des anciens +clients, ainsi que le chargement, la liaison du champ et l'enregistrement PostIt. Installer FFmpeg 5+ et ffprobe pour ces tests serveur : ils generent une source H.264/AAC synthetique de quatre secondes, verifient le remuxage reel (pistes, duree, faststart, decodage et seek a deux secondes), puis les reponses diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 7884f601d..4f3edf355 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -48,6 +48,10 @@ const bundleNames = [ // of bootstrap, jquery-ui, etc., which are NOT touched by this // toolchain). const staticAssets = [ + { + src: 'node_modules/hls.js/dist/hls.light.min.js', + dst: 'hls.light.min.js', + }, { src: 'node_modules/@aspnet/signalr/dist/browser/signalr.min.js', dst: 'signalr.min.js', diff --git a/package.json b/package.json index e419cd0ca..c8d376253 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "description": "Yavsc front-end toolchain (esbuild bundles for Yavsc.Org).", "scripts": { "build:js": "node esbuild.config.mjs", + "build:podcasts": "node -e \"require('node:fs').copyFileSync('node_modules/hls.js/dist/hls.light.min.js', 'src/Yavsc.Org/wwwroot/js/hls.light.min.js')\"", "build:js:watch": "node esbuild.config.mjs --watch", "clean:js": "node esbuild.config.mjs --clean" }, @@ -13,6 +14,7 @@ "bootstrap": "^4.6.2", "dropzone": "^5.9.3", "eonasdan-bootstrap-datetimepicker": "^4.17.49", + "hls.js": "1.6.13", "jquery": "^3.7.1", "jquery-timepicker": "^1.3.3", "jquery-ui": "^1.13.3", diff --git a/src/PostIt/PostIt.Tests/BlogReaderTests.cs b/src/PostIt/PostIt.Tests/BlogReaderTests.cs index 4ee5b101f..d07cf7a30 100644 --- a/src/PostIt/PostIt.Tests/BlogReaderTests.cs +++ b/src/PostIt/PostIt.Tests/BlogReaderTests.cs @@ -89,6 +89,20 @@ public class BlogReaderTests { var vm = new VideoPageViewModel(new Uri(source), "video/mp4"); Assert.Equal(expected, vm.CanOpenExternal); + Assert.Equal(vm.Source, vm.ExternalUri); + } + + [Theory] + [InlineData("https://org.example.test/Podcast", true)] + [InlineData("about:blank", false)] + public void Video_external_action_uses_the_web_resource_instead_of_the_playback_stream(string externalUrl, bool expected) + { + var source = new Uri("https://api.example.test/api/v1/podcasts/123/index.m3u8"); + var externalUri = new Uri(externalUrl); + var vm = new VideoPageViewModel(source, "application/vnd.apple.mpegurl", externalUri); + Assert.Equal(source, vm.Source); + Assert.Equal(externalUri, vm.ExternalUri); + Assert.Equal(expected, vm.CanOpenExternal); } [AvaloniaFact] diff --git a/src/PostIt/PostIt.Tests/PerformerDonationSettingsTests.cs b/src/PostIt/PostIt.Tests/PerformerDonationSettingsTests.cs new file mode 100644 index 000000000..3078e401d --- /dev/null +++ b/src/PostIt/PostIt.Tests/PerformerDonationSettingsTests.cs @@ -0,0 +1,72 @@ +using Avalonia.Controls; +using Avalonia.Headless.XUnit; +using PostIt.Services; +using PostIt.ViewModels; +using PostIt.Views; +using Yavsc.Abstract.Workflow; +using Yavsc.Api.Client; + +namespace PostIt.Tests; + +public sealed class PerformerDonationSettingsTests +{ + [Theory] + [InlineData("performer@payments.example", "performer@payments.example")] + [InlineData(" performer@payments.example ", "performer@payments.example")] + [InlineData("", "")] + public void Performer_editor_includes_the_paypal_recipient_in_the_save_payload(string value, string expected) + { + var vm = new PerformerConfigurationPageViewModel { DonationPayPalEmail = value }; + Assert.Equal(expected, vm.BuildPerformerProfileSettings().DonationPayPalEmail); + } + + [AvaloniaFact] + public async Task Performer_editor_loads_displays_edits_and_saves_the_private_recipient() + { + var tokenPath = Path.Combine(Path.GetTempPath(), "postit-donation-" + Guid.NewGuid().ToString("N") + ".json"); + try + { + await using var api = new PerformerApi(tokenPath); + var vm = new PerformerConfigurationPageViewModel(api, new ActivityApiClient(api, api.Settings.ApiUrl)); + await vm.RefreshAsync(); + Assert.Equal("initial@payments.example", vm.DonationPayPalEmail); + var page = new PerformerConfigurationPage { DataContext = vm }; + var input = page.FindControl("DonationPayPalEmailInput")!; + Assert.Equal("initial@payments.example", input.Text); + input.Text = "updated@payments.example"; + Assert.Equal("updated@payments.example", vm.DonationPayPalEmail); + await vm.SaveAsync(); + Assert.Equal("updated@payments.example", api.Saved?.DonationPayPalEmail); + input.Text = ""; + await vm.SaveAsync(); + Assert.Equal("", api.Saved?.DonationPayPalEmail); + } + finally + { + if (File.Exists(tokenPath)) File.Delete(tokenPath); + } + } + + private sealed class PerformerApi(string tokenPath) : YavscApiClient(new Settings + { + Authentication = new AuthenticationSettings + { + Authority = "https://org.example.test", ClientId = "postit", Scopes = ["openid", "api"] + } + }, new TokenStore(tokenPath)) + { + public PerformerProfileSettings? Saved { get; private set; } + + public override Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + if (typeof(T) == typeof(List)) + return Task.FromResult((T)(object)new List()); + Assert.EndsWith("/account/performer-profile", path); + if (method == HttpMethod.Put) Saved = Assert.IsType(body); + return Task.FromResult((T)(object)new PerformerProfileSettings + { + PerformerId = "author", DonationPayPalEmail = "initial@payments.example" + }); + } + } +} diff --git a/src/PostIt/PostIt/ViewModels/Blogs/VideoPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Blogs/VideoPageViewModel.cs index 38bf4b1c8..80f6de51a 100644 --- a/src/PostIt/PostIt/ViewModels/Blogs/VideoPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Blogs/VideoPageViewModel.cs @@ -5,14 +5,15 @@ using Yavsc.Api.Client; namespace PostIt.ViewModels; -public partial class VideoPageViewModel(Uri source, string mimeType) : ViewModelBase +public partial class VideoPageViewModel(Uri source, string mimeType, Uri? externalUri = null) : ViewModelBase { public Uri Source { get; } = source; + public Uri ExternalUri { get; } = externalUri ?? source; public string MimeType { get; } = mimeType; public PodcastApiClient? PodcastClient { get; init; } public PodcastResponse? Podcast { get; set; } - public bool CanOpenExternal => CanLaunchExternally(Source); + public bool CanOpenExternal => CanLaunchExternally(ExternalUri); public string Title => Podcast?.Title ?? Uri.UnescapeDataString(System.IO.Path.GetFileName(Source.AbsolutePath)); public override bool CanNavigateNext { get => false; protected set { } } public override bool CanNavigatePrevious { get => true; protected set { } } diff --git a/src/PostIt/PostIt/ViewModels/PerformerConfigurationPageViewModel.cs b/src/PostIt/PostIt/ViewModels/PerformerConfigurationPageViewModel.cs index 91b6d0572..0083a38db 100644 --- a/src/PostIt/PostIt/ViewModels/PerformerConfigurationPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/PerformerConfigurationPageViewModel.cs @@ -50,6 +50,9 @@ public partial class PerformerConfigurationPageViewModel : ViewModelBase [ObservableProperty] public partial string Website { get; set; } = string.Empty; + [ObservableProperty] + public partial string DonationPayPalEmail { get; set; } = string.Empty; + [ObservableProperty] public partial bool Active { get; set; } @@ -101,6 +104,7 @@ public partial class PerformerConfigurationPageViewModel : ViewModelBase ExerciseCountryCode = string.IsNullOrWhiteSpace(dto?.ExerciseCountryCode) ? "fr" : dto.ExerciseCountryCode; SIREN = dto?.SIREN ?? string.Empty; Website = dto?.WebSite ?? string.Empty; + DonationPayPalEmail = dto?.DonationPayPalEmail ?? string.Empty; Active = dto?.Active ?? false; AcceptNotifications = dto?.AcceptNotifications ?? false; AcceptPublicContact = dto?.AcceptPublicContact ?? false; @@ -142,6 +146,7 @@ public partial class PerformerConfigurationPageViewModel : ViewModelBase ExerciseCountryCode = string.IsNullOrWhiteSpace(ExerciseCountryCode) ? "fr" : ExerciseCountryCode, SIREN = SIREN, WebSite = Website, + DonationPayPalEmail = DonationPayPalEmail.Trim(), Active = Active, AcceptNotifications = AcceptNotifications, AcceptPublicContact = AcceptPublicContact, diff --git a/src/PostIt/PostIt/ViewModels/PodcastsPageViewModel.cs b/src/PostIt/PostIt/ViewModels/PodcastsPageViewModel.cs index 6d9877da8..420024342 100644 --- a/src/PostIt/PostIt/ViewModels/PodcastsPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/PodcastsPageViewModel.cs @@ -263,7 +263,8 @@ public partial class PodcastsPageViewModel : ViewModelBase var podcast = SelectedPodcast ?? throw new InvalidOperationException("Selectionnez un podcast."); var app = Application.Current as App ?? throw new InvalidOperationException("Application indisponible."); var manifestUrl = new Uri(new Uri(_api.Settings.ApiUrl.TrimEnd('/') + "/"), $"podcasts/{podcast.Id}/index.m3u8"); - await app.PushPageAsync(new VideoPageViewModel(manifestUrl, "application/vnd.apple.mpegurl") + var catalogueUrl = new Uri(new Uri(_api.Settings.Authentication.Authority.TrimEnd('/') + "/"), "Podcast"); + await app.PushPageAsync(new VideoPageViewModel(manifestUrl, "application/vnd.apple.mpegurl", catalogueUrl) { PodcastClient = _client, Podcast = podcast }); diff --git a/src/PostIt/PostIt/Views/Layout/PerformerConfigurationPage.axaml b/src/PostIt/PostIt/Views/Layout/PerformerConfigurationPage.axaml index ff9e0fe31..f8d02b32a 100644 --- a/src/PostIt/PostIt/Views/Layout/PerformerConfigurationPage.axaml +++ b/src/PostIt/PostIt/Views/Layout/PerformerConfigurationPage.axaml @@ -27,6 +27,16 @@ + + + + + Owner-only PayPal recipient. Null preserves the existing setting; empty disables donations. + [MaxLength(127)] + public string? DonationPayPalEmail { get; set; } public bool Active { get; set; } public bool AcceptNotifications { get; set; } public bool AcceptPublicContact { get; set; } diff --git a/src/Yavsc.Api/Controllers/accounting/AccountController.cs b/src/Yavsc.Api/Controllers/accounting/AccountController.cs index d91534096..bb41168b8 100644 --- a/src/Yavsc.Api/Controllers/accounting/AccountController.cs +++ b/src/Yavsc.Api/Controllers/accounting/AccountController.cs @@ -573,6 +573,7 @@ namespace Yavsc.WebApi.Controllers } [HttpGet("performer-profile")] + [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] public async Task GetPerformerProfile() { var uid = User.GetUserId(); @@ -598,6 +599,7 @@ namespace Yavsc.WebApi.Controllers ExerciseCountryCode = profile?.ExerciseCountryCode ?? "fr", SIREN = profile?.SIREN ?? string.Empty, WebSite = profile?.WebSite ?? string.Empty, + DonationPayPalEmail = profile?.DonationPayPalEmail ?? string.Empty, Active = profile?.Active ?? false, AcceptNotifications = profile?.AcceptNotifications ?? false, AcceptPublicContact = profile?.AcceptPublicContact ?? false, @@ -630,6 +632,20 @@ namespace Yavsc.WebApi.Controllers return Forbid(); } + if (!ModelState.IsValid) + return ValidationProblem(ModelState); + if (model.DonationPayPalEmail is { } email) + { + model.DonationPayPalEmail = email.Trim(); + if (model.DonationPayPalEmail.Length > 0 + && (model.DonationPayPalEmail.Length > 127 + || !new System.ComponentModel.DataAnnotations.EmailAddressAttribute().IsValid(model.DonationPayPalEmail))) + { + ModelState.AddModelError(nameof(model.DonationPayPalEmail), "Une adresse e-mail PayPal valide est requise."); + return ValidationProblem(ModelState); + } + } + var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == uid); if (user is null) { @@ -655,7 +671,7 @@ namespace Yavsc.WebApi.Controllers AcceptNotifications = model.AcceptNotifications, AcceptPublicContact = model.AcceptPublicContact, UseGeoLocalizationToReduceDistanceWithClients = model.UseGeoLocalizationToReduceDistanceWithClients, - OrganizationAddress = new Yavsc.Models.Relationship.Location() + OrganizationAddress = new Yavsc.Models.Relationship.Location { Address = string.Empty } }; _dbContext.Performers.Add(profile); } @@ -670,6 +686,9 @@ namespace Yavsc.WebApi.Controllers profile.UseGeoLocalizationToReduceDistanceWithClients = model.UseGeoLocalizationToReduceDistanceWithClients; } + if (model.DonationPayPalEmail != null) + profile.DonationPayPalEmail = model.DonationPayPalEmail.Length == 0 ? null : model.DonationPayPalEmail; + var desiredCodes = (model.SelectedActivityCodes ?? []) .Where(code => !string.IsNullOrWhiteSpace(code)) .Distinct(StringComparer.OrdinalIgnoreCase) diff --git a/src/Yavsc.Org/Controllers/Accounting/ManageController.cs b/src/Yavsc.Org/Controllers/Accounting/ManageController.cs index f6b211018..e3d76169d 100644 --- a/src/Yavsc.Org/Controllers/Accounting/ManageController.cs +++ b/src/Yavsc.Org/Controllers/Accounting/ManageController.cs @@ -664,8 +664,13 @@ namespace Yavsc.Controllers if (_dbContext.Performers.Any(p=>p.PerformerId == uid)) { _dbContext.Update(model); + _dbContext.Entry(model).Property(p => p.DonationPayPalEmail).IsModified = false; + } + else + { + model.DonationPayPalEmail = null; + _dbContext.Performers.Add(model); } - else _dbContext.Performers.Add(model); _dbContext.SaveChanges(User.GetUserId()); // Give this user the Performer role if (!User.IsInMsRole("Performer")) @@ -716,6 +721,8 @@ namespace Yavsc.Controllers bool existing = _dbContext.Performers.Any(x => x.PerformerId == uid); if (existing) { + if (await _dbContext.PerformerDonations.AnyAsync(d => d.PerformerId == uid)) + return Conflict("Ce profil a recu des dons et doit etre conserve pour leur suivi. Desactivez les nouveaux dons dans les parametres PayPal."); _dbContext.Performers.Remove( _dbContext.Performers.First(x => x.PerformerId == uid) ); diff --git a/src/Yavsc.Org/Controllers/DonationController.cs b/src/Yavsc.Org/Controllers/DonationController.cs new file mode 100644 index 000000000..f3e7d6565 --- /dev/null +++ b/src/Yavsc.Org/Controllers/DonationController.cs @@ -0,0 +1,232 @@ +#nullable enable + +using System.Text; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Yavsc.Models; +using Yavsc.Models.Payment; +using Yavsc.Models.Workflow; +using Yavsc.Server.Helpers; +using Yavsc.ViewModels.Podcast; +using Yavsc.Web.Services; + +namespace Yavsc.Controllers; + +[Authorize(Policy = "WebUser")] +[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] +public sealed class DonationController(ApplicationDbContext db, IPayPalDonationGateway paypal, + IOptions siteOptions, ILogger logger) : Controller +{ + [HttpGet] + public async Task Index(CancellationToken ct) + => View(await db.PerformerDonations.AsNoTracking().Where(d => d.DonorId == User.GetUserId()) + .OrderByDescending(d => d.CreatedAt) + .Join(db.Users, d => d.PerformerId, u => u.Id, + (d, u) => new DonationDetailsViewModel(d, u.UserName ?? d.PerformerId)).ToArrayAsync(ct)); + + [HttpGet] + public async Task Settings(CancellationToken ct) + { + var performer = await db.Performers.FindAsync([User.GetUserId()], ct); + if (performer == null) + { + Response.StatusCode = StatusCodes.Status404NotFound; + ViewData["PaymentError"] = "Creez d'abord votre profil performer dans Mon compte > Parametres professionnels pour recevoir des dons."; + return View("Error"); + } + return View(new DonationSettingsViewModel { PayPalEmail = performer.DonationPayPalEmail }); + } + + [HttpPost, ValidateAntiForgeryToken] + public async Task Settings(DonationSettingsViewModel model, CancellationToken ct) + { + var performer = await db.Performers.FindAsync([User.GetUserId()], ct); + if (performer == null) return NotFound(); + if (!ModelState.IsValid) return View(model); + performer.DonationPayPalEmail = string.IsNullOrWhiteSpace(model.PayPalEmail) ? null : model.PayPalEmail.Trim(); + await db.SaveChangesAsync(ct); + TempData["DonationSettingsSaved"] = true; + return RedirectToAction(nameof(Settings)); + } + + [HttpGet] + public async Task Create(string id, CancellationToken ct) + { + var performer = await RecipientAsync(id, ct); + if (performer == null) return NotFound(); + return View(new CreateDonationViewModel { PerformerId = id, PerformerName = performer.Performer.UserName ?? id }); + } + + [HttpPost, ValidateAntiForgeryToken] + public async Task Create(CreateDonationViewModel model, CancellationToken ct) + { + var performer = await RecipientAsync(model.PerformerId, ct); + if (performer == null) return NotFound(); + model.PerformerName = performer.Performer.UserName ?? performer.PerformerId; + if (decimal.Round(model.Amount, 2) != model.Amount) + ModelState.AddModelError(nameof(model.Amount), "Le montant doit avoir au maximum deux decimales."); + if (!ModelState.IsValid) return View(model); + var urls = PaymentUrls(); + var donation = new PerformerDonation + { + Id = Guid.NewGuid(), DonorId = User.GetUserId(), PerformerId = performer.PerformerId, + RecipientEmail = performer.DonationPayPalEmail!, Amount = model.Amount, CreatedAt = DateTimeOffset.UtcNow + }; + db.PerformerDonations.Add(donation); + await db.SaveChangesAsync(ct); + donation.CheckoutToken = await paypal.CreateAsync(donation, + urls.Return + donation.Id, urls.Cancel + donation.Id, urls.Notify, ct); + donation.State = PerformerDonationState.AwaitingApproval; + await SaveAsync(donation, ct); + return Redirect(paypal.CheckoutUrl(donation.CheckoutToken)); + } + + [HttpGet] + public async Task Return(Guid id, string token, CancellationToken ct) + { + var donation = await OwnedAsync(id, ct); + if (donation == null || string.IsNullOrWhiteSpace(token) || donation.CheckoutToken != token) return NotFound(); + if (donation.State == PerformerDonationState.AwaitingApproval) + await paypal.ApproveAsync(donation, ct); + return RedirectToAction(nameof(Details), new { id }); + } + + [HttpGet] + public async Task Details(Guid id, CancellationToken ct) + { + var donation = await OwnedAsync(id, ct); + if (donation == null) return NotFound(); + var name = await db.Users.Where(u => u.Id == donation.PerformerId).Select(u => u.UserName).SingleAsync(ct); + return View(new DonationDetailsViewModel(donation, name ?? donation.PerformerId)); + } + + [HttpPost, ValidateAntiForgeryToken] + public async Task Confirm(Guid id, CancellationToken ct) + { + var donation = await OwnedAsync(id, ct); + if (donation == null) return NotFound(); + if (donation.State == PerformerDonationState.Completed) + return RedirectToAction(nameof(Details), new { id }); + if (donation.State != PerformerDonationState.AwaitingApproval) + throw new DonationPaymentException(409, "Ce paiement est deja traite ou en cours de verification. Ne creez pas un second don."); + var approval = await paypal.ApproveAsync(donation, ct); + donation.State = PerformerDonationState.Processing; + await SaveAsync(donation, ct); + var receipt = await paypal.CaptureAsync(donation, approval.PayerId, ct); + // An IPN may have confirmed the payment while the capture request was in flight. + await db.Entry(donation).ReloadAsync(ct); + if (donation.TransactionId != null && donation.TransactionId != receipt.TransactionId) + throw new DonationPaymentException(409, "La reference PayPal ne correspond pas au don."); + if (donation.State is PerformerDonationState.Processing or PerformerDonationState.Pending) + { + donation.TransactionId = receipt.TransactionId; + donation.State = receipt.Completed ? PerformerDonationState.Completed : PerformerDonationState.Pending; + donation.CompletedAt = receipt.Completed ? DateTimeOffset.UtcNow : null; + await SaveAsync(donation, ct); + } + return RedirectToAction(nameof(Details), new { id }); + } + + [HttpGet] + public async Task Cancel(Guid id, CancellationToken ct) + { + var donation = await OwnedAsync(id, ct); + if (donation == null) return NotFound(); + return View(donation); + } + + [HttpPost, ValidateAntiForgeryToken] + public async Task CancelConfirm(Guid id, CancellationToken ct) + { + var donation = await OwnedAsync(id, ct); + if (donation == null) return NotFound(); + if (donation.State == PerformerDonationState.AwaitingApproval) + { + donation.State = PerformerDonationState.Cancelled; + await SaveAsync(donation, ct); + } + return RedirectToAction(nameof(Details), new { id }); + } + + [AllowAnonymous, HttpPost, IgnoreAntiforgeryToken, RawPayPalNotification, RequestSizeLimit(64 * 1024)] + public async Task Notify(CancellationToken ct) + { + using var reader = new StreamReader(Request.Body, Encoding.UTF8); + var body = await reader.ReadToEndAsync(ct); + var fields = await paypal.VerifyNotificationAsync(body, ct); + if (!Guid.TryParseExact(fields.GetValueOrDefault("invoice"), "N", out var id)) + throw new DonationPaymentException(400, "Reference de don PayPal invalide."); + var donation = await db.PerformerDonations.FindAsync([id], ct); + if (donation == null) return NotFound(); + var transactionId = fields.GetValueOrDefault("txn_id"); + if (fields.GetValueOrDefault("txn_type") != "express_checkout" + || !string.Equals(fields.GetValueOrDefault("receiver_email"), donation.RecipientEmail, StringComparison.OrdinalIgnoreCase) + || !PayPalDonationGateway.SameAmount(fields.GetValueOrDefault("mc_gross"), donation.Amount) + || fields.GetValueOrDefault("mc_currency") != donation.Currency + || string.IsNullOrWhiteSpace(transactionId) || transactionId.Length > 32 + || (donation.TransactionId != null && donation.TransactionId != transactionId) + || donation.State is PerformerDonationState.Created or PerformerDonationState.AwaitingApproval or PerformerDonationState.Cancelled) + throw new DonationPaymentException(409, "La notification PayPal ne correspond pas au don."); + var paymentState = fields.GetValueOrDefault("payment_status"); + if (paymentState is not ("Completed" or "Pending" or "Denied" or "Failed" or "Expired" or "Voided")) + { + logger.LogWarning("PayPal donation {DonationId} notification has unsupported status {Status}", id, paymentState); + return Ok(); + } + + if (donation.State != PerformerDonationState.Completed) + { + donation.TransactionId = transactionId; + donation.State = paymentState switch + { + "Completed" => PerformerDonationState.Completed, + "Pending" => PerformerDonationState.Pending, + _ => PerformerDonationState.Failed + }; + donation.CompletedAt = paymentState == "Completed" ? DateTimeOffset.UtcNow : null; + await SaveAsync(donation, ct); + } + return Ok(); + } + + private Task RecipientAsync(string id, CancellationToken ct) + => db.Performers.Include(p => p.Performer).SingleOrDefaultAsync(p => p.PerformerId == id + && p.Active && p.DonationPayPalEmail != null && p.DonationPayPalEmail != "", ct); + + private Task OwnedAsync(Guid id, CancellationToken ct) + => db.PerformerDonations.SingleOrDefaultAsync(d => d.Id == id && d.DonorId == User.GetUserId(), ct); + + private async Task SaveAsync(PerformerDonation donation, CancellationToken ct) + { + donation.Revision = Guid.NewGuid(); + await db.SaveChangesAsync(ct); + } + + private (string Return, string Cancel, string Notify) PaymentUrls() + { + if (!Uri.TryCreate(siteOptions.Value.ExternalUrl, UriKind.Absolute, out var origin) + || origin.Scheme != Uri.UriSchemeHttps || !string.IsNullOrEmpty(origin.UserInfo)) + throw new DonationPaymentException(503, "L'adresse HTTPS du site n'est pas configuree."); + var root = siteOptions.Value.ExternalUrl.TrimEnd('/') + "/"; + return (root + "Donation/Return?id=", root + "Donation/Cancel?id=", root + "Donation/Notify"); + } + + public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var executed = await next(); + var error = executed.Exception; + if (error == null || error is not (DonationPaymentException or DbUpdateConcurrencyException) || Response.HasStarted) + return; + logger.LogError(error, "Donation request failed for {Action}", context.ActionDescriptor.DisplayName); + executed.ExceptionHandled = true; + Response.StatusCode = error is DonationPaymentException payment ? payment.StatusCode : 409; + ViewData["PaymentError"] = error is DonationPaymentException known + ? known.Message : "Ce don a ete modifie par une autre requete. Consultez son etat avant de recommencer."; + if (context.ActionArguments.TryGetValue("id", out var id) && id is Guid donationId) + ViewData["DonationId"] = donationId; + executed.Result = View("Error"); + } +} diff --git a/src/Yavsc.Org/Controllers/PodcastController.cs b/src/Yavsc.Org/Controllers/PodcastController.cs new file mode 100644 index 000000000..eb350ac2f --- /dev/null +++ b/src/Yavsc.Org/Controllers/PodcastController.cs @@ -0,0 +1,104 @@ +#nullable enable + +using System.Globalization; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.EntityFrameworkCore; +using Yavsc.Abstract.Streaming; +using Yavsc.Services; +using Yavsc.Models; +using Yavsc.ViewModels.Podcast; +using Yavsc.Web.Services; + +namespace Yavsc.Controllers; + +[Authorize(Policy = "WebUser")] +[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] +public sealed class PodcastController( + PodcastWebClient api, + ApplicationDbContext db, + ILogger logger) : Controller +{ + [HttpGet] + public async Task Index(CancellationToken ct) + { + var podcasts = await api.GetAsync>("", ct); + var ownerIds = podcasts.Select(p => p.OwnerId).Distinct().ToArray(); + var performers = await db.Performers.AsNoTracking() + .Where(p => ownerIds.Contains(p.PerformerId) && p.Active && p.DonationPayPalEmail != null + && p.DonationPayPalEmail != "") + .Select(p => p.PerformerId).ToArrayAsync(ct); + return View(new PodcastCatalogueViewModel( + podcasts.OrderByDescending(p => p.CreatedAt).ToArray(), performers.ToHashSet(StringComparer.Ordinal))); + } + + [HttpGet("Podcast/{id:guid}/index.m3u8")] + public async Task Playlist(Guid id, CancellationToken ct) + { + var manifest = await api.GetAsync(id.ToString(), ct); + var text = new StringBuilder("#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:15\n") + .Append("#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-PLAYLIST-TYPE:EVENT\n"); + foreach (var segment in manifest.Segments) + { + text.Append(CultureInfo.InvariantCulture, $"#EXTINF:{segment.DurationSeconds:0.######},\n"); + text.Append(Url.Action(nameof(Segment), new { id, sequence = segment.Sequence })).Append('\n'); + } + if (manifest.Podcast.State != PodcastState.Live) text.Append("#EXT-X-ENDLIST\n"); + return Content(text.ToString(), "application/vnd.apple.mpegurl", Encoding.UTF8); + } + + [HttpGet("Podcast/{id:guid}/segments/{sequence:int:min(0)}.ts")] + public Task Segment(Guid id, int sequence, CancellationToken ct) + => ProxyAsync($"{id}/segments/{sequence}.ts", "video/mp2t", ct); + + [HttpGet("Podcast/{id:guid}/replay.mp4")] + [HttpHead("Podcast/{id:guid}/replay.mp4")] + public Task Replay(Guid id, CancellationToken ct) + => ProxyAsync($"{id}/replay.mp4", "video/mp4", ct); + + private async Task ProxyAsync(string path, string contentType, CancellationToken ct) + { + RangeHeaderValue? range = null; + if (Request.Headers.TryGetValue("Range", out var value) + && !RangeHeaderValue.TryParse(value.ToString(), out range)) + return BadRequest(new ProblemDetails { Status = 400, Title = "Plage de lecture invalide." }); + + using var response = await api.SendAsync(HttpMethods.IsHead(Request.Method) + ? HttpMethod.Head : HttpMethod.Get, path, range, ct); + Response.StatusCode = (int)response.StatusCode; + Response.ContentType = contentType; + Response.Headers.CacheControl = "private, no-store"; + Response.ContentLength = response.Content.Headers.ContentLength; + if (response.Content.Headers.ContentRange != null) + Response.Headers.ContentRange = response.Content.Headers.ContentRange.ToString(); + if (response.Headers.AcceptRanges.Count > 0) + Response.Headers.AcceptRanges = string.Join(", ", response.Headers.AcceptRanges); + if (!HttpMethods.IsHead(Request.Method)) + await response.Content.CopyToAsync(Response.Body, ct); + return new EmptyResult(); + } + + public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var executed = await next(); + var error = executed.Exception; + if (error == null || Response.HasStarted || HttpContext.RequestAborted.IsCancellationRequested) + return; + if (error is not (PodcastException or HttpRequestException or JsonException or TaskCanceledException)) + return; + + logger.LogError(error, "Podcast web request failed for {Action}", context.ActionDescriptor.DisplayName); + var status = error is PodcastException podcastError ? podcastError.StatusCode : 502; + var message = error is PodcastException known + ? known.Message : "Le service de podcasts est indisponible. Reessayez plus tard."; + Response.StatusCode = status; + executed.ExceptionHandled = true; + executed.Result = context.ActionDescriptor.RouteValues["action"] == nameof(Index) + ? View("Index", new PodcastCatalogueViewModel([], new HashSet(), message)) + : StatusCode(status, new ProblemDetails { Status = status, Title = message }); + } +} diff --git a/src/Yavsc.Org/Controllers/RawPayPalNotificationAttribute.cs b/src/Yavsc.Org/Controllers/RawPayPalNotificationAttribute.cs new file mode 100644 index 000000000..f3277e137 --- /dev/null +++ b/src/Yavsc.Org/Controllers/RawPayPalNotificationAttribute.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Yavsc.Controllers; + +[AttributeUsage(AttributeTargets.Method)] +public sealed class RawPayPalNotificationAttribute : Attribute, IResourceFilter +{ + public void OnResourceExecuting(ResourceExecutingContext context) + { + context.ValueProviderFactories.RemoveType(); + context.ValueProviderFactories.RemoveType(); + context.ValueProviderFactories.RemoveType(); + } + + public void OnResourceExecuted(ResourceExecutedContext context) { } +} diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index 38ce39fb1..b271d3730 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -92,6 +92,10 @@ public static class HostingExtensions // Add the system clock service _ = services.AddSingleton(); _ = services.AddSingleton(); + services.AddHttpClient() + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false }); + services.AddHttpClient() + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false }); _ = services.AddTransient(); AddIdentityDBAndStores(builder) @@ -278,6 +282,8 @@ public static class HostingExtensions // options.AddPolicy("EmployeeId", policy => policy.RequireClaim("EmployeeId", "123", "456")); // options.AddPolicy("BuildingEntry", policy => policy.Requirements.Add(new OfficeEntryRequirement())); options.AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser()); + options.AddPolicy("WebUser", new AuthorizationPolicyBuilder(IdentityConstants.ApplicationScheme) + .RequireAuthenticatedUser().Build()); options.AddPolicy("TheAuthor", policy => policy.Requirements.Add(new EditPermission())); }); diff --git a/src/Yavsc.Org/Migrations/20261102093713_PerformerPayPalDonations.Designer.cs b/src/Yavsc.Org/Migrations/20261102093713_PerformerPayPalDonations.Designer.cs new file mode 100644 index 000000000..f2678b28d --- /dev/null +++ b/src/Yavsc.Org/Migrations/20261102093713_PerformerPayPalDonations.Designer.cs @@ -0,0 +1,5053 @@ +// +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("20261102093713_PerformerPayPalDonations")] + partial class PerformerPayPalDonations + { + /// + 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("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("HideIpAddressInCalls") + .HasColumnType("boolean"); + + 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.EstimateAttachedFile", b => + { + b.Property("FileId") + .HasColumnType("bigint"); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.HasKey("FileId", "EstimateId"); + + b.HasIndex("EstimateId"); + + b.ToTable("EstimateAttachedFiles"); + }); + + 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.InvoicePayment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CommandId") + .HasColumnType("bigint"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("ReceivedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("RecordedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("RecordedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Reference") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ReferenceScope") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.HasKey("Id"); + + b.HasIndex("CommandId"); + + b.HasIndex("Source", "ReferenceScope", "Reference") + .IsUnique(); + + b.ToTable("InvoicePayments"); + }); + + 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("AmountDue") + .HasColumnType("numeric"); + + b.Property("AmountPaid") + .ValueGeneratedOnAdd() + .HasColumnType("numeric") + .HasDefaultValue(0m); + + 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("InvoiceClientId") + .HasColumnType("text"); + + b.Property("InvoiceCurrency") + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("InvoiceEstimateId") + .HasColumnType("bigint"); + + b.Property("InvoiceLinesJson") + .HasColumnType("text"); + + b.Property("InvoicePerformerId") + .HasColumnType("text"); + + b.Property("InvoicedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PaymentRevision") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValue(new Guid("00000000-0000-0000-0000-000000000000")); + + 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("InvoiceEstimateId") + .IsUnique(); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.ToTable("NominativeServiceCommands"); + + b.HasDiscriminator("Discriminator").HasValue("NominativeServiceCommand"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.QueryAttachedFile", b => + { + b.Property("FileId") + .HasColumnType("bigint"); + + b.Property("CommandId") + .HasColumnType("bigint"); + + b.HasKey("FileId", "CommandId"); + + b.HasIndex("CommandId"); + + b.ToTable("QueryAttachedFiles"); + }); + + 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") + .IsRequired() + .HasColumnType("text"); + + b.Property("Length") + .HasColumnType("bigint"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + 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.Payment.PerformerDonation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric(18,2)"); + + b.Property("CheckoutToken") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("DonorId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("PerformerId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("RecipientEmail") + .IsRequired() + .HasMaxLength(127) + .HasColumnType("character varying(127)"); + + b.Property("Revision") + .IsConcurrencyToken() + .HasColumnType("uuid"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("TransactionId") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.HasKey("Id"); + + b.HasIndex("CheckoutToken") + .IsUnique(); + + b.HasIndex("DonorId"); + + b.HasIndex("PerformerId"); + + b.HasIndex("TransactionId") + .IsUnique(); + + b.ToTable("PerformerDonations"); + }); + + 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.ResourceUsageRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiCalls") + .HasColumnType("numeric"); + + b.Property("BandwidthMb") + .HasColumnType("numeric"); + + b.Property("CpuSeconds") + .HasColumnType("numeric"); + + b.Property("Currency") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(8) + .HasColumnType("character varying(8)") + .HasDefaultValue("EUR"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("EstimatedCompensation") + .HasColumnType("numeric"); + + b.Property("RecordedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("StorageMb") + .HasColumnType("numeric"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "RecordedAtUtc"); + + b.ToTable("ResourceUsageRecords"); + }); + + 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("DonationPayPalEmail") + .HasMaxLength(127) + .HasColumnType("character varying(127)"); + + 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.EstimateAttachedFile", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", "Estimate") + .WithMany() + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.UploadedFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Estimate"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.InvoicePayment", b => + { + b.HasOne("Yavsc.Models.Billing.NominativeServiceCommand", "Command") + .WithMany() + .HasForeignKey("CommandId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Command"); + }); + + 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.Billing.Estimate", null) + .WithMany() + .HasForeignKey("InvoiceEstimateId") + .OnDelete(DeleteBehavior.Restrict); + + 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.QueryAttachedFile", b => + { + b.HasOne("Yavsc.Models.Billing.NominativeServiceCommand", "Command") + .WithMany() + .HasForeignKey("CommandId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.UploadedFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Command"); + + b.Navigation("File"); + }); + + 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.Blog.UploadedFile", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + 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.Payment.PerformerDonation", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("DonorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", null) + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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/20261102093713_PerformerPayPalDonations.cs b/src/Yavsc.Org/Migrations/20261102093713_PerformerPayPalDonations.cs new file mode 100644 index 000000000..562d37b0c --- /dev/null +++ b/src/Yavsc.Org/Migrations/20261102093713_PerformerPayPalDonations.cs @@ -0,0 +1,89 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Yavsc.Migrations +{ + /// + public partial class PerformerPayPalDonations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DonationPayPalEmail", + table: "Performers", + type: "character varying(127)", + maxLength: 127, + nullable: true); + + migrationBuilder.CreateTable( + name: "PerformerDonations", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + DonorId = table.Column(type: "character varying(450)", maxLength: 450, nullable: false), + PerformerId = table.Column(type: "character varying(450)", maxLength: 450, nullable: false), + RecipientEmail = table.Column(type: "character varying(127)", maxLength: 127, nullable: false), + Amount = table.Column(type: "numeric(18,2)", nullable: false), + Currency = table.Column(type: "character varying(3)", maxLength: 3, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + State = table.Column(type: "integer", nullable: false), + CheckoutToken = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + TransactionId = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + CompletedAt = table.Column(type: "timestamp with time zone", nullable: true), + Revision = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PerformerDonations", x => x.Id); + table.ForeignKey( + name: "FK_PerformerDonations_AspNetUsers_DonorId", + column: x => x.DonorId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_PerformerDonations_Performers_PerformerId", + column: x => x.PerformerId, + principalTable: "Performers", + principalColumn: "PerformerId", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_PerformerDonations_CheckoutToken", + table: "PerformerDonations", + column: "CheckoutToken", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PerformerDonations_DonorId", + table: "PerformerDonations", + column: "DonorId"); + + migrationBuilder.CreateIndex( + name: "IX_PerformerDonations_PerformerId", + table: "PerformerDonations", + column: "PerformerId"); + + migrationBuilder.CreateIndex( + name: "IX_PerformerDonations_TransactionId", + table: "PerformerDonations", + column: "TransactionId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PerformerDonations"); + + migrationBuilder.DropColumn( + name: "DonationPayPalEmail", + table: "Performers"); + } + } +} diff --git a/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs index aa20d28f3..c47d1fbb3 100644 --- a/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs @@ -2738,6 +2738,71 @@ namespace Yavsc.Migrations b.ToTable("PayPalPayment"); }); + modelBuilder.Entity("Yavsc.Models.Payment.PerformerDonation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric(18,2)"); + + b.Property("CheckoutToken") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("DonorId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("PerformerId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("RecipientEmail") + .IsRequired() + .HasMaxLength(127) + .HasColumnType("character varying(127)"); + + b.Property("Revision") + .IsConcurrencyToken() + .HasColumnType("uuid"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("TransactionId") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.HasKey("Id"); + + b.HasIndex("CheckoutToken") + .IsUnique(); + + b.HasIndex("DonorId"); + + b.HasIndex("PerformerId"); + + b.HasIndex("TransactionId") + .IsUnique(); + + b.ToTable("PerformerDonations"); + }); + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => { b.Property("Id") @@ -3266,6 +3331,10 @@ namespace Yavsc.Migrations b.Property("Active") .HasColumnType("boolean"); + b.Property("DonationPayPalEmail") + .HasMaxLength(127) + .HasColumnType("character varying(127)"); + b.Property("ExerciseCountryCode") .IsRequired() .HasColumnType("text"); @@ -4514,6 +4583,21 @@ namespace Yavsc.Migrations b.Navigation("Executor"); }); + modelBuilder.Entity("Yavsc.Models.Payment.PerformerDonation", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("DonorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", null) + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => { b.HasOne("Yavsc.Models.ApplicationUser", null) diff --git a/src/Yavsc.Org/Services/PayPalDonationGateway.cs b/src/Yavsc.Org/Services/PayPalDonationGateway.cs new file mode 100644 index 000000000..b2472ff60 --- /dev/null +++ b/src/Yavsc.Org/Services/PayPalDonationGateway.cs @@ -0,0 +1,174 @@ +#nullable enable + +using System.Globalization; +using System.Text; +using Microsoft.Extensions.Options; +using Yavsc.Models.Payment; + +namespace Yavsc.Web.Services; + +public sealed class DonationPaymentException(int statusCode, string message) : Exception(message) +{ + public int StatusCode { get; } = statusCode; +} + +public sealed record DonationApproval(string PayerId); +public sealed record DonationReceipt(string TransactionId, bool Completed); + +public interface IPayPalDonationGateway +{ + Task CreateAsync(PerformerDonation donation, string returnUrl, string cancelUrl, string notifyUrl, CancellationToken ct); + Task ApproveAsync(PerformerDonation donation, CancellationToken ct); + Task CaptureAsync(PerformerDonation donation, string payerId, CancellationToken ct); + Task> VerifyNotificationAsync(string body, CancellationToken ct); + string CheckoutUrl(string token); +} + +public sealed class PayPalDonationGateway(HttpClient http, IOptions options) : IPayPalDonationGateway +{ + private PayPalSettings Settings + { + get + { + var settings = options.Value; + if (settings.Mode is not ("production" or "sandbox")) + throw new DonationPaymentException(503, "Le paiement PayPal n'est pas configure."); + return settings; + } + } + + public string CheckoutUrl(string token) + => $"https://{(Settings.Mode == "production" ? "www" : "www.sandbox")}.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token={Uri.EscapeDataString(token)}"; + + public async Task CreateAsync(PerformerDonation donation, string returnUrl, string cancelUrl, + string notifyUrl, CancellationToken ct) + { + var fields = PaymentFields(donation); + fields["RETURNURL"] = returnUrl; + fields["CANCELURL"] = cancelUrl; + fields["PAYMENTREQUEST_0_NOTIFYURL"] = notifyUrl; + fields["PAYMENTREQUEST_0_DESC"] = "Don au performer sur Yavsc"; + fields["NOSHIPPING"] = "1"; + var result = await CallAsync("SetExpressCheckout", fields, ct); + var token = Required(result, "TOKEN"); + return token.Length <= 100 ? token : throw new DonationPaymentException(502, "Jeton PayPal invalide."); + } + + public async Task ApproveAsync(PerformerDonation donation, CancellationToken ct) + { + var fields = await CallAsync("GetExpressCheckoutDetails", new() + { + ["TOKEN"] = donation.CheckoutToken ?? throw new InvalidOperationException("Missing donation checkout token.") + }, ct); + if (Required(fields, "TOKEN") != donation.CheckoutToken + || Required(fields, "PAYMENTREQUEST_0_INVNUM") != donation.Id.ToString("N") + || Required(fields, "PAYMENTREQUEST_0_CURRENCYCODE") != donation.Currency + || !SameAmount(fields.GetValueOrDefault("PAYMENTREQUEST_0_AMT"), donation.Amount) + || !string.Equals(Required(fields, "PAYMENTREQUEST_0_SELLERPAYPALACCOUNTID"), + donation.RecipientEmail, StringComparison.OrdinalIgnoreCase)) + throw new DonationPaymentException(409, "La confirmation PayPal ne correspond pas a ce don."); + return new DonationApproval(Required(fields, "PAYERID")); + } + + public async Task CaptureAsync(PerformerDonation donation, string payerId, CancellationToken ct) + { + var fields = PaymentFields(donation); + fields["TOKEN"] = donation.CheckoutToken!; + fields["PAYERID"] = payerId; + fields["MSGSUBID"] = donation.Id.ToString("N"); + var result = await CallAsync("DoExpressCheckoutPayment", fields, ct); + if (!SameAmount(result.GetValueOrDefault("PAYMENTINFO_0_AMT"), donation.Amount) + || Required(result, "PAYMENTINFO_0_CURRENCYCODE") != donation.Currency + || !string.Equals(Required(result, "PAYMENTINFO_0_SELLERPAYPALACCOUNTID"), + donation.RecipientEmail, StringComparison.OrdinalIgnoreCase) + || Required(result, "PAYMENTINFO_0_PAYMENTSTATUS") is not ("Completed" or "Pending")) + throw new DonationPaymentException(409, "Le paiement PayPal n'est pas confirme pour ce don."); + var transactionId = Required(result, "PAYMENTINFO_0_TRANSACTIONID"); + if (transactionId.Length > 32) + throw new DonationPaymentException(502, "Reference de transaction PayPal invalide."); + return new DonationReceipt(transactionId, + result["PAYMENTINFO_0_PAYMENTSTATUS"] == "Completed"); + } + + public async Task> VerifyNotificationAsync(string body, CancellationToken ct) + { + var endpoint = Settings.Mode == "production" + ? "https://ipnpb.paypal.com/cgi-bin/webscr" : "https://ipnpb.sandbox.paypal.com/cgi-bin/webscr"; + using var content = new StringContent("cmd=_notify-validate&" + body, Encoding.UTF8, "application/x-www-form-urlencoded"); + using var response = await PostAsync(endpoint, content, ct); + if ((await response.Content.ReadAsStringAsync(ct)).Trim() != "VERIFIED") + throw new DonationPaymentException(400, "Notification PayPal non verifiee."); + return Parse(body); + } + + public static bool SameAmount(string? value, decimal amount) + => decimal.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var actual) + && actual == amount; + + private static Dictionary PaymentFields(PerformerDonation donation) => new() + { + ["PAYMENTREQUEST_0_PAYMENTACTION"] = "Sale", + ["PAYMENTREQUEST_0_AMT"] = donation.Amount.ToString("F2", CultureInfo.InvariantCulture), + ["PAYMENTREQUEST_0_CURRENCYCODE"] = donation.Currency, + ["PAYMENTREQUEST_0_INVNUM"] = donation.Id.ToString("N"), + ["PAYMENTREQUEST_0_PAYMENTREQUESTID"] = donation.Id.ToString("N"), + ["PAYMENTREQUEST_0_SELLERPAYPALACCOUNTID"] = donation.RecipientEmail + }; + + private async Task> CallAsync(string method, Dictionary fields, CancellationToken ct) + { + var settings = Settings; + var accounts = settings.Accounts?.Where(a => a.ApiUsername == settings.MerchantAccountUserName).ToArray(); + if (accounts is not { Length: 1 } || string.IsNullOrWhiteSpace(accounts[0].ApiUsername) + || string.IsNullOrWhiteSpace(accounts[0].ApiPassword) || string.IsNullOrWhiteSpace(accounts[0].Signature)) + throw new DonationPaymentException(503, "Le compte marchand PayPal n'est pas configure."); + fields["METHOD"] = method; + fields["VERSION"] = "124.0"; + fields["USER"] = accounts[0].ApiUsername; + fields["PWD"] = accounts[0].ApiPassword; + fields["SIGNATURE"] = accounts[0].Signature; + using var content = new FormUrlEncodedContent(fields); + using var response = await PostAsync(settings.Mode == "production" + ? "https://api-3t.paypal.com/nvp" : "https://api-3t.sandbox.paypal.com/nvp", content, ct); + var result = Parse(await response.Content.ReadAsStringAsync(ct)); + if (result.GetValueOrDefault("ACK") is not ("Success" or "SuccessWithWarning")) + throw new DonationPaymentException(502, "PayPal a refuse cette operation. Aucun don n'est marque comme paye."); + return result; + } + + private async Task PostAsync(string endpoint, HttpContent content, CancellationToken ct) + { + try + { + var response = await http.PostAsync(endpoint, content, ct); + if (response.IsSuccessStatusCode) return response; + response.Dispose(); + throw new DonationPaymentException(502, "Le service PayPal est indisponible."); + } + catch (HttpRequestException ex) + { + throw new DonationPaymentException(502, $"La connexion PayPal a echoue ({ex.HttpRequestError}). Verifiez l'etat du don avant de recommencer."); + } + catch (OperationCanceledException) when (!ct.IsCancellationRequested) + { + throw new DonationPaymentException(504, "La reponse PayPal a expire. Verifiez l'etat du don avant de recommencer."); + } + } + + private static Dictionary Parse(string body) + { + var result = new Dictionary(StringComparer.Ordinal); + foreach (var pair in body.Split('&')) + { + var parts = pair.Split('=', 2); + if (parts.Length != 2 || !result.TryAdd(Decode(parts[0]), Decode(parts[1]))) + throw new DonationPaymentException(502, "Reponse PayPal invalide."); + } + return result; + } + + private static string Decode(string value) => Uri.UnescapeDataString(value.Replace("+", " ")); + private static string Required(IReadOnlyDictionary fields, string name) + => fields.TryGetValue(name, out var value) && !string.IsNullOrWhiteSpace(value) + ? value : throw new DonationPaymentException(502, "Reponse PayPal incomplete."); +} diff --git a/src/Yavsc.Org/Services/PodcastWebClient.cs b/src/Yavsc.Org/Services/PodcastWebClient.cs new file mode 100644 index 000000000..55024cd96 --- /dev/null +++ b/src/Yavsc.Org/Services/PodcastWebClient.cs @@ -0,0 +1,76 @@ +#nullable enable + +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Security.Claims; +using IdentityServer8; +using Microsoft.Extensions.Options; +using Yavsc.Services; +using Yavsc.Server.Helpers; + +namespace Yavsc.Web.Services; + +public sealed class PodcastWebClient(HttpClient http, IOptions options, + IdentityServerTools authority, IHttpContextAccessor contextAccessor) +{ + private string? _accessToken; + + public async Task GetAsync(string path, CancellationToken ct) + { + using var response = await SendAsync(HttpMethod.Get, path, null, ct); + return await response.Content.ReadFromJsonAsync(ct) + ?? throw new JsonException("Empty podcast API response."); + } + + public async Task SendAsync(HttpMethod method, string path, + RangeHeaderValue? range, CancellationToken ct) + { + var settings = options.Value; + if (!Uri.TryCreate(settings.ApiUrl, UriKind.Absolute, out var address) + || address.Scheme != Uri.UriSchemeHttps || !string.IsNullOrEmpty(address.UserInfo)) + throw new PodcastException(503, "La connexion aux podcasts n'est pas configuree."); + + if (_accessToken == null) + { + var user = contextAccessor.HttpContext?.User; + var userId = user?.GetUserId(); + if (user?.Identity?.IsAuthenticated != true || string.IsNullOrWhiteSpace(userId)) + throw new PodcastException(401, "Une connexion est requise pour lire les podcasts."); + _accessToken = await authority.IssueJwtAsync(300, new List + { + new("sub", userId), + new("name", user.Identity.Name ?? userId), + new("scope", "api"), + new("aud", "api") + }); + if (string.IsNullOrWhiteSpace(_accessToken)) + throw new InvalidOperationException("Yavsc.Org did not issue a podcast access token."); + } + + address = new Uri(settings.ApiUrl.TrimEnd('/') + "/"); + if (!address.AbsolutePath.EndsWith("/" + Constants.APIPrefix + "/", StringComparison.Ordinal)) + address = new Uri(address, Constants.APIPrefix + "/"); + using var request = new HttpRequestMessage(method, new Uri(address, "podcasts/" + path)); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken); + request.Headers.Range = range; + var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct); + if (!response.IsSuccessStatusCode && response.StatusCode != HttpStatusCode.RequestedRangeNotSatisfiable) + { + var status = response.StatusCode; + response.Dispose(); + throw new PodcastException(status switch + { + HttpStatusCode.NotFound => 404, + HttpStatusCode.Conflict => 409, + _ => 502 + }, status switch + { + HttpStatusCode.NotFound => "Podcast introuvable.", + HttpStatusCode.Conflict => "Ce replay n'est pas encore disponible.", + _ => "Le service de podcasts est indisponible." + }); + } + return response; + } +} diff --git a/src/Yavsc.Org/ViewModels/Podcast/DonationViewModels.cs b/src/Yavsc.Org/ViewModels/Podcast/DonationViewModels.cs new file mode 100644 index 000000000..4c7b06dbd --- /dev/null +++ b/src/Yavsc.Org/ViewModels/Podcast/DonationViewModels.cs @@ -0,0 +1,26 @@ +#nullable enable + +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Yavsc.Models.Payment; + +namespace Yavsc.ViewModels.Podcast; + +public sealed class CreateDonationViewModel +{ + [Required] + public string PerformerId { get; set; } = ""; + [BindNever] + public string PerformerName { get; set; } = ""; + [Range(typeof(decimal), "0.01", "9999999999999999.99", + ParseLimitsInInvariantCulture = true, ConvertValueInInvariantCulture = true)] + public decimal Amount { get; set; } = 5m; +} + +public sealed class DonationSettingsViewModel +{ + [EmailAddress, StringLength(127)] + public string? PayPalEmail { get; set; } +} + +public sealed record DonationDetailsViewModel(PerformerDonation Donation, string PerformerName); diff --git a/src/Yavsc.Org/ViewModels/Podcast/PodcastCatalogueViewModel.cs b/src/Yavsc.Org/ViewModels/Podcast/PodcastCatalogueViewModel.cs new file mode 100644 index 000000000..7ea4ca128 --- /dev/null +++ b/src/Yavsc.Org/ViewModels/Podcast/PodcastCatalogueViewModel.cs @@ -0,0 +1,10 @@ +#nullable enable + +using Yavsc.Abstract.Streaming; + +namespace Yavsc.ViewModels.Podcast; + +public sealed record PodcastCatalogueViewModel( + IReadOnlyList Podcasts, + IReadOnlySet DonationPerformerIds, + string? Error = null); diff --git a/src/Yavsc.Org/Views/Donation/Cancel.cshtml b/src/Yavsc.Org/Views/Donation/Cancel.cshtml new file mode 100644 index 000000000..807d0718e --- /dev/null +++ b/src/Yavsc.Org/Views/Donation/Cancel.cshtml @@ -0,0 +1,10 @@ +@model Yavsc.Models.Payment.PerformerDonation +@{ + ViewBag.Title = Localizer["Annuler le don"]; +} +

@ViewBag.Title

+

@Localizer["L'annulation est possible tant que le paiement n'a pas été lancé."]

+
+ +
+@Localizer["Voir l'état du don"] diff --git a/src/Yavsc.Org/Views/Donation/Create.cshtml b/src/Yavsc.Org/Views/Donation/Create.cshtml new file mode 100644 index 000000000..5fdc6462d --- /dev/null +++ b/src/Yavsc.Org/Views/Donation/Create.cshtml @@ -0,0 +1,18 @@ +@model Yavsc.ViewModels.Podcast.CreateDonationViewModel +@{ + ViewBag.Title = Localizer["Soutenir un performer"]; +} +

@Localizer["Faire un don à {0}", Model.PerformerName]

+

@Localizer["Votre don sera payé directement sur le compte PayPal du performer. Yavsc ne l'encaisse pas."]

+

@Localizer["Le don est facultatif et indépendant de l'accès aux podcasts. Aucun reçu fiscal n'est délivré."]

+
+ + +
+ + + +
+ + @Localizer["Retour aux podcasts"] +
diff --git a/src/Yavsc.Org/Views/Donation/Details.cshtml b/src/Yavsc.Org/Views/Donation/Details.cshtml new file mode 100644 index 000000000..706aa5169 --- /dev/null +++ b/src/Yavsc.Org/Views/Donation/Details.cshtml @@ -0,0 +1,36 @@ +@using Yavsc.Models.Payment +@model Yavsc.ViewModels.Podcast.DonationDetailsViewModel +@{ + ViewBag.Title = Localizer["Votre don"]; + var donation = Model.Donation; +} +

@Localizer["Votre don à {0}", Model.PerformerName]

+

@donation.Amount.ToString("F2") @donation.Currency

+

@Localizer["Référence"] : @donation.Id

+@switch (donation.State) +{ + case PerformerDonationState.AwaitingApproval: +

@Localizer["Après votre accord sur PayPal, confirmez le paiement ci-dessous. Le montant et le bénéficiaire sont vérifiés par Yavsc auprès de PayPal."]

+
+ +
+ break; + case PerformerDonationState.Completed: +

@Localizer["Merci ! PayPal a confirmé le paiement de votre don au performer."]

+

@Localizer["Transaction PayPal"] : @donation.TransactionId

+ break; + case PerformerDonationState.Cancelled: +

@Localizer["Ce don a été annulé avant paiement."]

+ break; + case PerformerDonationState.Created: +

@Localizer["Le paiement de ce don n'a pas été lancé."]

+ break; + case PerformerDonationState.Failed: +

@Localizer["PayPal a signalé l'échec de ce paiement. Ce don n'est pas payé."]

+ break; + default: +

@Localizer["Le paiement n'est pas confirmé. Il peut être en cours de traitement par PayPal. Ne créez pas de second don et actualisez cette page. Si cet état persiste, contactez Yavsc avec la référence du don."]

+ break; +} +@Localizer["Actualiser l'état"] +@Localizer["Retour aux podcasts"] diff --git a/src/Yavsc.Org/Views/Donation/Error.cshtml b/src/Yavsc.Org/Views/Donation/Error.cshtml new file mode 100644 index 000000000..6742cab50 --- /dev/null +++ b/src/Yavsc.Org/Views/Donation/Error.cshtml @@ -0,0 +1,13 @@ +@{ + ViewBag.Title = Localizer["Paiement indisponible"]; +} +

@ViewBag.Title

+ +

@Localizer["Un retour du navigateur ne prouve pas qu'un paiement a été effectué. Consultez l'état du don avant de recommencer."]

+@if (ViewData["DonationId"] is Guid id) +{ + @Localizer["Voir l'état du don"] +} +@Localizer["Retour aux podcasts"] +@Localizer["Mes dons"] +@Localizer["Mon profil performer"] diff --git a/src/Yavsc.Org/Views/Donation/Index.cshtml b/src/Yavsc.Org/Views/Donation/Index.cshtml new file mode 100644 index 000000000..c7d708d99 --- /dev/null +++ b/src/Yavsc.Org/Views/Donation/Index.cshtml @@ -0,0 +1,24 @@ +@model IReadOnlyList +@{ + ViewBag.Title = Localizer["Mes dons"]; +} +

@ViewBag.Title

+@if (Model.Count == 0) +{ +

@Localizer["Vous n'avez pas encore initié de don."]

+} +else +{ + +} +@Localizer["Retour aux podcasts"] diff --git a/src/Yavsc.Org/Views/Donation/Settings.cshtml b/src/Yavsc.Org/Views/Donation/Settings.cshtml new file mode 100644 index 000000000..5b4039c76 --- /dev/null +++ b/src/Yavsc.Org/Views/Donation/Settings.cshtml @@ -0,0 +1,21 @@ +@model Yavsc.ViewModels.Podcast.DonationSettingsViewModel +@{ + ViewBag.Title = Localizer["Dons PayPal"]; +} +

@ViewBag.Title

+@if (TempData["DonationSettingsSaved"] != null) +{ +

@Localizer["Les paramètres de dons ont été enregistrés."]

+} +

@Localizer["Configurez votre propre compte PayPal pour recevoir directement les dons. Vérifiez soigneusement cette adresse : elle désigne le bénéficiaire du paiement."]

+

@Localizer["Laissez le champ vide pour désactiver les nouveaux dons. Les dons déjà initiés conservent leur bénéficiaire."]

+
+ +
+ + + +
+ + @Localizer["Mon compte"] +
diff --git a/src/Yavsc.Org/Views/Manage/Index.cshtml b/src/Yavsc.Org/Views/Manage/Index.cshtml index 13b45339d..4504bc48f 100755 --- a/src/Yavsc.Org/Views/Manage/Index.cshtml +++ b/src/Yavsc.Org/Views/Manage/Index.cshtml @@ -115,6 +115,13 @@ Html.DisplayText("Modify");
Bank info:
@Html.DisplayFor(m => m.BankInfo) [@(Model.BankInfo==null?"Set":"Modify")]
+ @if (Model.HaveProfessionalSettings) + { +
@Localizer["Dons PayPal"]
+
@Localizer["Configurer la réception des dons"]
+ } +
@Localizer["Mes dons"]
+
@Localizer["Suivre mes dons aux performers"]
Your posts:
@Model.PostsCounter
diff --git a/src/Yavsc.Org/Views/Manage/SetActivity.cshtml b/src/Yavsc.Org/Views/Manage/SetActivity.cshtml index 198361100..b096fc21d 100644 --- a/src/Yavsc.Org/Views/Manage/SetActivity.cshtml +++ b/src/Yavsc.Org/Views/Manage/SetActivity.cshtml @@ -14,6 +14,11 @@ @Html.DisplayFor(model => model) +@if (!string.IsNullOrWhiteSpace(Model.PerformerId)) +{ +

@Localizer["Configurer mes dons PayPal"]

+} +

Setup below your activity parameters:


diff --git a/src/Yavsc.Org/Views/Podcast/Index.cshtml b/src/Yavsc.Org/Views/Podcast/Index.cshtml new file mode 100644 index 000000000..7121349fd --- /dev/null +++ b/src/Yavsc.Org/Views/Podcast/Index.cshtml @@ -0,0 +1,105 @@ +@using Yavsc.Abstract.Streaming +@model Yavsc.ViewModels.Podcast.PodcastCatalogueViewModel +@{ + ViewBag.Title = Localizer["Podcasts"]; +} + +
+
+

@Localizer["Podcasts"]

+

@Localizer["Retrouvez les directs et les replays de la communauté Yavsc."]

+

@Localizer["Lecture réservée aux utilisateurs connectés. Les flux sont fournis par Yavsc.Api."]

+
+ +
+ +@if (Model.Error != null) +{ + +} +else if (Model.Podcasts.Count == 0) +{ +
@Localizer["Aucun podcast n'est disponible pour le moment."]
+} +else +{ +
+ @foreach (var podcast in Model.Podcasts) + { + var ready = podcast.State == PodcastState.Completed && podcast.Replay?.State == PodcastReplayState.Ready; + var stateLabel = podcast.State switch + { + PodcastState.Live => Localizer["En direct"], + PodcastState.Completed => Localizer["Replay"], + _ => Localizer["Direct interrompu"] + }; +
+
+
+
+

@podcast.Title

+ @stateLabel +
+

+ @podcast.OwnerName · +

+

@Localizer["Durée : {0} s", podcast.DurationSeconds.ToString("0.##")]

+ @if (podcast.SegmentCount > 0) + { + + @if (!ready) + { + + } + + @if (!ready && podcast.State == PodcastState.Completed) + { +

+ @(podcast.Replay?.State == PodcastReplayState.Failed + ? Localizer["La préparation du replay MP4 a échoué. L'auteur peut la relancer depuis PostIt."] + : Localizer["Le replay MP4 est en préparation. La lecture HLS reste disponible."]) +

+ } + } + else + { +

@Localizer["Aucun segment n'est encore disponible. Actualisez la page pour suivre ce podcast."]

+ } +
+ +
+
+ } +
+} + +@section scripts { + + +} diff --git a/src/Yavsc.Org/Views/Shared/_Nav.cshtml b/src/Yavsc.Org/Views/Shared/_Nav.cshtml index 6d5e7bb78..1644ccae4 100644 --- a/src/Yavsc.Org/Views/Shared/_Nav.cshtml +++ b/src/Yavsc.Org/Views/Shared/_Nav.cshtml @@ -15,6 +15,7 @@ background-attachment: fixed;">