diff --git a/DysonNetwork.Drive/Startup/BroadcastEventHandler.cs b/DysonNetwork.Drive/Startup/BroadcastEventHandler.cs index 79bab7f..265f2e7 100644 --- a/DysonNetwork.Drive/Startup/BroadcastEventHandler.cs +++ b/DysonNetwork.Drive/Startup/BroadcastEventHandler.cs @@ -14,7 +14,7 @@ public class BroadcastEventHandler( { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - await foreach (var msg in nats.SubscribeAsync("accounts.deleted", cancellationToken: stoppingToken)) + await foreach (var msg in nats.SubscribeAsync(AccountDeletedEvent.Type, cancellationToken: stoppingToken)) { try { diff --git a/DysonNetwork.Pass/Migrations/20250904144723_AddOrderProductIdentifier.Designer.cs b/DysonNetwork.Pass/Migrations/20250904144723_AddOrderProductIdentifier.Designer.cs new file mode 100644 index 0000000..eb415d9 --- /dev/null +++ b/DysonNetwork.Pass/Migrations/20250904144723_AddOrderProductIdentifier.Designer.cs @@ -0,0 +1,2021 @@ +// +using System; +using System.Collections.Generic; +using System.Text.Json; +using DysonNetwork.Pass; +using DysonNetwork.Pass.Account; +using DysonNetwork.Pass.Wallet; +using DysonNetwork.Shared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace DysonNetwork.Pass.Migrations +{ + [DbContext(typeof(AppDatabase))] + [Migration("20250904144723_AddOrderProductIdentifier")] + partial class AddOrderProductIdentifier + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AbuseReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("reason"); + + b.Property("Resolution") + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("resolution"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("resolved_at"); + + b.Property("ResourceIdentifier") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("resource_identifier"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_abuse_reports"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_abuse_reports_account_id"); + + b.ToTable("abuse_reports", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("activated_at"); + + b.Property("AutomatedId") + .HasColumnType("uuid") + .HasColumnName("automated_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsSuperuser") + .HasColumnType("boolean") + .HasColumnName("is_superuser"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("language"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("Nick") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("nick"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_accounts_name"); + + b.ToTable("accounts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AccountAuthFactor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property>("Config") + .HasColumnType("jsonb") + .HasColumnName("config"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("EnabledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("enabled_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Secret") + .HasMaxLength(8196) + .HasColumnType("character varying(8196)") + .HasColumnName("secret"); + + b.Property("Trustworthy") + .HasColumnType("integer") + .HasColumnName("trustworthy"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_auth_factors"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_auth_factors_account_id"); + + b.ToTable("account_auth_factors", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AccountBadge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("activated_at"); + + b.Property("Caption") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("caption"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Label") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("label"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_badges"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_badges_account_id"); + + b.ToTable("badges", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AccountConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccessToken") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("access_token"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("LastUsedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_at"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("ProvidedIdentifier") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("provided_identifier"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("provider"); + + b.Property("RefreshToken") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("refresh_token"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_connections"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_connections_account_id"); + + b.ToTable("account_connections", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AccountContact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsPrimary") + .HasColumnType("boolean") + .HasColumnName("is_primary"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_at"); + + b.HasKey("Id") + .HasName("pk_account_contacts"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_contacts_account_id"); + + b.ToTable("account_contacts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AccountProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("ActiveBadge") + .HasColumnType("jsonb") + .HasColumnName("active_badge"); + + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("bio"); + + b.Property("Birthday") + .HasColumnType("timestamp with time zone") + .HasColumnName("birthday"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Experience") + .HasColumnType("integer") + .HasColumnName("experience"); + + b.Property("FirstName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("first_name"); + + b.Property("Gender") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("gender"); + + b.Property("LastName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("last_name"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_at"); + + b.Property>("Links") + .HasColumnType("jsonb") + .HasColumnName("links"); + + b.Property("Location") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("location"); + + b.Property("MiddleName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("middle_name"); + + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); + + b.Property("Pronouns") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("pronouns"); + + b.Property("SocialCredits") + .HasColumnType("double precision") + .HasColumnName("social_credits"); + + b.Property("TimeZone") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("time_zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Verification") + .HasColumnType("jsonb") + .HasColumnName("verification"); + + b.HasKey("Id") + .HasName("pk_account_profiles"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_account_profiles_account_id"); + + b.ToTable("account_profiles", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.ActionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("action"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IpAddress") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("ip_address"); + + b.Property("Location") + .HasColumnType("geometry") + .HasColumnName("location"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("SessionId") + .HasColumnType("uuid") + .HasColumnName("session_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserAgent") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("user_agent"); + + b.HasKey("Id") + .HasName("pk_action_logs"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_action_logs_account_id"); + + b.ToTable("action_logs", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.CheckInResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("BackdatedFrom") + .HasColumnType("timestamp with time zone") + .HasColumnName("backdated_from"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Level") + .HasColumnType("integer") + .HasColumnName("level"); + + b.Property("RewardExperience") + .HasColumnType("integer") + .HasColumnName("reward_experience"); + + b.Property("RewardPoints") + .HasColumnType("numeric") + .HasColumnName("reward_points"); + + b.Property>("Tips") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("tips"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_check_in_results"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_check_in_results_account_id"); + + b.ToTable("account_check_in_results", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.MagicSpell", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Spell") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("spell"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_magic_spells"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_magic_spells_account_id"); + + b.HasIndex("Spell") + .IsUnique() + .HasDatabaseName("ix_magic_spells_spell"); + + b.ToTable("magic_spells", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.Punishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property>("BlockedPermissions") + .HasColumnType("jsonb") + .HasColumnName("blocked_permissions"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("reason"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_punishments"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_punishments_account_id"); + + b.ToTable("punishments", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.Relationship", b => + { + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("RelatedId") + .HasColumnType("uuid") + .HasColumnName("related_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Status") + .HasColumnType("smallint") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("AccountId", "RelatedId") + .HasName("pk_account_relationships"); + + b.HasIndex("RelatedId") + .HasDatabaseName("ix_account_relationships_related_id"); + + b.ToTable("account_relationships", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.Status", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Attitude") + .HasColumnType("integer") + .HasColumnName("attitude"); + + b.Property("ClearedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("cleared_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsInvisible") + .HasColumnType("boolean") + .HasColumnName("is_invisible"); + + b.Property("IsNotDisturb") + .HasColumnType("boolean") + .HasColumnName("is_not_disturb"); + + b.Property("Label") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("label"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_statuses"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_statuses_account_id"); + + b.ToTable("account_statuses", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Auth.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("label"); + + b.Property("SessionId") + .HasColumnType("uuid") + .HasColumnName("session_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_api_keys"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_api_keys_account_id"); + + b.HasIndex("SessionId") + .HasDatabaseName("ix_api_keys_session_id"); + + b.ToTable("api_keys", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthChallenge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property>("Audiences") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("audiences"); + + b.Property>("BlacklistFactors") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("blacklist_factors"); + + b.Property("ClientId") + .HasColumnType("uuid") + .HasColumnName("client_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("FailedAttempts") + .HasColumnType("integer") + .HasColumnName("failed_attempts"); + + b.Property("IpAddress") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("ip_address"); + + b.Property("Location") + .HasColumnType("geometry") + .HasColumnName("location"); + + b.Property("Nonce") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("nonce"); + + b.Property>("Scopes") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("scopes"); + + b.Property("StepRemain") + .HasColumnType("integer") + .HasColumnName("step_remain"); + + b.Property("StepTotal") + .HasColumnType("integer") + .HasColumnName("step_total"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserAgent") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("user_agent"); + + b.HasKey("Id") + .HasName("pk_auth_challenges"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_auth_challenges_account_id"); + + b.HasIndex("ClientId") + .HasDatabaseName("ix_auth_challenges_client_id"); + + b.ToTable("auth_challenges", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthClient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("device_id"); + + b.Property("DeviceLabel") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("device_label"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("device_name"); + + b.Property("Platform") + .HasColumnType("integer") + .HasColumnName("platform"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_auth_clients"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_auth_clients_account_id"); + + b.ToTable("auth_clients", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("AppId") + .HasColumnType("uuid") + .HasColumnName("app_id"); + + b.Property("ChallengeId") + .HasColumnType("uuid") + .HasColumnName("challenge_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("LastGrantedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_granted_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_auth_sessions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_auth_sessions_account_id"); + + b.HasIndex("ChallengeId") + .HasDatabaseName("ix_auth_sessions_challenge_id"); + + b.ToTable("auth_sessions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Credit.SocialCreditRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Delta") + .HasColumnType("double precision") + .HasColumnName("delta"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("reason"); + + b.Property("ReasonType") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("reason_type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_social_credit_records"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_social_credit_records_account_id"); + + b.ToTable("social_credit_records", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Leveling.ExperienceRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("BonusMultiplier") + .HasColumnType("double precision") + .HasColumnName("bonus_multiplier"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Delta") + .HasColumnType("bigint") + .HasColumnName("delta"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("reason"); + + b.Property("ReasonType") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("reason_type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_experience_records"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_experience_records_account_id"); + + b.ToTable("experience_records", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("key"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_permission_groups"); + + b.ToTable("permission_groups", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionGroupMember", b => + { + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("Actor") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("actor"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("GroupId", "Actor") + .HasName("pk_permission_group_members"); + + b.ToTable("permission_group_members", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("actor"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("Area") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("area"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("key"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Value") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_permission_nodes"); + + b.HasIndex("GroupId") + .HasDatabaseName("ix_permission_nodes_group_id"); + + b.HasIndex("Key", "Area", "Actor") + .HasDatabaseName("ix_permission_nodes_key_area_actor"); + + b.ToTable("permission_nodes", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("Code") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DiscountAmount") + .HasColumnType("numeric") + .HasColumnName("discount_amount"); + + b.Property("DiscountRate") + .HasColumnType("double precision") + .HasColumnName("discount_rate"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Identifier") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("identifier"); + + b.Property("MaxUsage") + .HasColumnType("integer") + .HasColumnName("max_usage"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_coupons"); + + b.ToTable("wallet_coupons", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("AppIdentifier") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("app_identifier"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("PayeeWalletId") + .HasColumnType("uuid") + .HasColumnName("payee_wallet_id"); + + b.Property("ProductIdentifier") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("product_identifier"); + + b.Property("Remarks") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("remarks"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("TransactionId") + .HasColumnType("uuid") + .HasColumnName("transaction_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_payment_orders"); + + b.HasIndex("PayeeWalletId") + .HasDatabaseName("ix_payment_orders_payee_wallet_id"); + + b.HasIndex("TransactionId") + .HasDatabaseName("ix_payment_orders_transaction_id"); + + b.ToTable("payment_orders", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("BasePrice") + .HasColumnType("numeric") + .HasColumnName("base_price"); + + b.Property("BegunAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("begun_at"); + + b.Property("CouponId") + .HasColumnType("uuid") + .HasColumnName("coupon_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ended_at"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("identifier"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsFreeTrial") + .HasColumnType("boolean") + .HasColumnName("is_free_trial"); + + b.Property("PaymentDetails") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payment_details"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("payment_method"); + + b.Property("RenewalAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("renewal_at"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_subscriptions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_wallet_subscriptions_account_id"); + + b.HasIndex("CouponId") + .HasDatabaseName("ix_wallet_subscriptions_coupon_id"); + + b.HasIndex("Identifier") + .HasDatabaseName("ix_wallet_subscriptions_identifier"); + + b.ToTable("wallet_subscriptions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("PayeeWalletId") + .HasColumnType("uuid") + .HasColumnName("payee_wallet_id"); + + b.Property("PayerWalletId") + .HasColumnType("uuid") + .HasColumnName("payer_wallet_id"); + + b.Property("Remarks") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("remarks"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_payment_transactions"); + + b.HasIndex("PayeeWalletId") + .HasDatabaseName("ix_payment_transactions_payee_wallet_id"); + + b.HasIndex("PayerWalletId") + .HasDatabaseName("ix_payment_transactions_payer_wallet_id"); + + b.ToTable("payment_transactions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Wallet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallets"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_wallets_account_id"); + + b.ToTable("wallets", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.WalletPocket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("WalletId") + .HasColumnType("uuid") + .HasColumnName("wallet_id"); + + b.HasKey("Id") + .HasName("pk_wallet_pockets"); + + b.HasIndex("WalletId") + .HasDatabaseName("ix_wallet_pockets_wallet_id"); + + b.ToTable("wallet_pockets", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AbuseReport", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_abuse_reports_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AccountAuthFactor", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany("AuthFactors") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_auth_factors_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AccountBadge", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany("Badges") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_badges_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AccountConnection", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany("Connections") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_connections_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AccountContact", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany("Contacts") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_contacts_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AccountProfile", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithOne("Profile") + .HasForeignKey("DysonNetwork.Pass.Account.AccountProfile", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_profiles_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.ActionLog", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_action_logs_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.CheckInResult", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_check_in_results_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.MagicSpell", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .HasConstraintName("fk_magic_spells_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.Punishment", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_punishments_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.Relationship", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany("OutgoingRelationships") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_relationships_accounts_account_id"); + + b.HasOne("DysonNetwork.Pass.Account.Account", "Related") + .WithMany("IncomingRelationships") + .HasForeignKey("RelatedId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_relationships_accounts_related_id"); + + b.Navigation("Account"); + + b.Navigation("Related"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.Status", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_statuses_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Auth.ApiKey", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_api_keys_accounts_account_id"); + + b.HasOne("DysonNetwork.Pass.Auth.AuthSession", "Session") + .WithMany() + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_api_keys_auth_sessions_session_id"); + + b.Navigation("Account"); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthChallenge", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany("Challenges") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_challenges_accounts_account_id"); + + b.HasOne("DysonNetwork.Pass.Auth.AuthClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .HasConstraintName("fk_auth_challenges_auth_clients_client_id"); + + b.Navigation("Account"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthClient", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_clients_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthSession", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany("Sessions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_sessions_accounts_account_id"); + + b.HasOne("DysonNetwork.Pass.Auth.AuthChallenge", "Challenge") + .WithMany() + .HasForeignKey("ChallengeId") + .HasConstraintName("fk_auth_sessions_auth_challenges_challenge_id"); + + b.Navigation("Account"); + + b.Navigation("Challenge"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Credit.SocialCreditRecord", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_social_credit_records_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Leveling.ExperienceRecord", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_experience_records_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionGroupMember", b => + { + b.HasOne("DysonNetwork.Pass.Permission.PermissionGroup", "Group") + .WithMany("Members") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_permission_group_members_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionNode", b => + { + b.HasOne("DysonNetwork.Pass.Permission.PermissionGroup", "Group") + .WithMany("Nodes") + .HasForeignKey("GroupId") + .HasConstraintName("fk_permission_nodes_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Order", b => + { + b.HasOne("DysonNetwork.Pass.Wallet.Wallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .HasConstraintName("fk_payment_orders_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Pass.Wallet.Transaction", "Transaction") + .WithMany() + .HasForeignKey("TransactionId") + .HasConstraintName("fk_payment_orders_payment_transactions_transaction_id"); + + b.Navigation("PayeeWallet"); + + b.Navigation("Transaction"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Subscription", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_subscriptions_accounts_account_id"); + + b.HasOne("DysonNetwork.Pass.Wallet.Coupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .HasConstraintName("fk_wallet_subscriptions_wallet_coupons_coupon_id"); + + b.Navigation("Account"); + + b.Navigation("Coupon"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Transaction", b => + { + b.HasOne("DysonNetwork.Pass.Wallet.Wallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Pass.Wallet.Wallet", "PayerWallet") + .WithMany() + .HasForeignKey("PayerWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payer_wallet_id"); + + b.Navigation("PayeeWallet"); + + b.Navigation("PayerWallet"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Wallet", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallets_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.WalletPocket", b => + { + b.HasOne("DysonNetwork.Pass.Wallet.Wallet", "Wallet") + .WithMany("Pockets") + .HasForeignKey("WalletId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_pockets_wallets_wallet_id"); + + b.Navigation("Wallet"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.Account", b => + { + b.Navigation("AuthFactors"); + + b.Navigation("Badges"); + + b.Navigation("Challenges"); + + b.Navigation("Connections"); + + b.Navigation("Contacts"); + + b.Navigation("IncomingRelationships"); + + b.Navigation("OutgoingRelationships"); + + b.Navigation("Profile") + .IsRequired(); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionGroup", b => + { + b.Navigation("Members"); + + b.Navigation("Nodes"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Wallet", b => + { + b.Navigation("Pockets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DysonNetwork.Pass/Migrations/20250904144723_AddOrderProductIdentifier.cs b/DysonNetwork.Pass/Migrations/20250904144723_AddOrderProductIdentifier.cs new file mode 100644 index 0000000..ba30744 --- /dev/null +++ b/DysonNetwork.Pass/Migrations/20250904144723_AddOrderProductIdentifier.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DysonNetwork.Pass.Migrations +{ + /// + public partial class AddOrderProductIdentifier : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "product_identifier", + table: "payment_orders", + type: "character varying(4096)", + maxLength: 4096, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "product_identifier", + table: "payment_orders"); + } + } +} diff --git a/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs b/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs index 736bcc5..cd300fc 100644 --- a/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs +++ b/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs @@ -1381,6 +1381,11 @@ namespace DysonNetwork.Pass.Migrations .HasColumnType("uuid") .HasColumnName("payee_wallet_id"); + b.Property("ProductIdentifier") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("product_identifier"); + b.Property("Remarks") .HasMaxLength(4096) .HasColumnType("character varying(4096)") diff --git a/DysonNetwork.Pass/Resources/Localization/NotificationResource.Designer.cs b/DysonNetwork.Pass/Resources/Localization/NotificationResource.Designer.cs index 75bfdf4..415557b 100644 --- a/DysonNetwork.Pass/Resources/Localization/NotificationResource.Designer.cs +++ b/DysonNetwork.Pass/Resources/Localization/NotificationResource.Designer.cs @@ -170,5 +170,47 @@ namespace DysonNetwork.Sphere.Resources.Localization { return ResourceManager.GetString("NewLoginBody", resourceCulture); } } + + internal static string FriendRequestTitle { + get { + return ResourceManager.GetString("FriendRequestTitle", resourceCulture); + } + } + + internal static string FriendRequestBody { + get { + return ResourceManager.GetString("FriendRequestBody", resourceCulture); + } + } + + internal static string OrderReceivedTitle { + get { + return ResourceManager.GetString("OrderReceivedTitle", resourceCulture); + } + } + + internal static string OrderReceivedBody { + get { + return ResourceManager.GetString("OrderReceivedBody", resourceCulture); + } + } + + internal static string TransactionNewTitle { + get { + return ResourceManager.GetString("TransactionNewTitle", resourceCulture); + } + } + + internal static string TransactionNewBodyPlus { + get { + return ResourceManager.GetString("TransactionNewBodyPlus", resourceCulture); + } + } + + internal static string TransactionNewBodyMinus { + get { + return ResourceManager.GetString("TransactionNewBodyMinus", resourceCulture); + } + } } } diff --git a/DysonNetwork.Pass/Resources/Localization/NotificationResource.resx b/DysonNetwork.Pass/Resources/Localization/NotificationResource.resx index dfbe7ef..24a0232 100644 --- a/DysonNetwork.Pass/Resources/Localization/NotificationResource.resx +++ b/DysonNetwork.Pass/Resources/Localization/NotificationResource.resx @@ -78,7 +78,7 @@ Order {0} recipent - {0} {1} was removed from your wallet to pay {2} + Paid order {2} with {0} {1} New login detected @@ -92,4 +92,19 @@ You can go to relationships page and decide accept their request or not. + + Order {0} recipent + + + Received {2} payment of {0} {1} + + + Transaction {0} + + + {0} {1} added to your wallet + + + {0} {1} removed from your wallet + \ No newline at end of file diff --git a/DysonNetwork.Pass/Resources/Localization/NotificationResource.zh-hans.resx b/DysonNetwork.Pass/Resources/Localization/NotificationResource.zh-hans.resx index 10546f1..695454d 100644 --- a/DysonNetwork.Pass/Resources/Localization/NotificationResource.zh-hans.resx +++ b/DysonNetwork.Pass/Resources/Localization/NotificationResource.zh-hans.resx @@ -67,10 +67,10 @@ 感谢你支持 Solar Network 的开发!你的 {0} 天 {1} 订阅刚刚开始,接下来来探索新解锁的新功能吧! - 订单回执 {0} + 订单收据 {0} - {0} {1} 已从你的帐户中扣除来支付 {2} + 已支付订单 {2} 的 {0} {1} 检测到新登陆 @@ -84,4 +84,19 @@ 您可以前往人际关系页面来决定时候要接受他们的邀请。 + + 订单收据 {0} + + + 收到订单 {2} 支付的 {0} {1} + + + 交易 {0} + + + {0} {1} 添加到了您的钱包 + + + {0} {1} 从您的钱包移除 + \ No newline at end of file diff --git a/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs b/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs index 94f475a..2e4ca59 100644 --- a/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs +++ b/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs @@ -4,6 +4,7 @@ using DysonNetwork.Pass.Auth; using DysonNetwork.Pass.Credit; using DysonNetwork.Pass.Leveling; using DysonNetwork.Pass.Permission; +using DysonNetwork.Pass.Wallet; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.FileProviders; using Prometheus; @@ -81,6 +82,8 @@ public static class ApplicationConfiguration app.MapGrpcService(); app.MapGrpcService(); app.MapGrpcService(); + app.MapGrpcService(); + app.MapGrpcService(); return app; } diff --git a/DysonNetwork.Pass/Startup/BroadcastEventHandler.cs b/DysonNetwork.Pass/Startup/BroadcastEventHandler.cs new file mode 100644 index 0000000..5d2f9c4 --- /dev/null +++ b/DysonNetwork.Pass/Startup/BroadcastEventHandler.cs @@ -0,0 +1,34 @@ +using System.Text.Json; +using DysonNetwork.Pass.Wallet; +using DysonNetwork.Shared.Stream; +using NATS.Client.Core; + +namespace DysonNetwork.Pass.Startup; + +public class BroadcastEventHandler( + INatsConnection nats, + ILogger logger, + IServiceProvider serviceProvider +) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await foreach (var msg in nats.SubscribeAsync(PaymentOrderEvent.Type, cancellationToken: stoppingToken)) + { + try + { + var evt = JsonSerializer.Deserialize(msg.Data); + + if (evt?.ProductIdentifier is null || !evt.ProductIdentifier.StartsWith(SubscriptionType.StellarProgram)) + continue; + + logger.LogInformation("Stellar program order paid: {OrderId}", evt.OrderId); + + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing AccountDeleted"); + } + } + } +} \ No newline at end of file diff --git a/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs index 7e6355e..4c08a28 100644 --- a/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs @@ -54,10 +54,6 @@ public static class ServiceCollectionExtensions services.AddPusherService(); - // Register gRPC services - services.AddScoped(); - services.AddScoped(); - // Register OIDC services services.AddScoped(); services.AddScoped(); diff --git a/DysonNetwork.Pass/Wallet/OrderController.cs b/DysonNetwork.Pass/Wallet/OrderController.cs index bbca4a9..6444394 100644 --- a/DysonNetwork.Pass/Wallet/OrderController.cs +++ b/DysonNetwork.Pass/Wallet/OrderController.cs @@ -15,9 +15,7 @@ public class OrderController(PaymentService payment, AuthService auth, AppDataba var order = await db.PaymentOrders.FindAsync(id); if (order == null) - { return NotFound(); - } return Ok(order); } @@ -41,7 +39,7 @@ public class OrderController(PaymentService payment, AuthService auth, AppDataba return BadRequest("Wallet was not found."); // Pay the order - var paidOrder = await payment.PayOrderAsync(id, wallet.Id); + var paidOrder = await payment.PayOrderAsync(id, wallet); return Ok(paidOrder); } catch (InvalidOperationException ex) diff --git a/DysonNetwork.Pass/Wallet/Payment.cs b/DysonNetwork.Pass/Wallet/Payment.cs index 78da3be..33f0e46 100644 --- a/DysonNetwork.Pass/Wallet/Payment.cs +++ b/DysonNetwork.Pass/Wallet/Payment.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Globalization; using DysonNetwork.Shared.Data; using NodaTime; using NodaTime.Serialization.Protobuf; @@ -23,11 +24,14 @@ public enum OrderStatus public class Order : ModelBase { + public const string InternalAppIdentifier = "internal"; + public Guid Id { get; set; } = Guid.NewGuid(); public OrderStatus Status { get; set; } = OrderStatus.Unpaid; [MaxLength(128)] public string Currency { get; set; } = null!; [MaxLength(4096)] public string? Remarks { get; set; } [MaxLength(4096)] public string? AppIdentifier { get; set; } + [MaxLength(4096)] public string? ProductIdentifier { get; set; } [Column(TypeName = "jsonb")] public Dictionary? Meta { get; set; } public decimal Amount { get; set; } public Instant ExpiredAt { get; set; } @@ -44,10 +48,11 @@ public class Order : ModelBase Currency = Currency, Remarks = Remarks, AppIdentifier = AppIdentifier, + ProductIdentifier = ProductIdentifier, Meta = Meta == null ? null : Google.Protobuf.ByteString.CopyFrom(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(Meta)), - Amount = Amount.ToString(), + Amount = Amount.ToString(CultureInfo.InvariantCulture), ExpiredAt = ExpiredAt.ToTimestamp(), PayeeWalletId = PayeeWalletId?.ToString(), TransactionId = TransactionId?.ToString(), @@ -61,6 +66,7 @@ public class Order : ModelBase Currency = proto.Currency, Remarks = proto.Remarks, AppIdentifier = proto.AppIdentifier, + ProductIdentifier = proto.ProductIdentifier, Meta = proto.HasMeta ? System.Text.Json.JsonSerializer.Deserialize>(proto.Meta.ToByteArray()) : null, diff --git a/DysonNetwork.Pass/Wallet/PaymentService.cs b/DysonNetwork.Pass/Wallet/PaymentService.cs index 98d6e50..88d211d 100644 --- a/DysonNetwork.Pass/Wallet/PaymentService.cs +++ b/DysonNetwork.Pass/Wallet/PaymentService.cs @@ -1,9 +1,12 @@ using System.Globalization; +using System.Text.Json; using DysonNetwork.Pass.Localization; using DysonNetwork.Shared.Proto; +using DysonNetwork.Shared.Stream; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Localization; +using NATS.Client.Core; using NodaTime; using AccountService = DysonNetwork.Pass.Account.AccountService; @@ -13,7 +16,8 @@ public class PaymentService( AppDatabase db, WalletService wat, PusherService.PusherServiceClient pusher, - IStringLocalizer localizer + IStringLocalizer localizer, + INatsConnection nats ) { public async Task CreateOrderAsync( @@ -22,6 +26,7 @@ public class PaymentService( decimal amount, Duration? expiration = null, string? appIdentifier = null, + string? productIdentifier = null, Dictionary? meta = null, bool reuseable = true ) @@ -29,26 +34,25 @@ public class PaymentService( // Check if there's an existing unpaid order that can be reused if (reuseable && appIdentifier != null) { + var now = SystemClock.Instance.GetCurrentInstant(); var existingOrder = await db.PaymentOrders .Where(o => o.Status == OrderStatus.Unpaid && o.PayeeWalletId == payeeWalletId && o.Currency == currency && o.Amount == amount && o.AppIdentifier == appIdentifier && - o.ExpiredAt > SystemClock.Instance.GetCurrentInstant()) + o.ProductIdentifier == productIdentifier && + o.ExpiredAt > now) .FirstOrDefaultAsync(); // If an existing order is found, check if meta matches if (existingOrder != null && meta != null && existingOrder.Meta != null) { - // Compare meta dictionaries - if they are equivalent, reuse the order + // Compare the meta dictionary - if they are equivalent, reuse the order var metaMatches = existingOrder.Meta.Count == meta.Count && !existingOrder.Meta.Except(meta).Any(); - if (metaMatches) - { return existingOrder; - } } } @@ -60,6 +64,7 @@ public class PaymentService( Amount = amount, ExpiredAt = SystemClock.Instance.GetCurrentInstant().Plus(expiration ?? Duration.FromHours(24)), AppIdentifier = appIdentifier, + ProductIdentifier = productIdentifier, Meta = meta }; @@ -104,7 +109,8 @@ public class PaymentService( string currency, decimal amount, string? remarks = null, - TransactionType type = TransactionType.System + TransactionType type = TransactionType.System, + bool silent = false ) { if (payerWalletId == null && payeeWalletId == null) @@ -121,8 +127,12 @@ public class PaymentService( Type = type }; + Wallet? payerWallet = null, payeeWallet = null; + if (payerWalletId.HasValue) { + payerWallet = await db.Wallets.FirstOrDefaultAsync(e => e.AccountId == payerWalletId.Value); + var (payerPocket, isNewlyCreated) = await wat.GetOrCreateWalletPocketAsync(payerWalletId.Value, currency); @@ -137,6 +147,8 @@ public class PaymentService( if (payeeWalletId.HasValue) { + payeeWallet = await db.Wallets.FirstOrDefaultAsync(e => e.AccountId == payeeWalletId.Value); + var (payeePocket, isNewlyCreated) = await wat.GetOrCreateWalletPocketAsync(payeeWalletId.Value, currency, amount); @@ -149,13 +161,85 @@ public class PaymentService( db.PaymentTransactions.Add(transaction); await db.SaveChangesAsync(); + + if (!silent) + await NotifyNewTransaction(transaction, payerWallet, payeeWallet); + return transaction; } + + private async Task NotifyNewTransaction(Transaction transaction, Wallet? payerWallet, Wallet? payeeWallet) + { + if (payerWallet is not null) + { + var account = await db.Accounts + .Where(a => a.Id == payerWallet.AccountId) + .FirstOrDefaultAsync(); + if (account is null) return; - public async Task PayOrderAsync(Guid orderId, Guid payerWalletId) + AccountService.SetCultureInfo(account); + + // Due to ID is uuid, it longer than 8 words for sure + var readableTransactionId = transaction.Id.ToString().Replace("-", "")[..8]; + var readableTransactionRemark = transaction.Remarks ?? $"#{readableTransactionId}"; + + await pusher.SendPushNotificationToUserAsync( + new SendPushNotificationToUserRequest + { + UserId = account.Id.ToString(), + Notification = new PushNotification + { + Topic = "wallets.transactions", + Title = transaction.Amount > 0 + ? localizer["TransactionNewBodyMinus", readableTransactionRemark] + : localizer["TransactionNewBodyPlus", readableTransactionRemark], + Body = localizer["TransactionNewTitle", + transaction.Amount.ToString(CultureInfo.InvariantCulture), + transaction.Currency], + IsSavable = true + } + } + ); + } + + if (payeeWallet is not null) + { + var account = await db.Accounts + .Where(a => a.Id == payeeWallet.AccountId) + .FirstOrDefaultAsync(); + if (account is null) return; + + AccountService.SetCultureInfo(account); + + // Due to ID is uuid, it longer than 8 words for sure + var readableTransactionId = transaction.Id.ToString().Replace("-", "")[..8]; + var readableTransactionRemark = transaction.Remarks ?? $"#{readableTransactionId}"; + + await pusher.SendPushNotificationToUserAsync( + new SendPushNotificationToUserRequest + { + UserId = account.Id.ToString(), + Notification = new PushNotification + { + Topic = "wallets.transactions", + Title = transaction.Amount > 0 + ? localizer["TransactionNewBodyPlus", readableTransactionRemark] + : localizer["TransactionNewBodyMinus", readableTransactionRemark], + Body = localizer["TransactionNewTitle", + transaction.Amount.ToString(CultureInfo.InvariantCulture), + transaction.Currency], + IsSavable = true + } + } + ); + } + } + + public async Task PayOrderAsync(Guid orderId, Wallet payerWallet) { var order = await db.PaymentOrders .Include(o => o.Transaction) + .Include(o => o.PayeeWallet) .FirstOrDefaultAsync(o => o.Id == orderId); if (order == null) @@ -176,7 +260,7 @@ public class PaymentService( } var transaction = await CreateTransactionAsync( - payerWalletId, + payerWallet.Id, order.PayeeWalletId, order.Currency, order.Amount, @@ -186,41 +270,87 @@ public class PaymentService( order.TransactionId = transaction.Id; order.Transaction = transaction; order.Status = OrderStatus.Paid; - + await db.SaveChangesAsync(); - - await NotifyOrderPaid(order); - + + await NotifyOrderPaid(order, payerWallet, order.PayeeWallet); + + await nats.PublishAsync(PaymentOrderEvent.Type, JsonSerializer.SerializeToUtf8Bytes(new PaymentOrderEvent + { + OrderId = order.Id, + WalletId = payerWallet.Id, + AccountId = payerWallet.AccountId, + AppIdentifier = order.AppIdentifier, + ProductIdentifier = order.ProductIdentifier, + Meta = order.Meta ?? [], + Status = (int)order.Status, + })); + return order; } - private async Task NotifyOrderPaid(Order order) + private async Task NotifyOrderPaid(Order order, Wallet? payerWallet, Wallet? payeeWallet) { - if (order.PayeeWallet is null) return; - var account = await db.Accounts.FirstOrDefaultAsync(a => a.Id == order.PayeeWallet.AccountId); - if (account is null) return; - - AccountService.SetCultureInfo(account); + if (payerWallet is not null) + { + var account = await db.Accounts + .Where(a => a.Id == payerWallet.AccountId) + .FirstOrDefaultAsync(); + if (account is null) return; - // Due to ID is uuid, it longer than 8 words for sure - var readableOrderId = order.Id.ToString().Replace("-", "")[..8]; - var readableOrderRemark = order.Remarks ?? $"#{readableOrderId}"; + AccountService.SetCultureInfo(account); - - await pusher.SendPushNotificationToUserAsync( - new SendPushNotificationToUserRequest - { - UserId = account.Id.ToString(), - Notification = new PushNotification + // Due to ID is uuid, it longer than 8 words for sure + var readableOrderId = order.Id.ToString().Replace("-", "")[..8]; + var readableOrderRemark = order.Remarks ?? $"#{readableOrderId}"; + + + await pusher.SendPushNotificationToUserAsync( + new SendPushNotificationToUserRequest { - Topic = "wallets.orders.paid", - Title = localizer["OrderPaidTitle", $"#{readableOrderId}"], - Body = localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture), order.Currency, - readableOrderRemark], - IsSavable = true + UserId = account.Id.ToString(), + Notification = new PushNotification + { + Topic = "wallets.orders.paid", + Title = localizer["OrderPaidTitle", $"#{readableOrderId}"], + Body = localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture), + order.Currency, + readableOrderRemark], + IsSavable = true + } } - } - ); + ); + } + + if (payeeWallet is not null) + { + var account = await db.Accounts + .Where(a => a.Id == payeeWallet.AccountId) + .FirstOrDefaultAsync(); + if (account is null) return; + + AccountService.SetCultureInfo(account); + + // Due to ID is uuid, it longer than 8 words for sure + var readableOrderId = order.Id.ToString().Replace("-", "")[..8]; + var readableOrderRemark = order.Remarks ?? $"#{readableOrderId}"; + + await pusher.SendPushNotificationToUserAsync( + new SendPushNotificationToUserRequest + { + UserId = account.Id.ToString(), + Notification = new PushNotification + { + Topic = "wallets.orders.received", + Title = localizer["OrderReceivedTitle", $"#{readableOrderId}"], + Body = localizer["OrderReceivedBody", order.Amount.ToString(CultureInfo.InvariantCulture), + order.Currency, + readableOrderRemark], + IsSavable = true + } + } + ); + } } public async Task CancelOrderAsync(Guid orderId) diff --git a/DysonNetwork.Pass/Wallet/PaymentServiceGrpc.cs b/DysonNetwork.Pass/Wallet/PaymentServiceGrpc.cs index 7540fa3..18ea0ef 100644 --- a/DysonNetwork.Pass/Wallet/PaymentServiceGrpc.cs +++ b/DysonNetwork.Pass/Wallet/PaymentServiceGrpc.cs @@ -13,10 +13,10 @@ public class PaymentServiceGrpc(PaymentService paymentService) : Shared.Proto.Pa request.Currency, decimal.Parse(request.Amount), request.Expiration is not null ? Duration.FromSeconds(request.Expiration.Seconds) : null, - request.HasAppIdentifier ? request.AppIdentifier : null, - // Assuming meta is a JSON string + request.HasAppIdentifier ? request.AppIdentifier : Order.InternalAppIdentifier, + request.HasProductIdentifier ? request.ProductIdentifier : null, request.HasMeta - ? System.Text.Json.JsonSerializer.Deserialize>(request.Meta.ToStringUtf8()) + ? GrpcTypeHelper.ConvertByteStringToObject>(request.Meta) : null, request.Reuseable ); diff --git a/DysonNetwork.Pass/Wallet/SubscriptionRenewalJob.cs b/DysonNetwork.Pass/Wallet/SubscriptionRenewalJob.cs index 5ca89e7..815bd8b 100644 --- a/DysonNetwork.Pass/Wallet/SubscriptionRenewalJob.cs +++ b/DysonNetwork.Pass/Wallet/SubscriptionRenewalJob.cs @@ -86,7 +86,7 @@ public class SubscriptionRenewalJob( if (wallet is null) continue; // Process automatic payment from wallet - await paymentService.PayOrderAsync(order.Id, wallet.Id); + await paymentService.PayOrderAsync(order.Id, wallet); // Update subscription details subscription.BegunAt = subscription.EndedAt!.Value; diff --git a/DysonNetwork.Pass/Wallet/WalletServiceGrpc.cs b/DysonNetwork.Pass/Wallet/WalletServiceGrpc.cs index 2b5d2cd..d445abc 100644 --- a/DysonNetwork.Pass/Wallet/WalletServiceGrpc.cs +++ b/DysonNetwork.Pass/Wallet/WalletServiceGrpc.cs @@ -8,11 +8,7 @@ public class WalletServiceGrpc(WalletService walletService) : Shared.Proto.Walle public override async Task GetWallet(GetWalletRequest request, ServerCallContext context) { var wallet = await walletService.GetWalletAsync(Guid.Parse(request.AccountId)); - if (wallet == null) - { - throw new RpcException(new Status(StatusCode.NotFound, "Wallet not found.")); - } - return wallet.ToProtoValue(); + return wallet == null ? throw new RpcException(new Status(StatusCode.NotFound, "Wallet not found.")) : wallet.ToProtoValue(); } public override async Task CreateWallet(CreateWalletRequest request, ServerCallContext context) diff --git a/DysonNetwork.Shared/Proto/wallet.proto b/DysonNetwork.Shared/Proto/wallet.proto index f892f6a..74906b1 100644 --- a/DysonNetwork.Shared/Proto/wallet.proto +++ b/DysonNetwork.Shared/Proto/wallet.proto @@ -123,6 +123,7 @@ message CreateOrderRequest { string amount = 3; optional google.protobuf.Duration expiration = 4; optional string app_identifier = 5; + optional string product_identifier = 8; // Using bytes for meta to represent JSON. optional bytes meta = 6; bool reuseable = 7; @@ -135,6 +136,7 @@ message Order { string amount = 4; google.protobuf.Timestamp expired_at = 5; optional string app_identifier = 6; + optional string product_identifier = 12; // Using bytes for meta to represent JSON. optional bytes meta = 7; OrderStatus status = 8; diff --git a/DysonNetwork.Shared/Stream/PaymentEvent.cs b/DysonNetwork.Shared/Stream/PaymentEvent.cs new file mode 100644 index 0000000..cada601 --- /dev/null +++ b/DysonNetwork.Shared/Stream/PaymentEvent.cs @@ -0,0 +1,14 @@ +namespace DysonNetwork.Shared.Stream; + +public class PaymentOrderEvent +{ + public static string Type => "payments.orders"; + + public Guid OrderId { get; set; } + public Guid WalletId { get; set; } + public Guid AccountId { get; set; } + public string? AppIdentifier { get; set; } + public string? ProductIdentifier { get; set; } + public Dictionary Meta { get; set; } = null!; + public int Status { get; set; } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/AppDatabase.cs b/DysonNetwork.Sphere/AppDatabase.cs index 9c34281..9f61b4f 100644 --- a/DysonNetwork.Sphere/AppDatabase.cs +++ b/DysonNetwork.Sphere/AppDatabase.cs @@ -31,6 +31,7 @@ public class AppDatabase( public DbSet Posts { get; set; } = null!; public DbSet PostReactions { get; set; } = null!; + public DbSet PostAwards { get; set; } = null!; public DbSet PostTags { get; set; } = null!; public DbSet PostCategories { get; set; } = null!; public DbSet PostCollections { get; set; } = null!; diff --git a/DysonNetwork.Sphere/Post/Post.cs b/DysonNetwork.Sphere/Post/Post.cs index 9292093..ed8151a 100644 --- a/DysonNetwork.Sphere/Post/Post.cs +++ b/DysonNetwork.Sphere/Post/Post.cs @@ -51,6 +51,7 @@ public class Post : ModelBase, IIdentifiedResource, IActivity public int ViewsTotal { get; set; } public int Upvotes { get; set; } public int Downvotes { get; set; } + public decimal AwardedScore { get; set; } [NotMapped] public Dictionary ReactionsCount { get; set; } = new(); [NotMapped] public int RepliesCount { get; set; } [NotMapped] public Dictionary? ReactionsMade { get; set; } @@ -73,6 +74,7 @@ public class Post : ModelBase, IIdentifiedResource, IActivity public Guid PublisherId { get; set; } public Publisher.Publisher Publisher { get; set; } = null!; + public ICollection Awards { get; set; } = null!; public ICollection Reactions { get; set; } = new List(); public ICollection Tags { get; set; } = new List(); public ICollection Categories { get; set; } = new List(); @@ -168,3 +170,15 @@ public class PostReaction : ModelBase [JsonIgnore] public Post Post { get; set; } = null!; public Guid AccountId { get; set; } } + +public class PostAward : ModelBase +{ + public Guid Id { get; set; } + public decimal Amount { get; set; } + public PostReactionAttitude Attitude { get; set; } + [MaxLength(4096)] public string? Message { get; set; } + + public Guid PostId { get; set; } + [JsonIgnore] public Post Post { get; set; } = null!; + public Guid AccountId { get; set; } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Post/PostController.cs b/DysonNetwork.Sphere/Post/PostController.cs index 9cb4e46..2987617 100644 --- a/DysonNetwork.Sphere/Post/PostController.cs +++ b/DysonNetwork.Sphere/Post/PostController.cs @@ -579,6 +579,38 @@ public class PostController( return Ok(reaction); } + public class PostAwardRequest + { + public decimal Amount { get; set; } + [MaxLength(4096)] public string? Message { get; set; } + } + + [HttpPost("{id:guid}/awards")] + [Authorize] + public async Task> AwardPost(Guid id, [FromBody] PostAwardRequest request) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var friendsResponse = + await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest + { AccountId = currentUser.Id.ToString() }); + var userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); + var userPublishers = await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); + + var post = await db.Posts + .Where(e => e.Id == id) + .Include(e => e.Publisher) + .FilterWithVisibility(currentUser, userFriends, userPublishers) + .FirstOrDefaultAsync(); + if (post is null) return NotFound(); + + var accountId = Guid.Parse(currentUser.Id); + + // TODO Make payment, add record + + return Ok(); + } + public class PostPinRequest { [Required] public PostPinMode Mode { get; set; } @@ -600,7 +632,7 @@ public class PostController( var accountId = Guid.Parse(currentUser.Id); if (!await pub.IsMemberWithRole(post.PublisherId, accountId, PublisherMemberRole.Editor)) return StatusCode(403, "You are not an editor of this publisher"); - + if (request.Mode == PostPinMode.RealmPage && post.RealmId != null) { if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, RealmMemberRole.Moderator)) @@ -644,11 +676,11 @@ public class PostController( .Include(e => e.RepliedPost) .FirstOrDefaultAsync(); if (post is null) return NotFound(); - + var accountId = Guid.Parse(currentUser.Id); if (!await pub.IsMemberWithRole(post.PublisherId, accountId, PublisherMemberRole.Editor)) return StatusCode(403, "You are not an editor of this publisher"); - + if (post is { PinMode: PostPinMode.RealmPage, RealmId: not null }) { if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, RealmMemberRole.Moderator)) @@ -807,7 +839,7 @@ public class PostController( if (post is null) return NotFound(); if (!await pub.IsMemberWithRole(post.Publisher.Id, Guid.Parse(currentUser.Id), - Publisher.PublisherMemberRole.Editor)) + PublisherMemberRole.Editor)) return StatusCode(403, "You need at least be an editor to delete the publisher's post."); await ps.DeletePostAsync(post); diff --git a/DysonNetwork.Sphere/Startup/BroadcastEventHandler.cs b/DysonNetwork.Sphere/Startup/BroadcastEventHandler.cs index 0ba26dc..8952984 100644 --- a/DysonNetwork.Sphere/Startup/BroadcastEventHandler.cs +++ b/DysonNetwork.Sphere/Startup/BroadcastEventHandler.cs @@ -13,7 +13,7 @@ public class BroadcastEventHandler( { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - await foreach (var msg in nats.SubscribeAsync("accounts.deleted", cancellationToken: stoppingToken)) + await foreach (var msg in nats.SubscribeAsync(AccountDeletedEvent.Type, cancellationToken: stoppingToken)) { try {