diff --git a/DysonNetwork.Control/AppHost.cs b/DysonNetwork.Control/AppHost.cs index 4487c48..69f4168 100644 --- a/DysonNetwork.Control/AppHost.cs +++ b/DysonNetwork.Control/AppHost.cs @@ -4,9 +4,6 @@ var builder = DistributedApplication.CreateBuilder(args); var isDev = builder.Environment.IsDevelopment(); -// Database was configured separately in each service. -// var database = builder.AddPostgres("database"); - var cache = builder.AddRedis("cache"); var queue = builder.AddNats("queue").WithJetStream(); diff --git a/DysonNetwork.Develop/appsettings.json b/DysonNetwork.Develop/appsettings.json index 6863c3d..8ecf09a 100644 --- a/DysonNetwork.Develop/appsettings.json +++ b/DysonNetwork.Develop/appsettings.json @@ -10,12 +10,9 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "App": "Host=localhost;Port=5432;Database=dyson_network_dev;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" + "App": "Host=localhost;Port=5432;Database=dyson_develop;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" }, - "KnownProxies": [ - "127.0.0.1", - "::1" - ], + "KnownProxies": ["127.0.0.1", "::1"], "Swagger": { "PublicBasePath": "/develop" }, diff --git a/DysonNetwork.Pass/AppDatabase.cs b/DysonNetwork.Pass/AppDatabase.cs index 5ce4e2c..9e563f9 100644 --- a/DysonNetwork.Pass/AppDatabase.cs +++ b/DysonNetwork.Pass/AppDatabase.cs @@ -44,6 +44,7 @@ public class AppDatabase( public DbSet PaymentOrders { get; set; } = null!; public DbSet PaymentTransactions { get; set; } = null!; public DbSet WalletSubscriptions { get; set; } = null!; + public DbSet WalletGifts { get; set; } = null!; public DbSet WalletCoupons { get; set; } = null!; public DbSet Punishments { get; set; } = null!; @@ -278,4 +279,4 @@ public static class OptionalQueryExtensions { return condition ? transform(source) : source; } -} \ No newline at end of file +} diff --git a/DysonNetwork.Pass/DysonNetwork.Pass.sln b/DysonNetwork.Pass/DysonNetwork.Pass.sln new file mode 100644 index 0000000..11c7ff8 --- /dev/null +++ b/DysonNetwork.Pass/DysonNetwork.Pass.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Pass", "DysonNetwork.Pass.csproj", "{0E8F6522-90DE-5BDE-7127-114E02C2C10F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0E8F6522-90DE-5BDE-7127-114E02C2C10F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E8F6522-90DE-5BDE-7127-114E02C2C10F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E8F6522-90DE-5BDE-7127-114E02C2C10F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E8F6522-90DE-5BDE-7127-114E02C2C10F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8DAB9031-CC04-4A1A-A05A-4ADFEBAB90A8} + EndGlobalSection +EndGlobal diff --git a/DysonNetwork.Pass/Migrations/20251003061315_AddSubscriptionGift.Designer.cs b/DysonNetwork.Pass/Migrations/20251003061315_AddSubscriptionGift.Designer.cs new file mode 100644 index 0000000..649e6b1 --- /dev/null +++ b/DysonNetwork.Pass/Migrations/20251003061315_AddSubscriptionGift.Designer.cs @@ -0,0 +1,2207 @@ +// +using System; +using System.Collections.Generic; +using System.Text.Json; +using DysonNetwork.Pass; +using DysonNetwork.Shared.GeoIp; +using DysonNetwork.Shared.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace DysonNetwork.Pass.Migrations +{ + [DbContext(typeof(AppDatabase))] + [Migration("20251003061315_AddSubscriptionGift")] + partial class AddSubscriptionGift + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAbuseReport", 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.Shared.Models.SnAccount", 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("Region") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("region"); + + 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.Shared.Models.SnAccountAuthFactor", 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.Shared.Models.SnAccountBadge", 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.Shared.Models.SnAccountConnection", 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.Shared.Models.SnAccountContact", 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.Shared.Models.SnAccountProfile", 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.Shared.Models.SnAccountPunishment", 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.Shared.Models.SnAccountRelationship", 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.Shared.Models.SnAccountStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("AppIdentifier") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("app_identifier"); + + 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("IsAutomated") + .HasColumnType("boolean") + .HasColumnName("is_automated"); + + 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>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + 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.Shared.Models.SnActionLog", 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("jsonb") + .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.Shared.Models.SnApiKey", 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.Shared.Models.SnAuthChallenge", 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("jsonb") + .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.Shared.Models.SnAuthClient", 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.Shared.Models.SnAuthSession", 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.Shared.Models.SnCheckInResult", 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.Shared.Models.SnExperienceRecord", 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.Shared.Models.SnMagicSpell", 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.Shared.Models.SnPermissionGroup", 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.Shared.Models.SnPermissionGroupMember", 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.Shared.Models.SnPermissionNode", 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.Shared.Models.SnSocialCreditRecord", 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.Shared.Models.SnWallet", 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.Shared.Models.SnWalletCoupon", 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.Shared.Models.SnWalletGift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("BasePrice") + .HasColumnType("numeric") + .HasColumnName("base_price"); + + 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("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("FinalPrice") + .HasColumnType("numeric") + .HasColumnName("final_price"); + + b.Property("GiftCode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("gift_code"); + + b.Property("GifterId") + .HasColumnType("uuid") + .HasColumnName("gifter_id"); + + b.Property("IsOpenGift") + .HasColumnType("boolean") + .HasColumnName("is_open_gift"); + + b.Property("Message") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("message"); + + b.Property("PaymentDetails") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payment_details"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("payment_method"); + + b.Property("RecipientId") + .HasColumnType("uuid") + .HasColumnName("recipient_id"); + + b.Property("RedeemedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("redeemed_at"); + + b.Property("RedeemerId") + .HasColumnType("uuid") + .HasColumnName("redeemer_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("SubscriptionIdentifier") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("subscription_identifier"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_gifts"); + + b.HasIndex("CouponId") + .HasDatabaseName("ix_wallet_gifts_coupon_id"); + + b.HasIndex("GiftCode") + .HasDatabaseName("ix_wallet_gifts_gift_code"); + + b.HasIndex("GifterId") + .HasDatabaseName("ix_wallet_gifts_gifter_id"); + + b.HasIndex("RecipientId") + .HasDatabaseName("ix_wallet_gifts_recipient_id"); + + b.HasIndex("RedeemerId") + .HasDatabaseName("ix_wallet_gifts_redeemer_id"); + + b.ToTable("wallet_gifts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletOrder", 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.Shared.Models.SnWalletPocket", 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.Shared.Models.SnWalletSubscription", 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("GiftId") + .HasColumnType("uuid") + .HasColumnName("gift_id"); + + 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("GiftId") + .IsUnique() + .HasDatabaseName("ix_wallet_subscriptions_gift_id"); + + b.HasIndex("Identifier") + .HasDatabaseName("ix_wallet_subscriptions_identifier"); + + b.HasIndex("Status") + .HasDatabaseName("ix_wallet_subscriptions_status"); + + b.HasIndex("AccountId", "Identifier") + .HasDatabaseName("ix_wallet_subscriptions_account_id_identifier"); + + b.HasIndex("AccountId", "IsActive") + .HasDatabaseName("ix_wallet_subscriptions_account_id_is_active"); + + b.ToTable("wallet_subscriptions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletTransaction", 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.Shared.Models.SnAbuseReport", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_abuse_reports_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountAuthFactor", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("AuthFactors") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_auth_factors_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountBadge", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("Badges") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_badges_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountConnection", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("Connections") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_connections_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountContact", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("Contacts") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_contacts_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountProfile", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithOne("Profile") + .HasForeignKey("DysonNetwork.Shared.Models.SnAccountProfile", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_profiles_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountPunishment", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_punishments_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountRelationship", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("OutgoingRelationships") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_relationships_accounts_account_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "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.Shared.Models.SnAccountStatus", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_statuses_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnActionLog", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_action_logs_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnApiKey", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_api_keys_accounts_account_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAuthSession", "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.Shared.Models.SnAuthChallenge", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("Challenges") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_challenges_accounts_account_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAuthClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .HasConstraintName("fk_auth_challenges_auth_clients_client_id"); + + b.Navigation("Account"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthClient", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_clients_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthSession", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("Sessions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_sessions_accounts_account_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAuthChallenge", "Challenge") + .WithMany() + .HasForeignKey("ChallengeId") + .HasConstraintName("fk_auth_sessions_auth_challenges_challenge_id"); + + b.Navigation("Account"); + + b.Navigation("Challenge"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_check_in_results_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnExperienceRecord", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_experience_records_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .HasConstraintName("fk_magic_spells_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionGroupMember", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnPermissionGroup", "Group") + .WithMany("Members") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_permission_group_members_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionNode", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnPermissionGroup", "Group") + .WithMany("Nodes") + .HasForeignKey("GroupId") + .HasConstraintName("fk_permission_nodes_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_social_credit_records_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallets_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletGift", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWalletCoupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .HasConstraintName("fk_wallet_gifts_wallet_coupons_coupon_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Gifter") + .WithMany() + .HasForeignKey("GifterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_gifts_accounts_gifter_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Recipient") + .WithMany() + .HasForeignKey("RecipientId") + .HasConstraintName("fk_wallet_gifts_accounts_recipient_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Redeemer") + .WithMany() + .HasForeignKey("RedeemerId") + .HasConstraintName("fk_wallet_gifts_accounts_redeemer_id"); + + b.Navigation("Coupon"); + + b.Navigation("Gifter"); + + b.Navigation("Recipient"); + + b.Navigation("Redeemer"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletOrder", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .HasConstraintName("fk_payment_orders_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWalletTransaction", "Transaction") + .WithMany() + .HasForeignKey("TransactionId") + .HasConstraintName("fk_payment_orders_payment_transactions_transaction_id"); + + b.Navigation("PayeeWallet"); + + b.Navigation("Transaction"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletPocket", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "Wallet") + .WithMany("Pockets") + .HasForeignKey("WalletId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_pockets_wallets_wallet_id"); + + b.Navigation("Wallet"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletSubscription", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_subscriptions_accounts_account_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWalletCoupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .HasConstraintName("fk_wallet_subscriptions_wallet_coupons_coupon_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWalletGift", "Gift") + .WithOne("Subscription") + .HasForeignKey("DysonNetwork.Shared.Models.SnWalletSubscription", "GiftId") + .HasConstraintName("fk_wallet_subscriptions_wallet_gifts_gift_id"); + + b.Navigation("Account"); + + b.Navigation("Coupon"); + + b.Navigation("Gift"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletTransaction", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "PayerWallet") + .WithMany() + .HasForeignKey("PayerWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payer_wallet_id"); + + b.Navigation("PayeeWallet"); + + b.Navigation("PayerWallet"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccount", 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.Shared.Models.SnPermissionGroup", b => + { + b.Navigation("Members"); + + b.Navigation("Nodes"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b => + { + b.Navigation("Pockets"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletGift", b => + { + b.Navigation("Subscription"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DysonNetwork.Pass/Migrations/20251003061315_AddSubscriptionGift.cs b/DysonNetwork.Pass/Migrations/20251003061315_AddSubscriptionGift.cs new file mode 100644 index 0000000..1c16c40 --- /dev/null +++ b/DysonNetwork.Pass/Migrations/20251003061315_AddSubscriptionGift.cs @@ -0,0 +1,1142 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using DysonNetwork.Shared.GeoIp; +using DysonNetwork.Shared.Models; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace DysonNetwork.Pass.Migrations +{ + /// + public partial class AddSubscriptionGift : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "accounts", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + nick = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + language = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + region = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + activated_at = table.Column(type: "timestamp with time zone", nullable: true), + is_superuser = table.Column(type: "boolean", nullable: false), + automated_id = table.Column(type: "uuid", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_accounts", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "permission_groups", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + key = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_permission_groups", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "wallet_coupons", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + identifier = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), + code = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + affected_at = table.Column(type: "timestamp with time zone", nullable: true), + expired_at = table.Column(type: "timestamp with time zone", nullable: true), + discount_amount = table.Column(type: "numeric", nullable: true), + discount_rate = table.Column(type: "double precision", nullable: true), + max_usage = table.Column(type: "integer", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_wallet_coupons", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "abuse_reports", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + resource_identifier = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: false), + type = table.Column(type: "integer", nullable: false), + reason = table.Column(type: "character varying(8192)", maxLength: 8192, nullable: false), + resolved_at = table.Column(type: "timestamp with time zone", nullable: true), + resolution = table.Column(type: "character varying(8192)", maxLength: 8192, nullable: true), + account_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_abuse_reports", x => x.id); + table.ForeignKey( + name: "fk_abuse_reports_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "account_auth_factors", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + type = table.Column(type: "integer", nullable: false), + secret = table.Column(type: "character varying(8196)", maxLength: 8196, nullable: true), + config = table.Column>(type: "jsonb", nullable: true), + trustworthy = table.Column(type: "integer", nullable: false), + enabled_at = table.Column(type: "timestamp with time zone", nullable: true), + expired_at = table.Column(type: "timestamp with time zone", nullable: true), + account_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_account_auth_factors", x => x.id); + table.ForeignKey( + name: "fk_account_auth_factors_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "account_check_in_results", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + level = table.Column(type: "integer", nullable: false), + reward_points = table.Column(type: "numeric", nullable: true), + reward_experience = table.Column(type: "integer", nullable: true), + tips = table.Column>(type: "jsonb", nullable: false), + account_id = table.Column(type: "uuid", nullable: false), + backdated_from = table.Column(type: "timestamp with time zone", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_account_check_in_results", x => x.id); + table.ForeignKey( + name: "fk_account_check_in_results_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "account_connections", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + provider = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: false), + provided_identifier = table.Column(type: "character varying(8192)", maxLength: 8192, nullable: false), + meta = table.Column>(type: "jsonb", nullable: true), + access_token = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), + refresh_token = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), + last_used_at = table.Column(type: "timestamp with time zone", nullable: true), + account_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_account_connections", x => x.id); + table.ForeignKey( + name: "fk_account_connections_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "account_contacts", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + type = table.Column(type: "integer", nullable: false), + verified_at = table.Column(type: "timestamp with time zone", nullable: true), + is_primary = table.Column(type: "boolean", nullable: false), + is_public = table.Column(type: "boolean", nullable: false), + content = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + account_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_account_contacts", x => x.id); + table.ForeignKey( + name: "fk_account_contacts_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "account_profiles", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + first_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + middle_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + last_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + bio = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), + gender = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + pronouns = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + time_zone = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + location = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + links = table.Column>(type: "jsonb", nullable: true), + birthday = table.Column(type: "timestamp with time zone", nullable: true), + last_seen_at = table.Column(type: "timestamp with time zone", nullable: true), + verification = table.Column(type: "jsonb", nullable: true), + active_badge = table.Column(type: "jsonb", nullable: true), + experience = table.Column(type: "integer", nullable: false), + social_credits = table.Column(type: "double precision", nullable: false), + picture = table.Column(type: "jsonb", nullable: true), + background = table.Column(type: "jsonb", nullable: true), + account_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_account_profiles", x => x.id); + table.ForeignKey( + name: "fk_account_profiles_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "account_relationships", + columns: table => new + { + account_id = table.Column(type: "uuid", nullable: false), + related_id = table.Column(type: "uuid", nullable: false), + expired_at = table.Column(type: "timestamp with time zone", nullable: true), + status = table.Column(type: "smallint", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_account_relationships", x => new { x.account_id, x.related_id }); + table.ForeignKey( + name: "fk_account_relationships_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_account_relationships_accounts_related_id", + column: x => x.related_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "account_statuses", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + attitude = table.Column(type: "integer", nullable: false), + is_invisible = table.Column(type: "boolean", nullable: false), + is_not_disturb = table.Column(type: "boolean", nullable: false), + label = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + meta = table.Column>(type: "jsonb", nullable: true), + cleared_at = table.Column(type: "timestamp with time zone", nullable: true), + app_identifier = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), + is_automated = table.Column(type: "boolean", nullable: false), + account_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_account_statuses", x => x.id); + table.ForeignKey( + name: "fk_account_statuses_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "action_logs", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + action = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: false), + meta = table.Column>(type: "jsonb", nullable: false), + user_agent = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + ip_address = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + location = table.Column(type: "jsonb", nullable: true), + account_id = table.Column(type: "uuid", nullable: false), + session_id = table.Column(type: "uuid", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_action_logs", x => x.id); + table.ForeignKey( + name: "fk_action_logs_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "auth_clients", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + platform = table.Column(type: "integer", nullable: false), + device_name = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + device_label = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + device_id = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + account_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_auth_clients", x => x.id); + table.ForeignKey( + name: "fk_auth_clients_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "badges", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + type = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + label = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + caption = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), + meta = table.Column>(type: "jsonb", nullable: false), + activated_at = table.Column(type: "timestamp with time zone", nullable: true), + expired_at = table.Column(type: "timestamp with time zone", nullable: true), + account_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_badges", x => x.id); + table.ForeignKey( + name: "fk_badges_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "experience_records", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + reason_type = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + reason = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + delta = table.Column(type: "bigint", nullable: false), + bonus_multiplier = table.Column(type: "double precision", nullable: false), + account_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_experience_records", x => x.id); + table.ForeignKey( + name: "fk_experience_records_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "magic_spells", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + spell = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + type = table.Column(type: "integer", nullable: false), + expires_at = table.Column(type: "timestamp with time zone", nullable: true), + affected_at = table.Column(type: "timestamp with time zone", nullable: true), + meta = table.Column>(type: "jsonb", nullable: false), + account_id = table.Column(type: "uuid", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_magic_spells", x => x.id); + table.ForeignKey( + name: "fk_magic_spells_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "punishments", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + reason = table.Column(type: "character varying(8192)", maxLength: 8192, nullable: false), + expired_at = table.Column(type: "timestamp with time zone", nullable: true), + type = table.Column(type: "integer", nullable: false), + blocked_permissions = table.Column>(type: "jsonb", nullable: true), + account_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_punishments", x => x.id); + table.ForeignKey( + name: "fk_punishments_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "social_credit_records", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + reason_type = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + reason = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + delta = table.Column(type: "double precision", nullable: false), + expired_at = table.Column(type: "timestamp with time zone", nullable: true), + account_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_social_credit_records", x => x.id); + table.ForeignKey( + name: "fk_social_credit_records_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "wallets", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + account_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_wallets", x => x.id); + table.ForeignKey( + name: "fk_wallets_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "permission_group_members", + columns: table => new + { + group_id = table.Column(type: "uuid", nullable: false), + actor = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + expired_at = table.Column(type: "timestamp with time zone", nullable: true), + affected_at = table.Column(type: "timestamp with time zone", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_permission_group_members", x => new { x.group_id, x.actor }); + table.ForeignKey( + name: "fk_permission_group_members_permission_groups_group_id", + column: x => x.group_id, + principalTable: "permission_groups", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "permission_nodes", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + actor = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + area = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + key = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + value = table.Column(type: "jsonb", nullable: false), + expired_at = table.Column(type: "timestamp with time zone", nullable: true), + affected_at = table.Column(type: "timestamp with time zone", nullable: true), + group_id = table.Column(type: "uuid", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_permission_nodes", x => x.id); + table.ForeignKey( + name: "fk_permission_nodes_permission_groups_group_id", + column: x => x.group_id, + principalTable: "permission_groups", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "wallet_gifts", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + gifter_id = table.Column(type: "uuid", nullable: false), + recipient_id = table.Column(type: "uuid", nullable: true), + gift_code = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + message = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + subscription_identifier = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: false), + base_price = table.Column(type: "numeric", nullable: false), + final_price = table.Column(type: "numeric", nullable: false), + status = table.Column(type: "integer", nullable: false), + redeemed_at = table.Column(type: "timestamp with time zone", nullable: true), + redeemer_id = table.Column(type: "uuid", nullable: true), + expires_at = table.Column(type: "timestamp with time zone", nullable: false), + is_open_gift = table.Column(type: "boolean", nullable: false), + payment_method = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: false), + payment_details = table.Column(type: "jsonb", nullable: false), + coupon_id = table.Column(type: "uuid", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_wallet_gifts", x => x.id); + table.ForeignKey( + name: "fk_wallet_gifts_accounts_gifter_id", + column: x => x.gifter_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_wallet_gifts_accounts_recipient_id", + column: x => x.recipient_id, + principalTable: "accounts", + principalColumn: "id"); + table.ForeignKey( + name: "fk_wallet_gifts_accounts_redeemer_id", + column: x => x.redeemer_id, + principalTable: "accounts", + principalColumn: "id"); + table.ForeignKey( + name: "fk_wallet_gifts_wallet_coupons_coupon_id", + column: x => x.coupon_id, + principalTable: "wallet_coupons", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "auth_challenges", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + expired_at = table.Column(type: "timestamp with time zone", nullable: true), + step_remain = table.Column(type: "integer", nullable: false), + step_total = table.Column(type: "integer", nullable: false), + failed_attempts = table.Column(type: "integer", nullable: false), + type = table.Column(type: "integer", nullable: false), + blacklist_factors = table.Column>(type: "jsonb", nullable: false), + audiences = table.Column>(type: "jsonb", nullable: false), + scopes = table.Column>(type: "jsonb", nullable: false), + ip_address = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + user_agent = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + nonce = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + location = table.Column(type: "jsonb", nullable: true), + account_id = table.Column(type: "uuid", nullable: false), + client_id = table.Column(type: "uuid", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_auth_challenges", x => x.id); + table.ForeignKey( + name: "fk_auth_challenges_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_auth_challenges_auth_clients_client_id", + column: x => x.client_id, + principalTable: "auth_clients", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "payment_transactions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + currency = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + amount = table.Column(type: "numeric", nullable: false), + remarks = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), + type = table.Column(type: "integer", nullable: false), + payer_wallet_id = table.Column(type: "uuid", nullable: true), + payee_wallet_id = table.Column(type: "uuid", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_payment_transactions", x => x.id); + table.ForeignKey( + name: "fk_payment_transactions_wallets_payee_wallet_id", + column: x => x.payee_wallet_id, + principalTable: "wallets", + principalColumn: "id"); + table.ForeignKey( + name: "fk_payment_transactions_wallets_payer_wallet_id", + column: x => x.payer_wallet_id, + principalTable: "wallets", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "wallet_pockets", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + currency = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + amount = table.Column(type: "numeric", nullable: false), + wallet_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_wallet_pockets", x => x.id); + table.ForeignKey( + name: "fk_wallet_pockets_wallets_wallet_id", + column: x => x.wallet_id, + principalTable: "wallets", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "wallet_subscriptions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + begun_at = table.Column(type: "timestamp with time zone", nullable: false), + ended_at = table.Column(type: "timestamp with time zone", nullable: true), + identifier = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: false), + is_active = table.Column(type: "boolean", nullable: false), + is_free_trial = table.Column(type: "boolean", nullable: false), + status = table.Column(type: "integer", nullable: false), + payment_method = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: false), + payment_details = table.Column(type: "jsonb", nullable: false), + base_price = table.Column(type: "numeric", nullable: false), + coupon_id = table.Column(type: "uuid", nullable: true), + renewal_at = table.Column(type: "timestamp with time zone", nullable: true), + account_id = table.Column(type: "uuid", nullable: false), + gift_id = table.Column(type: "uuid", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_wallet_subscriptions", x => x.id); + table.ForeignKey( + name: "fk_wallet_subscriptions_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_wallet_subscriptions_wallet_coupons_coupon_id", + column: x => x.coupon_id, + principalTable: "wallet_coupons", + principalColumn: "id"); + table.ForeignKey( + name: "fk_wallet_subscriptions_wallet_gifts_gift_id", + column: x => x.gift_id, + principalTable: "wallet_gifts", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "auth_sessions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + last_granted_at = table.Column(type: "timestamp with time zone", nullable: true), + expired_at = table.Column(type: "timestamp with time zone", nullable: true), + account_id = table.Column(type: "uuid", nullable: false), + challenge_id = table.Column(type: "uuid", nullable: true), + app_id = table.Column(type: "uuid", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_auth_sessions", x => x.id); + table.ForeignKey( + name: "fk_auth_sessions_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_auth_sessions_auth_challenges_challenge_id", + column: x => x.challenge_id, + principalTable: "auth_challenges", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "payment_orders", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + status = table.Column(type: "integer", nullable: false), + currency = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + remarks = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), + app_identifier = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), + product_identifier = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), + meta = table.Column>(type: "jsonb", nullable: true), + amount = table.Column(type: "numeric", nullable: false), + expired_at = table.Column(type: "timestamp with time zone", nullable: false), + payee_wallet_id = table.Column(type: "uuid", nullable: true), + transaction_id = table.Column(type: "uuid", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_payment_orders", x => x.id); + table.ForeignKey( + name: "fk_payment_orders_payment_transactions_transaction_id", + column: x => x.transaction_id, + principalTable: "payment_transactions", + principalColumn: "id"); + table.ForeignKey( + name: "fk_payment_orders_wallets_payee_wallet_id", + column: x => x.payee_wallet_id, + principalTable: "wallets", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "api_keys", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + label = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + account_id = table.Column(type: "uuid", nullable: false), + session_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_api_keys", x => x.id); + table.ForeignKey( + name: "fk_api_keys_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_api_keys_auth_sessions_session_id", + column: x => x.session_id, + principalTable: "auth_sessions", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_abuse_reports_account_id", + table: "abuse_reports", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_account_auth_factors_account_id", + table: "account_auth_factors", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_account_check_in_results_account_id", + table: "account_check_in_results", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_account_connections_account_id", + table: "account_connections", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_account_contacts_account_id", + table: "account_contacts", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_account_profiles_account_id", + table: "account_profiles", + column: "account_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_account_relationships_related_id", + table: "account_relationships", + column: "related_id"); + + migrationBuilder.CreateIndex( + name: "ix_account_statuses_account_id", + table: "account_statuses", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_accounts_name", + table: "accounts", + column: "name", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_action_logs_account_id", + table: "action_logs", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_api_keys_account_id", + table: "api_keys", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_api_keys_session_id", + table: "api_keys", + column: "session_id"); + + migrationBuilder.CreateIndex( + name: "ix_auth_challenges_account_id", + table: "auth_challenges", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_auth_challenges_client_id", + table: "auth_challenges", + column: "client_id"); + + migrationBuilder.CreateIndex( + name: "ix_auth_clients_account_id", + table: "auth_clients", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_auth_sessions_account_id", + table: "auth_sessions", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_auth_sessions_challenge_id", + table: "auth_sessions", + column: "challenge_id"); + + migrationBuilder.CreateIndex( + name: "ix_badges_account_id", + table: "badges", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_experience_records_account_id", + table: "experience_records", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_magic_spells_account_id", + table: "magic_spells", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_magic_spells_spell", + table: "magic_spells", + column: "spell", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_payment_orders_payee_wallet_id", + table: "payment_orders", + column: "payee_wallet_id"); + + migrationBuilder.CreateIndex( + name: "ix_payment_orders_transaction_id", + table: "payment_orders", + column: "transaction_id"); + + migrationBuilder.CreateIndex( + name: "ix_payment_transactions_payee_wallet_id", + table: "payment_transactions", + column: "payee_wallet_id"); + + migrationBuilder.CreateIndex( + name: "ix_payment_transactions_payer_wallet_id", + table: "payment_transactions", + column: "payer_wallet_id"); + + migrationBuilder.CreateIndex( + name: "ix_permission_nodes_group_id", + table: "permission_nodes", + column: "group_id"); + + migrationBuilder.CreateIndex( + name: "ix_permission_nodes_key_area_actor", + table: "permission_nodes", + columns: new[] { "key", "area", "actor" }); + + migrationBuilder.CreateIndex( + name: "ix_punishments_account_id", + table: "punishments", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_social_credit_records_account_id", + table: "social_credit_records", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_wallet_gifts_coupon_id", + table: "wallet_gifts", + column: "coupon_id"); + + migrationBuilder.CreateIndex( + name: "ix_wallet_gifts_gift_code", + table: "wallet_gifts", + column: "gift_code"); + + migrationBuilder.CreateIndex( + name: "ix_wallet_gifts_gifter_id", + table: "wallet_gifts", + column: "gifter_id"); + + migrationBuilder.CreateIndex( + name: "ix_wallet_gifts_recipient_id", + table: "wallet_gifts", + column: "recipient_id"); + + migrationBuilder.CreateIndex( + name: "ix_wallet_gifts_redeemer_id", + table: "wallet_gifts", + column: "redeemer_id"); + + migrationBuilder.CreateIndex( + name: "ix_wallet_pockets_wallet_id", + table: "wallet_pockets", + column: "wallet_id"); + + migrationBuilder.CreateIndex( + name: "ix_wallet_subscriptions_account_id", + table: "wallet_subscriptions", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_wallet_subscriptions_account_id_identifier", + table: "wallet_subscriptions", + columns: new[] { "account_id", "identifier" }); + + migrationBuilder.CreateIndex( + name: "ix_wallet_subscriptions_account_id_is_active", + table: "wallet_subscriptions", + columns: new[] { "account_id", "is_active" }); + + migrationBuilder.CreateIndex( + name: "ix_wallet_subscriptions_coupon_id", + table: "wallet_subscriptions", + column: "coupon_id"); + + migrationBuilder.CreateIndex( + name: "ix_wallet_subscriptions_gift_id", + table: "wallet_subscriptions", + column: "gift_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_wallet_subscriptions_identifier", + table: "wallet_subscriptions", + column: "identifier"); + + migrationBuilder.CreateIndex( + name: "ix_wallet_subscriptions_status", + table: "wallet_subscriptions", + column: "status"); + + migrationBuilder.CreateIndex( + name: "ix_wallets_account_id", + table: "wallets", + column: "account_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "abuse_reports"); + + migrationBuilder.DropTable( + name: "account_auth_factors"); + + migrationBuilder.DropTable( + name: "account_check_in_results"); + + migrationBuilder.DropTable( + name: "account_connections"); + + migrationBuilder.DropTable( + name: "account_contacts"); + + migrationBuilder.DropTable( + name: "account_profiles"); + + migrationBuilder.DropTable( + name: "account_relationships"); + + migrationBuilder.DropTable( + name: "account_statuses"); + + migrationBuilder.DropTable( + name: "action_logs"); + + migrationBuilder.DropTable( + name: "api_keys"); + + migrationBuilder.DropTable( + name: "badges"); + + migrationBuilder.DropTable( + name: "experience_records"); + + migrationBuilder.DropTable( + name: "magic_spells"); + + migrationBuilder.DropTable( + name: "payment_orders"); + + migrationBuilder.DropTable( + name: "permission_group_members"); + + migrationBuilder.DropTable( + name: "permission_nodes"); + + migrationBuilder.DropTable( + name: "punishments"); + + migrationBuilder.DropTable( + name: "social_credit_records"); + + migrationBuilder.DropTable( + name: "wallet_pockets"); + + migrationBuilder.DropTable( + name: "wallet_subscriptions"); + + migrationBuilder.DropTable( + name: "auth_sessions"); + + migrationBuilder.DropTable( + name: "payment_transactions"); + + migrationBuilder.DropTable( + name: "permission_groups"); + + migrationBuilder.DropTable( + name: "wallet_gifts"); + + migrationBuilder.DropTable( + name: "auth_challenges"); + + migrationBuilder.DropTable( + name: "wallets"); + + migrationBuilder.DropTable( + name: "wallet_coupons"); + + migrationBuilder.DropTable( + name: "auth_clients"); + + migrationBuilder.DropTable( + name: "accounts"); + } + } +} diff --git a/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs b/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs index 546db4b..c82dca9 100644 --- a/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs +++ b/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs @@ -3,10 +3,8 @@ using System; using System.Collections.Generic; using System.Text.Json; using DysonNetwork.Pass; -using DysonNetwork.Pass.Account; using DysonNetwork.Shared.GeoIp; using DysonNetwork.Shared.Models; -using DysonNetwork.Shared.Proto; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -29,7 +27,7 @@ namespace DysonNetwork.Pass.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("DysonNetwork.Pass.Account.AbuseReport", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAbuseReport", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -86,7 +84,7 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("abuse_reports", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.Account", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccount", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -151,7 +149,7 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("accounts", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.AccountAuthFactor", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountAuthFactor", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -208,7 +206,7 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("account_auth_factors", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.AccountBadge", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountBadge", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -269,7 +267,7 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("badges", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.AccountConnection", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountConnection", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -331,7 +329,7 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("account_connections", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.AccountContact", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountContact", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -385,7 +383,7 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("account_contacts", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.AccountProfile", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountProfile", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -396,7 +394,7 @@ namespace DysonNetwork.Pass.Migrations .HasColumnType("uuid") .HasColumnName("account_id"); - b.Property("ActiveBadge") + b.Property("ActiveBadge") .HasColumnType("jsonb") .HasColumnName("active_badge"); @@ -494,180 +492,7 @@ namespace DysonNetwork.Pass.Migrations 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("jsonb") - .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 => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountPunishment", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -717,7 +542,7 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("punishments", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.Relationship", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountRelationship", b => { b.Property("AccountId") .HasColumnType("uuid") @@ -756,7 +581,7 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("account_relationships", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.Status", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountStatus", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -822,7 +647,68 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("account_statuses", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Auth.ApiKey", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnActionLog", 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("jsonb") + .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.Shared.Models.SnApiKey", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -867,7 +753,7 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("api_keys", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthChallenge", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthChallenge", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -960,7 +846,7 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("auth_challenges", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthClient", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthClient", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1013,7 +899,7 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("auth_clients", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthSession", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthSession", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1064,7 +950,7 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("auth_sessions", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Credit.SocialCreditRecord", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1075,6 +961,10 @@ namespace DysonNetwork.Pass.Migrations .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"); @@ -1083,40 +973,37 @@ namespace DysonNetwork.Pass.Migrations .HasColumnType("timestamp with time zone") .HasColumnName("deleted_at"); - b.Property("Delta") - .HasColumnType("double precision") - .HasColumnName("delta"); + b.Property("Level") + .HasColumnType("integer") + .HasColumnName("level"); - b.Property("ExpiredAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expired_at"); + b.Property("RewardExperience") + .HasColumnType("integer") + .HasColumnName("reward_experience"); - b.Property("Reason") + b.Property("RewardPoints") + .HasColumnType("numeric") + .HasColumnName("reward_points"); + + b.Property>("Tips") .IsRequired() - .HasMaxLength(1024) - .HasColumnType("character varying(1024)") - .HasColumnName("reason"); - - b.Property("ReasonType") - .IsRequired() - .HasMaxLength(1024) - .HasColumnType("character varying(1024)") - .HasColumnName("reason_type"); + .HasColumnType("jsonb") + .HasColumnName("tips"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasColumnName("updated_at"); b.HasKey("Id") - .HasName("pk_social_credit_records"); + .HasName("pk_account_check_in_results"); b.HasIndex("AccountId") - .HasDatabaseName("ix_social_credit_records_account_id"); + .HasDatabaseName("ix_account_check_in_results_account_id"); - b.ToTable("social_credit_records", (string)null); + b.ToTable("account_check_in_results", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Leveling.ExperienceRecord", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnExperienceRecord", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1168,7 +1055,66 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("experience_records", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionGroup", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", 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.Shared.Models.SnPermissionGroup", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1199,7 +1145,7 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("permission_groups", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionGroupMember", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionGroupMember", b => { b.Property("GroupId") .HasColumnType("uuid") @@ -1236,7 +1182,7 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("permission_group_members", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionNode", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionNode", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1302,7 +1248,91 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("permission_nodes", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Wallet.Coupon", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", 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.Shared.Models.SnWallet", 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.Shared.Models.SnWalletCoupon", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1357,7 +1387,115 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("wallet_coupons", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Wallet.Order", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletGift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("BasePrice") + .HasColumnType("numeric") + .HasColumnName("base_price"); + + 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("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("FinalPrice") + .HasColumnType("numeric") + .HasColumnName("final_price"); + + b.Property("GiftCode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("gift_code"); + + b.Property("GifterId") + .HasColumnType("uuid") + .HasColumnName("gifter_id"); + + b.Property("IsOpenGift") + .HasColumnType("boolean") + .HasColumnName("is_open_gift"); + + b.Property("Message") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("message"); + + b.Property("PaymentDetails") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payment_details"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("payment_method"); + + b.Property("RecipientId") + .HasColumnType("uuid") + .HasColumnName("recipient_id"); + + b.Property("RedeemedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("redeemed_at"); + + b.Property("RedeemerId") + .HasColumnType("uuid") + .HasColumnName("redeemer_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("SubscriptionIdentifier") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("subscription_identifier"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_gifts"); + + b.HasIndex("CouponId") + .HasDatabaseName("ix_wallet_gifts_coupon_id"); + + b.HasIndex("GiftCode") + .HasDatabaseName("ix_wallet_gifts_gift_code"); + + b.HasIndex("GifterId") + .HasDatabaseName("ix_wallet_gifts_gifter_id"); + + b.HasIndex("RecipientId") + .HasDatabaseName("ix_wallet_gifts_recipient_id"); + + b.HasIndex("RedeemerId") + .HasDatabaseName("ix_wallet_gifts_redeemer_id"); + + b.ToTable("wallet_gifts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletOrder", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1433,7 +1571,49 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("payment_orders", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Wallet.Subscription", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletPocket", 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.Shared.Models.SnWalletSubscription", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1468,6 +1648,10 @@ namespace DysonNetwork.Pass.Migrations .HasColumnType("timestamp with time zone") .HasColumnName("ended_at"); + b.Property("GiftId") + .HasColumnType("uuid") + .HasColumnName("gift_id"); + b.Property("Identifier") .IsRequired() .HasMaxLength(4096) @@ -1514,13 +1698,26 @@ namespace DysonNetwork.Pass.Migrations b.HasIndex("CouponId") .HasDatabaseName("ix_wallet_subscriptions_coupon_id"); + b.HasIndex("GiftId") + .IsUnique() + .HasDatabaseName("ix_wallet_subscriptions_gift_id"); + b.HasIndex("Identifier") .HasDatabaseName("ix_wallet_subscriptions_identifier"); + b.HasIndex("Status") + .HasDatabaseName("ix_wallet_subscriptions_status"); + + b.HasIndex("AccountId", "Identifier") + .HasDatabaseName("ix_wallet_subscriptions_account_id_identifier"); + + b.HasIndex("AccountId", "IsActive") + .HasDatabaseName("ix_wallet_subscriptions_account_id_is_active"); + b.ToTable("wallet_subscriptions", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Wallet.Transaction", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletTransaction", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1578,83 +1775,9 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("payment_transactions", (string)null); }); - modelBuilder.Entity("DysonNetwork.Pass.Wallet.Wallet", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAbuseReport", 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") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithMany() .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) @@ -1664,9 +1787,9 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Account"); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.AccountAuthFactor", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountAuthFactor", b => { - b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithMany("AuthFactors") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) @@ -1676,9 +1799,9 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Account"); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.AccountBadge", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountBadge", b => { - b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithMany("Badges") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) @@ -1688,9 +1811,9 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Account"); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.AccountConnection", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountConnection", b => { - b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithMany("Connections") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) @@ -1700,9 +1823,9 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Account"); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.AccountContact", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountContact", b => { - b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithMany("Contacts") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) @@ -1712,11 +1835,11 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Account"); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.AccountProfile", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountProfile", b => { - b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithOne("Profile") - .HasForeignKey("DysonNetwork.Pass.Account.AccountProfile", "AccountId") + .HasForeignKey("DysonNetwork.Shared.Models.SnAccountProfile", "AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_account_profiles_accounts_account_id"); @@ -1724,43 +1847,9 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Account"); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.ActionLog", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountPunishment", 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") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithMany() .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) @@ -1770,16 +1859,16 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Account"); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.Relationship", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountRelationship", b => { - b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithMany("OutgoingRelationships") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_account_relationships_accounts_account_id"); - b.HasOne("DysonNetwork.Pass.Account.Account", "Related") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Related") .WithMany("IncomingRelationships") .HasForeignKey("RelatedId") .OnDelete(DeleteBehavior.Cascade) @@ -1791,9 +1880,9 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Related"); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.Status", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountStatus", b => { - b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithMany() .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) @@ -1803,16 +1892,28 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Account"); }); - modelBuilder.Entity("DysonNetwork.Pass.Auth.ApiKey", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnActionLog", b => { - b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_action_logs_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnApiKey", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithMany() .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_api_keys_accounts_account_id"); - b.HasOne("DysonNetwork.Pass.Auth.AuthSession", "Session") + b.HasOne("DysonNetwork.Shared.Models.SnAuthSession", "Session") .WithMany() .HasForeignKey("SessionId") .OnDelete(DeleteBehavior.Cascade) @@ -1824,16 +1925,16 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Session"); }); - modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthChallenge", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthChallenge", b => { - b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithMany("Challenges") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_auth_challenges_accounts_account_id"); - b.HasOne("DysonNetwork.Pass.Auth.AuthClient", "Client") + b.HasOne("DysonNetwork.Shared.Models.SnAuthClient", "Client") .WithMany() .HasForeignKey("ClientId") .HasConstraintName("fk_auth_challenges_auth_clients_client_id"); @@ -1843,9 +1944,9 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Client"); }); - modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthClient", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthClient", b => { - b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithMany() .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) @@ -1855,16 +1956,16 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Account"); }); - modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthSession", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthSession", b => { - b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithMany("Sessions") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_auth_sessions_accounts_account_id"); - b.HasOne("DysonNetwork.Pass.Auth.AuthChallenge", "Challenge") + b.HasOne("DysonNetwork.Shared.Models.SnAuthChallenge", "Challenge") .WithMany() .HasForeignKey("ChallengeId") .HasConstraintName("fk_auth_sessions_auth_challenges_challenge_id"); @@ -1874,21 +1975,21 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Challenge"); }); - modelBuilder.Entity("DysonNetwork.Pass.Credit.SocialCreditRecord", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b => { - b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithMany() .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("fk_social_credit_records_accounts_account_id"); + .HasConstraintName("fk_account_check_in_results_accounts_account_id"); b.Navigation("Account"); }); - modelBuilder.Entity("DysonNetwork.Pass.Leveling.ExperienceRecord", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnExperienceRecord", b => { - b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithMany() .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) @@ -1898,9 +1999,19 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Account"); }); - modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionGroupMember", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b => { - b.HasOne("DysonNetwork.Pass.Permission.PermissionGroup", "Group") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .HasConstraintName("fk_magic_spells_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionGroupMember", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnPermissionGroup", "Group") .WithMany("Members") .HasForeignKey("GroupId") .OnDelete(DeleteBehavior.Cascade) @@ -1910,9 +2021,9 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Group"); }); - modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionNode", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionNode", b => { - b.HasOne("DysonNetwork.Pass.Permission.PermissionGroup", "Group") + b.HasOne("DysonNetwork.Shared.Models.SnPermissionGroup", "Group") .WithMany("Nodes") .HasForeignKey("GroupId") .HasConstraintName("fk_permission_nodes_permission_groups_group_id"); @@ -1920,62 +2031,21 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Group"); }); - modelBuilder.Entity("DysonNetwork.Pass.Wallet.Order", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", 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") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "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"); + .HasConstraintName("fk_social_credit_records_accounts_account_id"); b.Navigation("Account"); - - b.Navigation("Coupon"); }); - modelBuilder.Entity("DysonNetwork.Pass.Wallet.Transaction", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", 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") + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") .WithMany() .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) @@ -1985,9 +2055,59 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Account"); }); - modelBuilder.Entity("DysonNetwork.Pass.Wallet.WalletPocket", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletGift", b => { - b.HasOne("DysonNetwork.Pass.Wallet.Wallet", "Wallet") + b.HasOne("DysonNetwork.Shared.Models.SnWalletCoupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .HasConstraintName("fk_wallet_gifts_wallet_coupons_coupon_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Gifter") + .WithMany() + .HasForeignKey("GifterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_gifts_accounts_gifter_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Recipient") + .WithMany() + .HasForeignKey("RecipientId") + .HasConstraintName("fk_wallet_gifts_accounts_recipient_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Redeemer") + .WithMany() + .HasForeignKey("RedeemerId") + .HasConstraintName("fk_wallet_gifts_accounts_redeemer_id"); + + b.Navigation("Coupon"); + + b.Navigation("Gifter"); + + b.Navigation("Recipient"); + + b.Navigation("Redeemer"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletOrder", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .HasConstraintName("fk_payment_orders_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWalletTransaction", "Transaction") + .WithMany() + .HasForeignKey("TransactionId") + .HasConstraintName("fk_payment_orders_payment_transactions_transaction_id"); + + b.Navigation("PayeeWallet"); + + b.Navigation("Transaction"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletPocket", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "Wallet") .WithMany("Pockets") .HasForeignKey("WalletId") .OnDelete(DeleteBehavior.Cascade) @@ -1997,7 +2117,50 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Wallet"); }); - modelBuilder.Entity("DysonNetwork.Pass.Account.Account", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletSubscription", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_subscriptions_accounts_account_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWalletCoupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .HasConstraintName("fk_wallet_subscriptions_wallet_coupons_coupon_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWalletGift", "Gift") + .WithOne("Subscription") + .HasForeignKey("DysonNetwork.Shared.Models.SnWalletSubscription", "GiftId") + .HasConstraintName("fk_wallet_subscriptions_wallet_gifts_gift_id"); + + b.Navigation("Account"); + + b.Navigation("Coupon"); + + b.Navigation("Gift"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletTransaction", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "PayerWallet") + .WithMany() + .HasForeignKey("PayerWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payer_wallet_id"); + + b.Navigation("PayeeWallet"); + + b.Navigation("PayerWallet"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccount", b => { b.Navigation("AuthFactors"); @@ -2019,17 +2182,22 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Sessions"); }); - modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionGroup", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionGroup", b => { b.Navigation("Members"); b.Navigation("Nodes"); }); - modelBuilder.Entity("DysonNetwork.Pass.Wallet.Wallet", b => + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b => { b.Navigation("Pockets"); }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletGift", b => + { + b.Navigation("Subscription"); + }); #pragma warning restore 612, 618 } } diff --git a/DysonNetwork.Pass/Startup/BroadcastEventHandler.cs b/DysonNetwork.Pass/Startup/BroadcastEventHandler.cs index 682e792..83d17a1 100644 --- a/DysonNetwork.Pass/Startup/BroadcastEventHandler.cs +++ b/DysonNetwork.Pass/Startup/BroadcastEventHandler.cs @@ -49,31 +49,64 @@ public class BroadcastEventHandler( evt?.OrderId ); - if (evt?.ProductIdentifier is null || - !evt.ProductIdentifier.StartsWith(SubscriptionType.StellarProgram)) + if (evt?.ProductIdentifier is null) continue; - logger.LogInformation("Handling stellar program order: {OrderId}", evt.OrderId); - - await using var scope = serviceProvider.CreateAsyncScope(); - var db = scope.ServiceProvider.GetRequiredService(); - var subscriptions = scope.ServiceProvider.GetRequiredService(); - - var order = await db.PaymentOrders.FindAsync( - [evt.OrderId], - cancellationToken: stoppingToken - ); - if (order is null) + // Handle subscription orders + if (evt.ProductIdentifier.StartsWith(SubscriptionType.StellarProgram)) { - logger.LogWarning("Order with ID {OrderId} not found. Redelivering.", evt.OrderId); - await msg.NakAsync(cancellationToken: stoppingToken); + logger.LogInformation("Handling stellar program order: {OrderId}", evt.OrderId); + + await using var scope = serviceProvider.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var subscriptions = scope.ServiceProvider.GetRequiredService(); + + var order = await db.PaymentOrders.FindAsync( + [evt.OrderId], + cancellationToken: stoppingToken + ); + if (order is null) + { + logger.LogWarning("Order with ID {OrderId} not found. Redelivering.", evt.OrderId); + await msg.NakAsync(cancellationToken: stoppingToken); + continue; + } + + await subscriptions.HandleSubscriptionOrder(order); + + logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId); + await msg.AckAsync(cancellationToken: stoppingToken); + } + // Handle gift orders + else if (evt.Meta?.TryGetValue("gift_id", out var giftIdValue) == true) + { + logger.LogInformation("Handling gift order: {OrderId}", evt.OrderId); + + await using var scope = serviceProvider.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var subscriptions = scope.ServiceProvider.GetRequiredService(); + + var order = await db.PaymentOrders.FindAsync( + [evt.OrderId], + cancellationToken: stoppingToken + ); + if (order is null) + { + logger.LogWarning("Order with ID {OrderId} not found. Redelivering.", evt.OrderId); + await msg.NakAsync(cancellationToken: stoppingToken); + continue; + } + + await subscriptions.HandleGiftOrder(order); + + logger.LogInformation("Gift for order {OrderId} handled successfully.", evt.OrderId); + await msg.AckAsync(cancellationToken: stoppingToken); + } + else + { + // Not a subscription or gift order, skip continue; } - - await subscriptions.HandleSubscriptionOrder(order); - - logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId); - await msg.AckAsync(cancellationToken: stoppingToken); } catch (Exception ex) { diff --git a/DysonNetwork.Pass/Startup/ScheduledJobsConfiguration.cs b/DysonNetwork.Pass/Startup/ScheduledJobsConfiguration.cs index 92c7b48..905e103 100644 --- a/DysonNetwork.Pass/Startup/ScheduledJobsConfiguration.cs +++ b/DysonNetwork.Pass/Startup/ScheduledJobsConfiguration.cs @@ -46,6 +46,16 @@ public static class ScheduledJobsConfiguration .WithIntervalInMinutes(30) .RepeatForever()) ); + + var giftCleanupJob = new JobKey("GiftCleanup"); + q.AddJob(opts => opts.WithIdentity(giftCleanupJob)); + q.AddTrigger(opts => opts + .ForJob(giftCleanupJob) + .WithIdentity("GiftCleanupTrigger") + .WithSimpleSchedule(o => o + .WithIntervalInHours(1) + .RepeatForever()) + ); }); services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); diff --git a/DysonNetwork.Pass/Wallet/GiftCleanupJob.cs b/DysonNetwork.Pass/Wallet/GiftCleanupJob.cs new file mode 100644 index 0000000..50d6b0c --- /dev/null +++ b/DysonNetwork.Pass/Wallet/GiftCleanupJob.cs @@ -0,0 +1,40 @@ +using DysonNetwork.Shared.Models; +using Microsoft.EntityFrameworkCore; +using NodaTime; +using Quartz; + +namespace DysonNetwork.Pass.Wallet; + +public class GiftCleanupJob( + AppDatabase db, + ILogger logger +) : IJob +{ + public async Task Execute(IJobExecutionContext context) + { + logger.LogInformation("Starting gift cleanup job..."); + + var now = SystemClock.Instance.GetCurrentInstant(); + // Clean up gifts that are in Created status and older than 24 hours + var cutoffTime = now.Minus(Duration.FromHours(24)); + + var oldCreatedGifts = await db.WalletGifts + .Where(g => g.Status == GiftStatus.Created) + .Where(g => g.CreatedAt < cutoffTime) + .ToListAsync(); + + if (oldCreatedGifts.Count == 0) + { + logger.LogInformation("No old created gifts to clean up"); + return; + } + + logger.LogInformation("Found {Count} old created gifts to clean up", oldCreatedGifts.Count); + + // Remove the gifts + db.WalletGifts.RemoveRange(oldCreatedGifts); + await db.SaveChangesAsync(); + + logger.LogInformation("Successfully cleaned up {Count} old created gifts", oldCreatedGifts.Count); + } +} diff --git a/DysonNetwork.Pass/Wallet/SubscriptionGiftController.cs b/DysonNetwork.Pass/Wallet/SubscriptionGiftController.cs index 754c7de..87cc877 100644 --- a/DysonNetwork.Pass/Wallet/SubscriptionGiftController.cs +++ b/DysonNetwork.Pass/Wallet/SubscriptionGiftController.cs @@ -9,7 +9,10 @@ namespace DysonNetwork.Pass.Wallet; [ApiController] [Route("/api/subscriptions/gifts")] -public class GiftController(SubscriptionService subscriptions, AppDatabase db) : ControllerBase +public class SubscriptionGiftController( + SubscriptionService subscriptions, + AppDatabase db +) : ControllerBase { /// /// Lists gifts purchased by the current user. @@ -71,9 +74,9 @@ public class GiftController(SubscriptionService subscriptions, AppDatabase db) : if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); var gift = await db.WalletGifts - .Include(g => g.Gifter) - .Include(g => g.Recipient) - .Include(g => g.Redeemer) + .Include(g => g.Gifter).ThenInclude(a => a.Profile) + .Include(g => g.Recipient).ThenInclude(a => a.Profile) + .Include(g => g.Redeemer).ThenInclude(a => a.Profile) .Include(g => g.Subscription) .Include(g => g.Coupon) .FirstOrDefaultAsync(g => g.Id == giftId); @@ -101,7 +104,7 @@ public class GiftController(SubscriptionService subscriptions, AppDatabase db) : var canRedeem = false; var error = ""; - if (gift.Status != DysonNetwork.Shared.Models.GiftStatus.Sent) + if (gift.Status != DysonNetwork.Shared.Models.GiftStatus.Sent) { error = gift.Status switch { @@ -137,7 +140,8 @@ public class GiftController(SubscriptionService subscriptions, AppDatabase db) : .ToArray() : [gift.SubscriptionIdentifier]; - var existingSubscription = await subscriptions.GetSubscriptionAsync(currentUser.Id, subscriptionsInGroup); + var existingSubscription = + await subscriptions.GetSubscriptionAsync(currentUser.Id, subscriptionsInGroup); if (existingSubscription is not null) { error = "You already have an active subscription of this type."; @@ -147,7 +151,8 @@ public class GiftController(SubscriptionService subscriptions, AppDatabase db) : var profile = await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == currentUser.Id); if (profile is null || profile.Level < subscriptionInfo.RequiredLevel) { - error = $"Account level must be at least {subscriptionInfo.RequiredLevel} to redeem this gift."; + error = + $"Account level must be at least {subscriptionInfo.RequiredLevel} to redeem this gift."; } else { @@ -310,4 +315,26 @@ public class GiftController(SubscriptionService subscriptions, AppDatabase db) : return BadRequest(ex.Message); } } + + /// + /// Creates an order for an unpaid gift. + /// + [HttpPost("{giftId}/order")] + [Authorize] + public async Task> CreateGiftOrder(Guid giftId) + { + if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); + + try + { + var order = await subscriptions.CreateGiftOrder(currentUser.Id, giftId); + return order; + } + catch (InvalidOperationException ex) + { + return BadRequest(ex.Message); + } + } + + } diff --git a/DysonNetwork.Pass/Wallet/SubscriptionService.cs b/DysonNetwork.Pass/Wallet/SubscriptionService.cs index 8251cde..f8f9c84 100644 --- a/DysonNetwork.Pass/Wallet/SubscriptionService.cs +++ b/DysonNetwork.Pass/Wallet/SubscriptionService.cs @@ -42,6 +42,7 @@ public class SubscriptionService( : null; if (subscriptionInfo is null) throw new ArgumentOutOfRangeException(nameof(identifier), $@"Subscription {identifier} was not found."); + var subscriptionsInGroup = subscriptionInfo.GroupIdentifier is not null ? SubscriptionTypeData.SubscriptionDict .Where(s => s.Value.GroupIdentifier == subscriptionInfo.GroupIdentifier) @@ -57,36 +58,42 @@ public class SubscriptionService( if (existingSubscription is not null) return existingSubscription; + // Batch database queries for account profile and coupon to reduce round trips + var accountProfileTask = subscriptionInfo.RequiredLevel > 0 + ? db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == account.Id) + : Task.FromResult((Shared.Models.SnAccountProfile?)null); + + var prevFreeTrialTask = isFreeTrial + ? db.WalletSubscriptions.FirstOrDefaultAsync(s => s.AccountId == account.Id && s.Identifier == identifier && s.IsFreeTrial) + : Task.FromResult((SnWalletSubscription?)null); + + Guid couponGuidId = Guid.TryParse(coupon ?? "", out var parsedId) ? parsedId : Guid.Empty; + var couponTask = coupon != null + ? db.WalletCoupons.FirstOrDefaultAsync(c => + c.Id == couponGuidId || (c.Identifier != null && c.Identifier == coupon)) + : Task.FromResult((SnWalletCoupon?)null); + + // Await batched queries + var profile = await accountProfileTask; + var prevFreeTrial = await prevFreeTrialTask; + var couponData = await couponTask; + + // Validation checks if (subscriptionInfo.RequiredLevel > 0) { - var profile = await db.AccountProfiles - .Where(p => p.AccountId == account.Id) - .FirstOrDefaultAsync(); - if (profile is null) throw new InvalidOperationException("Account profile was not found."); + if (profile is null) + throw new InvalidOperationException("Account profile was not found."); if (profile.Level < subscriptionInfo.RequiredLevel) throw new InvalidOperationException( $"Account level must be at least {subscriptionInfo.RequiredLevel} to subscribe to {identifier}." ); } - if (isFreeTrial) - { - var prevFreeTrial = await db.WalletSubscriptions - .Where(s => s.AccountId == account.Id && s.Identifier == identifier && s.IsFreeTrial) - .FirstOrDefaultAsync(); - if (prevFreeTrial is not null) - throw new InvalidOperationException("Free trial already exists."); - } + if (isFreeTrial && prevFreeTrial != null) + throw new InvalidOperationException("Free trial already exists."); - SnWalletCoupon? couponData = null; - if (coupon is not null) - { - var inputCouponId = Guid.TryParse(coupon, out var parsedCouponId) ? parsedCouponId : Guid.Empty; - couponData = await db.WalletCoupons - .Where(c => (c.Id == inputCouponId) || (c.Identifier != null && c.Identifier == coupon)) - .FirstOrDefaultAsync(); - if (couponData is null) throw new InvalidOperationException($"Coupon {coupon} was not found."); - } + if (coupon != null && couponData is null) + throw new InvalidOperationException($"Coupon {coupon} was not found."); var now = SystemClock.Instance.GetCurrentInstant(); var subscription = new SnWalletSubscription @@ -266,6 +273,41 @@ public class SubscriptionService( ); } + /// + /// Creates a gift order for an unpaid gift. + /// + /// The account ID of the gifter. + /// The unique identifier for the gift. + /// A task that represents the asynchronous operation. The task result contains the created gift order. + /// Thrown when the gift is not found or not in payable status. + public async Task CreateGiftOrder(Guid accountId, Guid giftId) + { + var gift = await db.WalletGifts + .Where(g => g.Id == giftId && g.GifterId == accountId) + .Where(g => g.Status == DysonNetwork.Shared.Models.GiftStatus.Created) + .Include(g => g.Coupon) + .FirstOrDefaultAsync(); + if (gift is null) throw new InvalidOperationException("No matching gift found."); + + var subscriptionInfo = SubscriptionTypeData.SubscriptionDict + .TryGetValue(gift.SubscriptionIdentifier, out var template) + ? template + : null; + if (subscriptionInfo is null) throw new InvalidOperationException("No matching subscription found."); + + return await payment.CreateOrderAsync( + null, + subscriptionInfo.Currency, + gift.FinalPrice, + appIdentifier: "gift", + productIdentifier: gift.SubscriptionIdentifier, + meta: new Dictionary() + { + ["gift_id"] = gift.Id.ToString() + } + ); + } + public async Task HandleSubscriptionOrder(SnWalletOrder order) { if (order.Status != Shared.Models.OrderStatus.Paid || order.Meta?["subscription_id"] is not JsonElement subscriptionIdJson) @@ -285,14 +327,11 @@ public class SubscriptionService( if (subscription.Status == Shared.Models.SubscriptionStatus.Expired) { - var now = SystemClock.Instance.GetCurrentInstant(); - var cycle = subscription.BegunAt.Minus(subscription.RenewalAt ?? subscription.EndedAt ?? now); + // Calculate original cycle duration and extend from the current ended date + Duration originalCycle = subscription.EndedAt.Value - subscription.BegunAt; - var nextRenewalAt = subscription.RenewalAt?.Plus(cycle); - var nextEndedAt = subscription.EndedAt?.Plus(cycle); - - subscription.RenewalAt = nextRenewalAt; - subscription.EndedAt = nextEndedAt; + subscription.RenewalAt = subscription.RenewalAt.HasValue ? subscription.RenewalAt.Value.Plus(originalCycle) : subscription.EndedAt.Value.Plus(originalCycle); + subscription.EndedAt = subscription.EndedAt.Value.Plus(originalCycle); } subscription.Status = Shared.Models.SubscriptionStatus.Active; @@ -305,6 +344,36 @@ public class SubscriptionService( return subscription; } + public async Task HandleGiftOrder(SnWalletOrder order) + { + if (order.Status != Shared.Models.OrderStatus.Paid || order.Meta?["gift_id"] is not JsonElement giftIdJson) + throw new InvalidOperationException("Invalid order."); + + var giftId = Guid.TryParse(giftIdJson.ToString(), out var parsedGiftId) + ? parsedGiftId + : Guid.Empty; + if (giftId == Guid.Empty) + throw new InvalidOperationException("Invalid order."); + var gift = await db.WalletGifts + .Where(g => g.Id == giftId) + .Include(g => g.Coupon) + .FirstOrDefaultAsync(); + if (gift is null) + throw new InvalidOperationException("Invalid order."); + + if (gift.Status != DysonNetwork.Shared.Models.GiftStatus.Created) + throw new InvalidOperationException("Gift is not in payable status."); + + // Mark gift as sent after payment + gift.Status = DysonNetwork.Shared.Models.GiftStatus.Sent; + gift.UpdatedAt = SystemClock.Instance.GetCurrentInstant(); + + db.Update(gift); + await db.SaveChangesAsync(); + + return gift; + } + /// /// Updates the status of expired subscriptions to reflect their current state. /// This helps maintain accurate subscription records and is typically called periodically. @@ -326,16 +395,19 @@ public class SubscriptionService( if (expiredSubscriptions.Count == 0) return 0; + // Mark as expired foreach (var subscription in expiredSubscriptions) { subscription.Status = Shared.Models.SubscriptionStatus.Expired; - - // Clear the cache for this subscription - var cacheKey = $"{SubscriptionCacheKeyPrefix}{subscription.AccountId}:{subscription.Identifier}"; - await cache.RemoveAsync(cacheKey); } await db.SaveChangesAsync(); + + // Batch invalidate caches for better performance + var cacheTasks = expiredSubscriptions.Select(subscription => + cache.RemoveAsync($"{SubscriptionCacheKeyPrefix}{subscription.AccountId}:{subscription.Identifier}")); + await Task.WhenAll(cacheTasks); + return expiredSubscriptions.Count; } @@ -379,10 +451,11 @@ public class SubscriptionService( public async Task GetSubscriptionAsync(Guid accountId, params string[] identifiers) { // Create a unique cache key for this subscription - var hashBytes = MD5.HashData(Encoding.UTF8.GetBytes(string.Join(",", identifiers))); - var hashIdentifier = Convert.ToHexStringLower(hashBytes); + var identifierPart = identifiers.Length == 1 + ? identifiers[0] + : Convert.ToHexStringLower(MD5.HashData(Encoding.UTF8.GetBytes(string.Join(",", identifiers)))); - var cacheKey = $"{SubscriptionCacheKeyPrefix}{accountId}:{hashIdentifier}"; + var cacheKey = $"{SubscriptionCacheKeyPrefix}{accountId}:{identifierPart}"; // Try to get the subscription from cache first var (found, cachedSubscription) = await cache.GetAsyncWithStatus(cacheKey); @@ -443,17 +516,24 @@ public class SubscriptionService( var missingAccountIds = new List(); // Try to get the subscription from cache first - foreach (var accountId in accountIds) + var cacheTasks = accountIds.Select(async accountId => { var cacheKey = $"{SubscriptionPerkCacheKeyPrefix}{accountId}"; var (found, cachedSubscription) = await cache.GetAsyncWithStatus(cacheKey); + return (accountId, found, cachedSubscription); + }); + + var cacheResults = await Task.WhenAll(cacheTasks); + + foreach (var (accountId, found, cachedSubscription) in cacheResults) + { if (found && cachedSubscription != null) result[accountId] = cachedSubscription; else missingAccountIds.Add(accountId); } - if (missingAccountIds.Count <= 0) return result; + if (missingAccountIds.Count == 0) return result; // If not in cache, get from database var now = SystemClock.Instance.GetCurrentInstant(); @@ -464,18 +544,443 @@ public class SubscriptionService( .Where(s => s.EndedAt == null || s.EndedAt > now) .OrderByDescending(s => s.BegunAt) .ToListAsync(); - subscriptions = subscriptions.Where(s => s.IsAvailable).ToList(); - // Group the subscriptions by account id - foreach (var subscription in subscriptions) + // Group by account and select latest available subscription + var groupedSubscriptions = subscriptions + .Where(s => s.IsAvailable) + .GroupBy(s => s.AccountId) + .ToDictionary(g => g.Key, g => g.First()); + + // Update results and batch cache operations + var cacheSetTasks = new List(); + foreach (var kvp in groupedSubscriptions) { - result[subscription.AccountId] = subscription; - - // Cache the result if found (with 30 minutes expiry) - var cacheKey = $"{SubscriptionPerkCacheKeyPrefix}{subscription.AccountId}"; - await cache.SetAsync(cacheKey, subscription, TimeSpan.FromMinutes(30)); + result[kvp.Key] = kvp.Value; + var cacheKey = $"{SubscriptionPerkCacheKeyPrefix}{kvp.Key}"; + cacheSetTasks.Add(cache.SetAsync(cacheKey, kvp.Value, TimeSpan.FromMinutes(30))); } + await Task.WhenAll(cacheSetTasks); + return result; } -} \ No newline at end of file + + /// + /// Purchases a gift subscription that can be redeemed by another user. + /// + /// The account purchasing the gift. + /// Optional specific recipient. If null, creates an open gift anyone can redeem. + /// The subscription type being gifted. + /// Payment method used by the gifter. + /// Payment details from the gifter. + /// Optional personal message from the gifter. + /// Optional coupon code for discount. + /// How long the gift can be redeemed (default 30 days). + /// The duration of the subscription once redeemed (default 30 days). + /// The created gift record. + public async Task PurchaseGiftAsync( + SnAccount gifter, + Guid? recipientId, + string subscriptionIdentifier, + string paymentMethod, + SnPaymentDetails paymentDetails, + string? message = null, + string? coupon = null, + Duration? giftDuration = null, + Duration? cycleDuration = null) + { + // Validate subscription exists + var subscriptionInfo = SubscriptionTypeData + .SubscriptionDict.TryGetValue(subscriptionIdentifier, out var template) + ? template + : null; + if (subscriptionInfo is null) + throw new ArgumentOutOfRangeException(nameof(subscriptionIdentifier), + $@"Subscription {subscriptionIdentifier} was not found."); + + // Check if recipient account exists (if specified) + SnAccount? recipient = null; + if (recipientId.HasValue) + { + recipient = await db.Accounts + .Where(a => a.Id == recipientId.Value) + .Include(a => a.Profile) + .FirstOrDefaultAsync(); + if (recipient is null) + throw new ArgumentOutOfRangeException(nameof(recipientId), "Recipient account not found."); + } + + // Validate and get coupon if provided + Guid couponGuidId = Guid.TryParse(coupon ?? "", out var parsedId) ? parsedId : Guid.Empty; + var couponData = coupon != null + ? await db.WalletCoupons.FirstOrDefaultAsync(c => + c.Id == couponGuidId || (c.Identifier != null && c.Identifier == coupon)) + : null; + + if (coupon != null && couponData is null) + throw new InvalidOperationException($"Coupon {coupon} was not found."); + + // Set defaults + giftDuration ??= Duration.FromDays(30); // Gift expires in 30 days + cycleDuration ??= Duration.FromDays(30); // Subscription lasts 30 days once redeemed + + var now = SystemClock.Instance.GetCurrentInstant(); + + // Generate unique gift code + var giftCode = await GenerateUniqueGiftCodeAsync(); + + // Calculate final price (with potential coupon discount) + var tempSubscription = new SnWalletSubscription + { + BasePrice = subscriptionInfo.BasePrice, + CouponId = couponData?.Id, + Coupon = couponData, + BegunAt = now // Need for price calculation + }; + + var finalPrice = tempSubscription.CalculateFinalPriceAt(now); + + var gift = new SnWalletGift + { + GifterId = gifter.Id, + RecipientId = recipientId, + GiftCode = giftCode, + Message = message, + SubscriptionIdentifier = subscriptionIdentifier, + BasePrice = subscriptionInfo.BasePrice, + FinalPrice = finalPrice, + Status = DysonNetwork.Shared.Models.GiftStatus.Created, + ExpiresAt = now.Plus(giftDuration.Value), + IsOpenGift = !recipientId.HasValue, + PaymentMethod = paymentMethod, + PaymentDetails = paymentDetails, + CouponId = couponData?.Id, + CreatedAt = now, + UpdatedAt = now + }; + + db.WalletGifts.Add(gift); + await db.SaveChangesAsync(); + + // Create order and process payment + var order = await payment.CreateOrderAsync( + null, // No specific payee wallet for gifts + subscriptionInfo.Currency, + finalPrice, + appIdentifier: "gift", + productIdentifier: subscriptionIdentifier, + meta: new Dictionary + { + ["gift_id"] = gift.Id.ToString() + } + ); + + // If payment method is in-app wallet, process payment immediately + if (paymentMethod == SubscriptionPaymentMethod.InAppWallet) + { + var gifterWallet = await db.Wallets.FirstOrDefaultAsync(w => w.AccountId == gifter.Id); + if (gifterWallet == null) + throw new InvalidOperationException("Gifter wallet not found."); + + await payment.PayOrderAsync(order.Id, gifterWallet); + + // Mark gift as sent after successful payment + gift.Status = DysonNetwork.Shared.Models.GiftStatus.Sent; + gift.UpdatedAt = SystemClock.Instance.GetCurrentInstant(); + await db.SaveChangesAsync(); + } + + return gift; + } + + /// + /// Activates a gift using the redemption code, creating a subscription for the redeemer. + /// + /// The account redeeming the gift. + /// The unique redemption code. + /// A tuple containing the activated gift and the created subscription. + public async Task<(SnWalletGift Gift, SnWalletSubscription Subscription)> RedeemGiftAsync( + SnAccount redeemer, + string giftCode) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + // Find and validate the gift + var gift = await db.WalletGifts + .Include(g => g.Coupon) // Include coupon for price calculation + .FirstOrDefaultAsync(g => g.GiftCode == giftCode); + + if (gift is null) + throw new InvalidOperationException("Gift code not found."); + + if (gift.Status != DysonNetwork.Shared.Models.GiftStatus.Sent) + throw new InvalidOperationException("Gift is not available for redemption."); + + if (now > gift.ExpiresAt) + throw new InvalidOperationException("Gift has expired."); + + // Validate redeemer permissions + if (!gift.IsOpenGift && gift.RecipientId != redeemer.Id) + throw new InvalidOperationException("This gift is not intended for you."); + + // Check if redeemer already has this subscription type + var subscriptionInfo = SubscriptionTypeData + .SubscriptionDict.TryGetValue(gift.SubscriptionIdentifier, out var template) + ? template + : null; + if (subscriptionInfo is null) + throw new InvalidOperationException("Invalid gift subscription type."); + + var subscriptionsInGroup = subscriptionInfo.GroupIdentifier is not null + ? SubscriptionTypeData.SubscriptionDict + .Where(s => s.Value.GroupIdentifier == subscriptionInfo.GroupIdentifier) + .Select(s => s.Value.Identifier) + .ToArray() + : [gift.SubscriptionIdentifier]; + + var existingSubscription = await GetSubscriptionAsync(redeemer.Id, subscriptionsInGroup); + if (existingSubscription is not null) + throw new InvalidOperationException("You already have an active subscription of this type."); + + // Check account level requirement + if (subscriptionInfo.RequiredLevel > 0) + { + var profile = await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == redeemer.Id); + if (profile is null || profile.Level < subscriptionInfo.RequiredLevel) + throw new InvalidOperationException( + $"Account level must be at least {subscriptionInfo.RequiredLevel} to redeem this gift."); + } + + // Create the subscription from the gift + var cycleDuration = Duration.FromDays(30); // Standard 30-day subscription + var subscription = new SnWalletSubscription + { + BegunAt = now, + EndedAt = now.Plus(cycleDuration), + Identifier = gift.SubscriptionIdentifier, + IsActive = true, + IsFreeTrial = false, + Status = Shared.Models.SubscriptionStatus.Active, + PaymentMethod = $"gift:{gift.Id}", // Special payment method indicating gift redemption + PaymentDetails = new Shared.Models.SnPaymentDetails + { + Currency = "gift", + OrderId = gift.Id.ToString() + }, + BasePrice = gift.BasePrice, + CouponId = gift.CouponId, + Coupon = gift.Coupon, + RenewalAt = now.Plus(cycleDuration), + AccountId = redeemer.Id, + GiftId = gift.Id + }; + + // Update the gift status + gift.Status = DysonNetwork.Shared.Models.GiftStatus.Redeemed; + gift.RedeemedAt = now; + gift.RedeemerId = redeemer.Id; + gift.Subscription = subscription; + gift.UpdatedAt = now; + + // Save both gift and subscription + using var transaction = await db.Database.BeginTransactionAsync(); + try + { + db.WalletSubscriptions.Add(subscription); + db.WalletGifts.Update(gift); + await db.SaveChangesAsync(); + + await transaction.CommitAsync(); + } + catch + { + await transaction.RollbackAsync(); + throw; + } + + // Send notification to redeemer + await NotifyGiftRedeemed(gift, subscription, redeemer); + + // Send notification to gifter if different from redeemer + if (gift.GifterId != redeemer.Id) + { + var gifter = await db.Accounts.FirstOrDefaultAsync(a => a.Id == gift.GifterId); + if (gifter != null) + { + await NotifyGiftClaimedByRecipient(gift, subscription, gifter, redeemer); + } + } + + return (gift, subscription); + } + + /// + /// Retrieves a gift by its code (for redemption checking). + /// + public async Task GetGiftByCodeAsync(string giftCode) + { + return await db.WalletGifts + .Include(g => g.Gifter).ThenInclude(a => a.Profile) + .Include(g => g.Recipient).ThenInclude(a => a.Profile) + .Include(g => g.Redeemer).ThenInclude(a => a.Profile) + .Include(g => g.Coupon) + .FirstOrDefaultAsync(g => g.GiftCode == giftCode); + } + + /// + /// Retrieves gifts purchased by a specific account. + /// Only returns gifts that have been sent or processed (not created/unpaid ones). + /// + public async Task> GetGiftsByGifterAsync(Guid gifterId) + { + return await db.WalletGifts + .Include(g => g.Recipient).ThenInclude(a => a.Profile) + .Include(g => g.Redeemer).ThenInclude(a => a.Profile) + .Include(g => g.Subscription) + .Where(g => g.GifterId == gifterId && g.Status != DysonNetwork.Shared.Models.GiftStatus.Created) + .OrderByDescending(g => g.CreatedAt) + .ToListAsync(); + } + + public async Task> GetGiftsByRecipientAsync(Guid recipientId) + { + return await db.WalletGifts + .Include(g => g.Gifter).ThenInclude(a => a.Profile) + .Include(g => g.Redeemer).ThenInclude(a => a.Profile) + .Include(g => g.Subscription) + .Where(g => g.RecipientId == recipientId || (g.IsOpenGift && g.RedeemerId == recipientId)) + .OrderByDescending(g => g.CreatedAt) + .ToListAsync(); + } + + /// + /// Marks a gift as sent (ready for redemption). + /// + public async Task MarkGiftAsSentAsync(Guid giftId, Guid gifterId) + { + var gift = await db.WalletGifts.FirstOrDefaultAsync(g => g.Id == giftId && g.GifterId == gifterId); + if (gift is null) + throw new InvalidOperationException("Gift not found or access denied."); + + if (gift.Status != DysonNetwork.Shared.Models.GiftStatus.Created) + throw new InvalidOperationException("Gift cannot be marked as sent."); + + gift.Status = DysonNetwork.Shared.Models.GiftStatus.Sent; + gift.UpdatedAt = SystemClock.Instance.GetCurrentInstant(); + + await db.SaveChangesAsync(); + return gift; + } + + /// + /// Cancels a gift before it's redeemed. + /// + public async Task CancelGiftAsync(Guid giftId, Guid gifterId) + { + var gift = await db.WalletGifts.FirstOrDefaultAsync(g => g.Id == giftId && g.GifterId == gifterId); + if (gift is null) + throw new InvalidOperationException("Gift not found or access denied."); + + if (gift.Status != DysonNetwork.Shared.Models.GiftStatus.Created && gift.Status != DysonNetwork.Shared.Models.GiftStatus.Sent) + throw new InvalidOperationException("Gift cannot be cancelled."); + + gift.Status = DysonNetwork.Shared.Models.GiftStatus.Cancelled; + gift.UpdatedAt = SystemClock.Instance.GetCurrentInstant(); + + await db.SaveChangesAsync(); + return gift; + } + + private async Task GenerateUniqueGiftCodeAsync() + { + const int maxAttempts = 10; + const int codeLength = 12; + + for (int attempt = 0; attempt < maxAttempts; attempt++) + { + // Generate a random code + var code = GenerateRandomCode(codeLength); + + // Check if it already exists + var existingGift = await db.WalletGifts.FirstOrDefaultAsync(g => g.GiftCode == code); + if (existingGift is null) + return code; + } + + throw new InvalidOperationException("Unable to generate unique gift code."); + } + + private static string GenerateRandomCode(int length) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + var result = new char[length]; + for (int i = 0; i < length; i++) + { + result[i] = chars[Random.Shared.Next(chars.Length)]; + } + return new string(result); + } + + private async Task NotifyGiftRedeemed(SnWalletGift gift, SnWalletSubscription subscription, SnAccount redeemer) + { + Account.AccountService.SetCultureInfo(redeemer); + + var humanReadableName = + SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(subscription.Identifier, out var humanReadable) + ? humanReadable + : subscription.Identifier; + + var notification = new PushNotification + { + Topic = "gifts.redeemed", + Title = localizer["GiftRedeemedTitle"], + Body = localizer["GiftRedeemedBody", humanReadableName], + Meta = GrpcTypeHelper.ConvertObjectToByteString(new Dictionary + { + ["gift_id"] = gift.Id.ToString(), + ["subscription_id"] = subscription.Id.ToString() + }), + IsSavable = true + }; + + await pusher.SendPushNotificationToUserAsync( + new SendPushNotificationToUserRequest + { + UserId = redeemer.Id.ToString(), + Notification = notification + } + ); + } + + private async Task NotifyGiftClaimedByRecipient(SnWalletGift gift, SnWalletSubscription subscription, SnAccount gifter, SnAccount redeemer) + { + Account.AccountService.SetCultureInfo(gifter); + + var humanReadableName = + SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(subscription.Identifier, out var humanReadable) + ? humanReadable + : subscription.Identifier; + + var notification = new PushNotification + { + Topic = "gifts.claimed", + Title = localizer["GiftClaimedTitle"], + Body = localizer["GiftClaimedBody", humanReadableName, redeemer.Name ?? redeemer.Id.ToString()], + Meta = GrpcTypeHelper.ConvertObjectToByteString(new Dictionary + { + ["gift_id"] = gift.Id.ToString(), + ["subscription_id"] = subscription.Id.ToString(), + ["redeemer_id"] = redeemer.Id.ToString() + }), + IsSavable = true + }; + + await pusher.SendPushNotificationToUserAsync( + new SendPushNotificationToUserRequest + { + UserId = gifter.Id.ToString(), + Notification = notification + } + ); + } +} diff --git a/DysonNetwork.Ring/appsettings.json b/DysonNetwork.Ring/appsettings.json index 2ab4e9e..f9d021a 100644 --- a/DysonNetwork.Ring/appsettings.json +++ b/DysonNetwork.Ring/appsettings.json @@ -9,7 +9,7 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "App": "Host=localhost;Port=5432;Database=dyson_pusher;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" + "App": "Host=localhost;Port=5432;Database=dyson_ring;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" }, "Notifications": { "Push": { @@ -36,10 +36,7 @@ "GeoIp": { "DatabasePath": "./Keys/GeoLite2-City.mmdb" }, - "KnownProxies": [ - "127.0.0.1", - "::1" - ], + "KnownProxies": ["127.0.0.1", "::1"], "Service": { "Name": "DysonNetwork.Ring", "Url": "https://localhost:7259" diff --git a/DysonNetwork.Shared/Models/Subscription.cs b/DysonNetwork.Shared/Models/Subscription.cs index ef9a473..03ed392 100644 --- a/DysonNetwork.Shared/Models/Subscription.cs +++ b/DysonNetwork.Shared/Models/Subscription.cs @@ -58,6 +58,185 @@ public record class SubscriptionTypeData( }; } +/// +/// Represents a gifted subscription that can be claimed by another user. +/// Support both direct gifts (to specific users) and open gifts (anyone can redeem via link/code). +/// +[Index(nameof(GiftCode))] +[Index(nameof(GifterId))] +[Index(nameof(RecipientId))] +public class SnWalletGift : ModelBase +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + /// + /// The user who purchased/gave the gift. + /// + public Guid GifterId { get; set; } + public SnAccount Gifter { get; set; } = null!; + + /// + /// The intended recipient. Null for open gifts that anyone can redeem. + /// + public Guid? RecipientId { get; set; } + public SnAccount? Recipient { get; set; } + + /// + /// Unique redemption code/link identifier for the gift. + /// + [MaxLength(128)] + public string GiftCode { get; set; } = null!; + + /// + /// Optional custom message from the gifter. + /// + [MaxLength(1000)] + public string? Message { get; set; } + + /// + /// The subscription type being gifted. + /// + [MaxLength(4096)] + public string SubscriptionIdentifier { get; set; } = null!; + + /// + /// The original price before any discounts. + /// + public decimal BasePrice { get; set; } + + /// + /// The final price paid after discounts. + /// + public decimal FinalPrice { get; set; } + + /// + /// Current status of the gift. + /// + public GiftStatus Status { get; set; } = GiftStatus.Created; + + /// + /// When the gift was redeemed. Null if not yet redeemed. + /// + public Instant? RedeemedAt { get; set; } + + /// + /// The user who redeemed the gift (if different from recipient). + /// + public Guid? RedeemerId { get; set; } + public SnAccount? Redeemer { get; set; } + + /// + /// The subscription created when the gift is redeemed. + /// + public SnWalletSubscription? Subscription { get; set; } + + /// + /// When the gift expires and can no longer be redeemed. + /// + public Instant ExpiresAt { get; set; } + + /// + /// Whether this gift can be redeemed by anyone (open gift) or only the specified recipient. + /// + public bool IsOpenGift { get; set; } + + /// + /// Payment method used by the gifter. + /// + [MaxLength(4096)] + public string PaymentMethod { get; set; } = null!; + + [Column(TypeName = "jsonb")] + public SnPaymentDetails PaymentDetails { get; set; } = null!; + + /// + /// Coupon used for the gift purchase. + /// + public Guid? CouponId { get; set; } + public SnWalletCoupon? Coupon { get; set; } + + /// + /// Checks if the gift can still be redeemed. + /// + [NotMapped] + public bool IsRedeemable + { + get + { + if (Status != GiftStatus.Sent) return false; + var now = SystemClock.Instance.GetCurrentInstant(); + return now <= ExpiresAt; + } + } + + /// + /// Checks if the gift has expired. + /// + [NotMapped] + public bool IsExpired + { + get + { + if (Status == GiftStatus.Redeemed || Status == GiftStatus.Cancelled) return false; + var now = SystemClock.Instance.GetCurrentInstant(); + return now > ExpiresAt; + } + } + + // TODO: Uncomment once protobuf files are regenerated + /* + public Proto.Gift ToProtoValue() => new() + { + Id = Id.ToString(), + GifterId = GifterId.ToString(), + RecipientId = RecipientId?.ToString(), + GiftCode = GiftCode, + Message = Message, + SubscriptionIdentifier = SubscriptionIdentifier, + BasePrice = BasePrice.ToString(CultureInfo.InvariantCulture), + FinalPrice = FinalPrice.ToString(CultureInfo.InvariantCulture), + Status = (Proto.GiftStatus)Status, + RedeemedAt = RedeemedAt?.ToTimestamp(), + RedeemerId = RedeemerId?.ToString(), + SubscriptionId = SubscriptionId?.ToString(), + ExpiresAt = ExpiresAt.ToTimestamp(), + IsOpenGift = IsOpenGift, + PaymentMethod = PaymentMethod, + PaymentDetails = PaymentDetails.ToProtoValue(), + CouponId = CouponId?.ToString(), + Coupon = Coupon?.ToProtoValue(), + IsRedeemable = IsRedeemable, + IsExpired = IsExpired, + CreatedAt = CreatedAt.ToTimestamp(), + UpdatedAt = UpdatedAt.ToTimestamp() + }; + + public static SnWalletGift FromProtoValue(Proto.Gift proto) => new() + { + Id = Guid.Parse(proto.Id), + GifterId = Guid.Parse(proto.GifterId), + RecipientId = proto.HasRecipientId ? Guid.Parse(proto.RecipientId) : null, + GiftCode = proto.GiftCode, + Message = proto.Message, + SubscriptionIdentifier = proto.SubscriptionIdentifier, + BasePrice = decimal.Parse(proto.BasePrice), + FinalPrice = decimal.Parse(proto.FinalPrice), + Status = (GiftStatus)proto.Status, + RedeemedAt = proto.RedeemedAt?.ToInstant(), + RedeemerId = proto.HasRedeemerId ? Guid.Parse(proto.RedeemerId) : null, + SubscriptionId = proto.HasSubscriptionId ? Guid.Parse(proto.SubscriptionId) : null, + ExpiresAt = proto.ExpiresAt.ToInstant(), + IsOpenGift = proto.IsOpenGift, + PaymentMethod = proto.PaymentMethod, + PaymentDetails = SnPaymentDetails.FromProtoValue(proto.PaymentDetails), + CouponId = proto.HasCouponId ? Guid.Parse(proto.CouponId) : null, + Coupon = proto.Coupon is not null ? SnWalletCoupon.FromProtoValue(proto.Coupon) : null, + CreatedAt = proto.CreatedAt.ToInstant(), + UpdatedAt = proto.UpdatedAt.ToInstant() + }; + */ +} + public abstract class SubscriptionType { /// @@ -99,11 +278,24 @@ public enum SubscriptionStatus Cancelled } +public enum GiftStatus +{ + Created = 0, + Sent = 1, + Redeemed = 2, + Expired = 3, + Cancelled = 4 +} + /// /// The subscription is for the Stellar Program in most cases. /// The paid subscription in another word. /// [Index(nameof(Identifier))] +[Index(nameof(AccountId))] +[Index(nameof(Status))] +[Index(nameof(AccountId), nameof(Identifier))] +[Index(nameof(AccountId), nameof(IsActive))] public class SnWalletSubscription : ModelBase { public Guid Id { get; set; } = Guid.NewGuid(); @@ -142,40 +334,51 @@ public class SnWalletSubscription : ModelBase public Guid AccountId { get; set; } public SnAccount Account { get; set; } = null!; + /// + /// If this subscription was redeemed from a gift, this references the gift record. + /// + public Guid? GiftId { get; set; } + public SnWalletGift? Gift { get; set; } + [NotMapped] public bool IsAvailable { - get - { - if (!IsActive) return false; - - var now = SystemClock.Instance.GetCurrentInstant(); - - if (BegunAt > now) return false; - if (EndedAt.HasValue && now > EndedAt.Value) return false; - if (RenewalAt.HasValue && now > RenewalAt.Value) return false; - if (Status != SubscriptionStatus.Active) return false; - - return true; - } + get => IsAvailableAt(SystemClock.Instance.GetCurrentInstant()); } [NotMapped] public decimal FinalPrice { - get - { - if (IsFreeTrial) return 0; - if (Coupon == null) return BasePrice; + get => CalculateFinalPriceAt(SystemClock.Instance.GetCurrentInstant()); + } - var now = SystemClock.Instance.GetCurrentInstant(); - if (Coupon.AffectedAt.HasValue && now < Coupon.AffectedAt.Value || - Coupon.ExpiredAt.HasValue && now > Coupon.ExpiredAt.Value) return BasePrice; + /// + /// Optimized method to check availability at a specific instant (avoids repeated SystemClock calls). + /// + public bool IsAvailableAt(Instant currentInstant) + { + if (!IsActive) return false; + if (BegunAt > currentInstant) return false; + if (EndedAt.HasValue && currentInstant > EndedAt.Value) return false; + if (RenewalAt.HasValue && currentInstant > RenewalAt.Value) return false; + if (Status != SubscriptionStatus.Active) return false; + return true; + } - if (Coupon.DiscountAmount.HasValue) return BasePrice - Coupon.DiscountAmount.Value; - if (Coupon.DiscountRate.HasValue) return BasePrice * (decimal)(1 - Coupon.DiscountRate.Value); - return BasePrice; - } + /// + /// Optimized method to calculate final price at a specific instant (avoids repeated SystemClock calls). + /// + public decimal CalculateFinalPriceAt(Instant currentInstant) + { + if (IsFreeTrial) return 0; + if (Coupon == null) return BasePrice; + + if (Coupon.AffectedAt.HasValue && currentInstant < Coupon.AffectedAt.Value || + Coupon.ExpiredAt.HasValue && currentInstant > Coupon.ExpiredAt.Value) return BasePrice; + + if (Coupon.DiscountAmount.HasValue) return BasePrice - Coupon.DiscountAmount.Value; + if (Coupon.DiscountRate.HasValue) return BasePrice * (decimal)(1 - Coupon.DiscountRate.Value); + return BasePrice; } /// @@ -184,6 +387,9 @@ public class SnWalletSubscription : ModelBase /// public SnSubscriptionReferenceObject ToReference() { + // Cache the current instant once to avoid multiple SystemClock calls + var currentInstant = SystemClock.Instance.GetCurrentInstant(); + return new SnSubscriptionReferenceObject { Id = Id, @@ -191,11 +397,11 @@ public class SnWalletSubscription : ModelBase BegunAt = BegunAt, EndedAt = EndedAt, IsActive = IsActive, - IsAvailable = IsAvailable, + IsAvailable = IsAvailableAt(currentInstant), IsFreeTrial = IsFreeTrial, Status = Status, BasePrice = BasePrice, - FinalPrice = FinalPrice, + FinalPrice = CalculateFinalPriceAt(currentInstant), RenewalAt = RenewalAt, AccountId = AccountId }; @@ -263,11 +469,13 @@ public class SnSubscriptionReferenceObject : ModelBase public Instant? RenewalAt { get; set; } public Guid AccountId { get; set; } + private string? _displayName; + /// - /// Gets the human-readable name of the subscription type if available. + /// Gets the human-readable name of the subscription type if available (cached for performance). /// [NotMapped] - public string? DisplayName => SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(Identifier, out var name) + public string? DisplayName => _displayName ??= SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(Identifier, out var name) ? name : null; @@ -281,8 +489,8 @@ public class SnSubscriptionReferenceObject : ModelBase IsAvailable = IsAvailable, IsFreeTrial = IsFreeTrial, Status = (Proto.SubscriptionStatus)Status, - BasePrice = BasePrice.ToString(CultureInfo.CurrentCulture), - FinalPrice = FinalPrice.ToString(CultureInfo.CurrentCulture), + BasePrice = BasePrice.ToString(CultureInfo.InvariantCulture), + FinalPrice = FinalPrice.ToString(CultureInfo.InvariantCulture), RenewalAt = RenewalAt?.ToTimestamp(), AccountId = AccountId.ToString(), DisplayName = DisplayName, @@ -401,4 +609,4 @@ public class SnWalletCoupon : ModelBase CreatedAt = proto.CreatedAt.ToInstant(), UpdatedAt = proto.UpdatedAt.ToInstant() }; -} \ No newline at end of file +} diff --git a/DysonNetwork.Shared/Proto/wallet.proto b/DysonNetwork.Shared/Proto/wallet.proto index 841741b..7de8723 100644 --- a/DysonNetwork.Shared/Proto/wallet.proto +++ b/DysonNetwork.Shared/Proto/wallet.proto @@ -31,6 +31,16 @@ enum SubscriptionStatus { SUBSCRIPTION_STATUS_CANCELLED = 4; } +enum GiftStatus { + // Using proto3 enum naming convention + GIFT_STATUS_UNSPECIFIED = 0; + GIFT_STATUS_CREATED = 1; + GIFT_STATUS_SENT = 2; + GIFT_STATUS_REDEEMED = 3; + GIFT_STATUS_EXPIRED = 4; + GIFT_STATUS_CANCELLED = 5; +} + message Subscription { string id = 1; google.protobuf.Timestamp begun_at = 2; @@ -93,6 +103,31 @@ message Coupon { google.protobuf.Timestamp updated_at = 10; } +message Gift { + string id = 1; + string gifter_id = 2; + optional string recipient_id = 3; + string gift_code = 4; + optional string message = 5; + string subscription_identifier = 6; + string base_price = 7; + string final_price = 8; + GiftStatus status = 9; + optional google.protobuf.Timestamp redeemed_at = 10; + optional string redeemer_id = 11; + optional string subscription_id = 12; + google.protobuf.Timestamp expires_at = 13; + bool is_open_gift = 14; + string payment_method = 15; + PaymentDetails payment_details = 16; + optional string coupon_id = 17; + optional Coupon coupon = 18; + bool is_redeemable = 19; + bool is_expired = 20; + google.protobuf.Timestamp created_at = 21; + google.protobuf.Timestamp updated_at = 22; +} + service WalletService { rpc GetWallet(GetWalletRequest) returns (Wallet); rpc CreateWallet(CreateWalletRequest) returns (Wallet); diff --git a/README_GIFT_SUBSCRIPTIONS.md b/README_GIFT_SUBSCRIPTIONS.md new file mode 100644 index 0000000..bc66662 --- /dev/null +++ b/README_GIFT_SUBSCRIPTIONS.md @@ -0,0 +1,345 @@ +# Gift Subscriptions API Documentation + +## Overview + +The Gift Subscriptions feature allows users to purchase subscription gifts that can be redeemed by other users, enabling social gifting and subscription sharing within the DysonNetwork platform. + +If you use it through the gateway, the `/api` should be replaced with the `/id` + +### Key Features + +- **Purchase Gifts**: Users can buy subscriptions as gifts for specific recipients or as open gifts +- **Gift Codes**: Each gift has a unique redemption code +- **Flexible Redemption**: Open gifts can be redeemed by anyone, while targeted gifts are recipient-specific +- **Security**: Prevents duplicate subscriptions and enforces account level requirements +- **Integration**: Full integration with existing subscription, coupon, and pricing systems +- **Clean User Experience**: Unpaid gifts are hidden from users and automatically cleaned up +- **Automatic Maintenance**: Old unpaid gifts are removed after 24 hours + +## API Endpoints + +All endpoints are authenticated and require a valid user session. The base path for gift endpoints is `/api/gifts`. + +### 1. List Sent Gifts + +Retrieve gifts you have purchased. + +```http +GET /api/gifts/sent?offset=0&take=20 +Authorization: Bearer +``` + +**Response**: Array of `SnWalletGift` objects + +### 2. List Received Gifts + +Retrieve gifts sent to you or redeemed by you (for open gifts). + +```http +GET /api/gifts/received?offset=0&take=20 +Authorization: Bearer +``` + +**Response**: Array of `SnWalletGift` objects + +### 3. Get Specific Gift + +Retrieve details for a specific gift. + +```http +GET /api/gifts/{giftId} +Authorization: Bearer +``` + +**Parameters**: +- `giftId`: GUID of the gift + +**Response**: `SnWalletGift` object + +### 4. Check Gift Code + +Validate if a gift code can be redeemed by the current user. + +```http +GET /api/gifts/check/{giftCode} +Authorization: Bearer +``` + +**Response**: +```json +{ + "gift_code": "ABCD1234EFGH", + "subscription_identifier": "basic", + "can_redeem": true, + "error": null, + "message": "Happy birthday!" +} +``` + +### 5. Purchase a Gift + +Create and purchase a gift subscription. + +```http +POST /api/gifts/purchase +Authorization: Bearer +Content-Type: application/json + +{ + "subscription_identifier": "premium", + "recipient_id": "550e8400-e29b-41d4-a716-446655440000", // Optional: null for open gifts + "payment_method": "in_app_wallet", + "payment_details": { + "currency": "irl" + }, + "message": "Enjoy your premium subscription!", // Optional + "coupon": "SAVE20", // Optional + "gift_duration_days": 30, // Optional: defaults to 30 + "subscription_duration_days": 30 // Optional: defaults to 30 +} +``` + +**Response**: `SnWalletGift` object + +### 6. Redeem a Gift + +Redeem a gift code to create a subscription for yourself. + +```http +POST /api/gifts/redeem +Authorization: Bearer +Content-Type: application/json + +{ + "gift_code": "ABCD1234EFGH" +} +``` + +**Response**: +```json +{ + "gift": { ... }, + "subscription": { ... } +} +``` + +### 7. Mark Gift as Sent + +Mark a gift as sent (ready for redemption). + +```http +POST /api/gifts/{giftId}/send +Authorization: Bearer +``` + +**Parameters**: +- `giftId`: GUID of the gift to mark as sent + +### 8. Cancel a Gift + +Cancel a gift before it has been redeemed. + +```http +POST /api/gifts/{giftId}/cancel +Authorization: Bearer +``` + +**Parameters**: +- `giftId`: GUID of the gift to cancel + +## Usage Examples + +### Client Implementation + +Here are examples showing how to integrate gift subscriptions into your client application. + +#### Example 1: Purchase a Gift for a Specific User + +```javascript +async function purchaseGiftForFriend(subscriptionId, friendId, message) { + const response = await fetch('/api/gifts/purchase', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + subscription_identifier: subscriptionId, + recipient_id: friendId, + payment_method: 'in_app_wallet', + payment_details: { currency: 'irl' }, + message: message + }) + }); + + const gift = await response.json(); + return gift.gift_code; // Share this code with the friend +} +``` + +#### Example 2: Create an Open Gift + +```javascript +async function createOpenGift(subscriptionId) { + const response = await fetch('/api/gifts/purchase', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + subscription_identifier: subscriptionId, + payment_method: 'in_app_wallet', + payment_details: { currency: 'irl' }, + message: 'Redeem this anywhere!' + // No recipient_id makes it an open gift + }) + }); + + const gift = await response.json(); + // Mark as sent to make it redeemable + await markGiftAsSent(gift.id); + return gift; +} +``` + +#### Example 3: Redeem a Gift Code + +```javascript +async function redeemGiftCode(giftCode) { + // First, check if the gift can be redeemed + const checkResponse = await fetch(`/api/gifts/check/${giftCode}`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + const checkResult = await checkResponse.json(); + + if (!checkResult.canRedeem) { + throw new Error(checkResult.error); + } + + // If valid, redeem it + const redeemResponse = await fetch('/api/gifts/redeem', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + gift_code: giftCode + }) + }); + + const result = await redeemResponse.json(); + return result.subscription; // The newly created subscription +} +``` + +#### Example 4: Display User's Gift History + +```javascript +async function getGiftHistory() { + // Get gifts I sent + const sentResponse = await fetch('/api/gifts/sent', { + headers: { 'Authorization': `Bearer ${token}` } + }); + const sentGifts = await sentResponse.json(); + + // Get gifts I received + const receivedResponse = await fetch('/api/gifts/received', { + headers: { 'Authorization': `Bearer ${token}` } + }); + const receivedGifts = await receivedResponse.json(); + + return { sent: sentGifts, received: receivedGifts }; +} +``` + +## Gift Status Lifecycle + +Gifts follow this status lifecycle: + +1. **Created**: Initially purchased, can be cancelled or marked as sent + - **Note**: Gifts in "Created" status are not visible to users and are automatically cleaned up after 24 hours if unpaid +2. **Sent**: Made available for redemption, can be cancelled +3. **Redeemed**: Successfully redeemed, creates a subscription +4. **Cancelled**: Permanently cancelled, refund may be processed +5. **Expired**: Expired without redemption + +## Automatic Maintenance + +The system includes automatic cleanup to maintain data integrity: + +- **Unpaid Gift Cleanup**: Gifts that remain in "Created" status (unpaid) for more than 24 hours are automatically removed from the database +- **User Visibility**: Only gifts that have been successfully paid and sent are visible in user gift lists +- **Background Processing**: Cleanup runs hourly via scheduled jobs + +This ensures a clean user experience while preventing accumulation of abandoned gift purchases. + +## Validation Rules + +### Purchase Validation +- Subscription must exist and be valid +- If coupon provided, it must be valid and applicable +- Recipient account must exist (if specified) +- User must meet level requirements for the subscription + +### Redemption Validation +- Gift code must exist +- Gift must be in "Sent" status +- Gift must not be expired +- User must meet level requirements +- User must not already have an active subscription of the same type +- For targeted gifts, user must be the specified recipient + +## Pricing & Payments + +Gifts use the same pricing system as regular subscriptions: + +- Base price from subscription template +- Coupon discounts applied +- Currency conversion as needed +- Payment processing through existing payment methods + +## Notification Events + +The system sends push notifications for: + +- **gifts.redeemed**: When someone redeems your gift +- **gifts.claimed**: When the recipient redeems your targeted gift + +Notifications include gift and subscription details for rich UI updates. + +## Error Handling + +Common error responses: + +- `400 Bad Request`: Invalid parameters, validation failures +- `401 Unauthorized`: Missing or invalid authentication +- `403 Forbidden`: Insufficient permissions +- `404 Not Found`: Gift or subscription not found +- `409 Conflict`: Business logic violations (duplicate subscriptions, etc.) + +## Integration Notes + +### Database Schema +The feature adds a `wallet_gifts` table with relationships to: +- `accounts` (gifter, recipient, redeemer) +- `wallet_subscriptions` (created subscription) +- `wallet_coupons` (applied discounts) + +### Backwards Compatibility +- No changes to existing subscription endpoints +- New gift-related endpoints are additive +- Existing payment flows remain unchanged + +### Performance Considerations +- Gift codes are indexed for fast lookups +- Status filters optimize database queries +- Caching integrated with existing subscription caching + +## Support + +For implementation questions or issues, refer to the DysonNetwork API documentation or contact the development team. diff --git a/settings/develop.json b/settings/develop.json index a70297c..eb6fc8c 100644 --- a/settings/develop.json +++ b/settings/develop.json @@ -10,7 +10,7 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "App": "Host=host.docker.internal;Port=5432;Database=dyson_network_dev;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" + "App": "Host=host.docker.internal;Port=5432;Database=dyson_develop;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" }, "KnownProxies": ["127.0.0.1", "::1"], "Etcd": { diff --git a/settings/ring.json b/settings/ring.json index 58332b9..43ef537 100644 --- a/settings/ring.json +++ b/settings/ring.json @@ -9,7 +9,7 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "App": "Host=host.docker.internal;Port=5432;Database=dyson_pusher;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" + "App": "Host=host.docker.internal;Port=5432;Database=dyson_ring;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" }, "Notifications": { "Push": {