✨ Putting the stellar perks back
This commit is contained in:
		@@ -1,6 +1,7 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
using DysonNetwork.Pass.Wallet;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
@@ -31,6 +32,8 @@ public class Account : ModelBase
 | 
			
		||||
 | 
			
		||||
    [JsonIgnore] public ICollection<Relationship> OutgoingRelationships { get; set; } = new List<Relationship>();
 | 
			
		||||
    [JsonIgnore] public ICollection<Relationship> IncomingRelationships { get; set; } = new List<Relationship>();
 | 
			
		||||
    
 | 
			
		||||
    [NotMapped] public SubscriptionReferenceObject? PerkSubscription { get; set; }
 | 
			
		||||
 | 
			
		||||
    public Shared.Proto.Account ToProtoValue()
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using DysonNetwork.Pass.Auth;
 | 
			
		||||
using DysonNetwork.Pass.Wallet;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
@@ -12,6 +13,7 @@ public class AccountController(
 | 
			
		||||
    AppDatabase db,
 | 
			
		||||
    AuthService auth,
 | 
			
		||||
    AccountService accounts,
 | 
			
		||||
    SubscriptionService subscriptions,
 | 
			
		||||
    AccountEventService events
 | 
			
		||||
) : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
@@ -25,7 +27,12 @@ public class AccountController(
 | 
			
		||||
            .Include(e => e.Profile)
 | 
			
		||||
            .Where(a => a.Name == name)
 | 
			
		||||
            .FirstOrDefaultAsync();
 | 
			
		||||
        return account is null ? new NotFoundResult() : account;
 | 
			
		||||
        if (account is null) return new NotFoundResult();
 | 
			
		||||
        
 | 
			
		||||
        var perk = await subscriptions.GetPerkSubscriptionAsync(account.Id);
 | 
			
		||||
        account.PerkSubscription = perk?.ToReference();
 | 
			
		||||
        
 | 
			
		||||
        return account;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("{name}/badges")]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using DysonNetwork.Pass.Permission;
 | 
			
		||||
using DysonNetwork.Pass.Wallet;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using DysonNetwork.Shared.Proto;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
@@ -18,6 +19,7 @@ namespace DysonNetwork.Pass.Account;
 | 
			
		||||
public class AccountCurrentController(
 | 
			
		||||
    AppDatabase db,
 | 
			
		||||
    AccountService accounts,
 | 
			
		||||
    SubscriptionService subscriptions,
 | 
			
		||||
    AccountEventService events,
 | 
			
		||||
    AuthService auth,
 | 
			
		||||
    FileService.FileServiceClient files,
 | 
			
		||||
@@ -36,6 +38,9 @@ public class AccountCurrentController(
 | 
			
		||||
            .Include(e => e.Profile)
 | 
			
		||||
            .Where(e => e.Id == userId)
 | 
			
		||||
            .FirstOrDefaultAsync();
 | 
			
		||||
        
 | 
			
		||||
        var perk = await subscriptions.GetPerkSubscriptionAsync(account.Id);
 | 
			
		||||
        account.PerkSubscription = perk?.ToReference();
 | 
			
		||||
 | 
			
		||||
        return Ok(account);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,16 @@
 | 
			
		||||
using DysonNetwork.Pass.Wallet;
 | 
			
		||||
using DysonNetwork.Shared.Proto;
 | 
			
		||||
using Google.Protobuf.WellKnownTypes;
 | 
			
		||||
using Grpc.Core;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using NodaTime.Serialization.Protobuf;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Pass.Account;
 | 
			
		||||
 | 
			
		||||
public class AccountServiceGrpc(
 | 
			
		||||
    AppDatabase db,
 | 
			
		||||
    RelationshipService relationships,
 | 
			
		||||
    SubscriptionService subscriptions,
 | 
			
		||||
    IClock clock,
 | 
			
		||||
    ILogger<AccountServiceGrpc> logger
 | 
			
		||||
)
 | 
			
		||||
@@ -34,6 +35,9 @@ public class AccountServiceGrpc(
 | 
			
		||||
        if (account == null)
 | 
			
		||||
            throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, $"Account {request.Id} not found"));
 | 
			
		||||
 | 
			
		||||
        var perk = await subscriptions.GetPerkSubscriptionAsync(account.Id);
 | 
			
		||||
        account.PerkSubscription = perk?.ToReference();
 | 
			
		||||
 | 
			
		||||
        return account.ToProtoValue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -51,13 +55,21 @@ public class AccountServiceGrpc(
 | 
			
		||||
            .Where(a => accountIds.Contains(a.Id))
 | 
			
		||||
            .Include(a => a.Profile)
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
        
 | 
			
		||||
        var perks = await subscriptions.GetPerkSubscriptionsAsync(
 | 
			
		||||
            accounts.Select(x => x.Id).ToList()
 | 
			
		||||
        );
 | 
			
		||||
        foreach (var account in accounts)
 | 
			
		||||
            if (perks.TryGetValue(account.Id, out var perk))
 | 
			
		||||
                account.PerkSubscription = perk?.ToReference();
 | 
			
		||||
 | 
			
		||||
        var response = new GetAccountBatchResponse();
 | 
			
		||||
        response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue()));
 | 
			
		||||
        return response;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task<GetAccountBatchResponse> LookupAccountBatch(LookupAccountBatchRequest request, ServerCallContext context)
 | 
			
		||||
    public override async Task<GetAccountBatchResponse> LookupAccountBatch(LookupAccountBatchRequest request,
 | 
			
		||||
        ServerCallContext context)
 | 
			
		||||
    {
 | 
			
		||||
        var accountNames = request.Names.ToList();
 | 
			
		||||
        var accounts = await _db.Accounts
 | 
			
		||||
@@ -65,6 +77,14 @@ public class AccountServiceGrpc(
 | 
			
		||||
            .Where(a => accountNames.Contains(a.Name))
 | 
			
		||||
            .Include(a => a.Profile)
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
        
 | 
			
		||||
        var perks = await subscriptions.GetPerkSubscriptionsAsync(
 | 
			
		||||
            accounts.Select(x => x.Id).ToList()
 | 
			
		||||
        );
 | 
			
		||||
        foreach (var account in accounts)
 | 
			
		||||
            if (perks.TryGetValue(account.Id, out var perk))
 | 
			
		||||
                account.PerkSubscription = perk?.ToReference();
 | 
			
		||||
        
 | 
			
		||||
        var response = new GetAccountBatchResponse();
 | 
			
		||||
        response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue()));
 | 
			
		||||
        return response;
 | 
			
		||||
@@ -101,6 +121,13 @@ public class AccountServiceGrpc(
 | 
			
		||||
            .Include(a => a.Profile)
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
 | 
			
		||||
        var perks = await subscriptions.GetPerkSubscriptionsAsync(
 | 
			
		||||
            accounts.Select(x => x.Id).ToList()
 | 
			
		||||
        );
 | 
			
		||||
        foreach (var account in accounts)
 | 
			
		||||
            if (perks.TryGetValue(account.Id, out var perk))
 | 
			
		||||
                account.PerkSubscription = perk?.ToReference();
 | 
			
		||||
 | 
			
		||||
        var response = new ListAccountsResponse
 | 
			
		||||
        {
 | 
			
		||||
            TotalSize = totalCount,
 | 
			
		||||
 
 | 
			
		||||
@@ -176,6 +176,57 @@ public class Subscription : ModelBase
 | 
			
		||||
            return BasePrice;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Returns a reference object that contains a subset of subscription data
 | 
			
		||||
    /// suitable for client-side use, with sensitive information removed.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public SubscriptionReferenceObject ToReference()
 | 
			
		||||
    {
 | 
			
		||||
        return new SubscriptionReferenceObject
 | 
			
		||||
        {
 | 
			
		||||
            Id = Id,
 | 
			
		||||
            Identifier = Identifier,
 | 
			
		||||
            BegunAt = BegunAt,
 | 
			
		||||
            EndedAt = EndedAt,
 | 
			
		||||
            IsActive = IsActive,
 | 
			
		||||
            IsAvailable = IsAvailable,
 | 
			
		||||
            IsFreeTrial = IsFreeTrial,
 | 
			
		||||
            Status = Status,
 | 
			
		||||
            BasePrice = BasePrice,
 | 
			
		||||
            FinalPrice = FinalPrice,
 | 
			
		||||
            RenewalAt = RenewalAt,
 | 
			
		||||
            AccountId = AccountId
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// A reference object for Subscription that contains only non-sensitive information
 | 
			
		||||
/// suitable for client-side use.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class SubscriptionReferenceObject : ModelBase
 | 
			
		||||
{
 | 
			
		||||
    public Guid Id { get; set; }
 | 
			
		||||
    public string Identifier { get; set; } = null!;
 | 
			
		||||
    public Instant BegunAt { get; set; }
 | 
			
		||||
    public Instant? EndedAt { get; set; }
 | 
			
		||||
    public bool IsActive { get; set; }
 | 
			
		||||
    public bool IsAvailable { get; set; }
 | 
			
		||||
    public bool IsFreeTrial { get; set; }
 | 
			
		||||
    public SubscriptionStatus Status { get; set; }
 | 
			
		||||
    public decimal BasePrice { get; set; }
 | 
			
		||||
    public decimal FinalPrice { get; set; }
 | 
			
		||||
    public Instant? RenewalAt { get; set; }
 | 
			
		||||
    public Guid AccountId { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets the human-readable name of the subscription type if available.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotMapped]
 | 
			
		||||
    public string? DisplayName => SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(Identifier, out var name) 
 | 
			
		||||
        ? name 
 | 
			
		||||
        : null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class PaymentDetails
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
using System.Security.Cryptography;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using DysonNetwork.Pass.Localization;
 | 
			
		||||
using DysonNetwork.Pass.Wallet.PaymentHandlers;
 | 
			
		||||
@@ -377,7 +379,10 @@ public class SubscriptionService(
 | 
			
		||||
    public async Task<Subscription?> GetSubscriptionAsync(Guid accountId, params string[] identifiers)
 | 
			
		||||
    {
 | 
			
		||||
        // Create a unique cache key for this subscription
 | 
			
		||||
        var cacheKey = $"{SubscriptionCacheKeyPrefix}{accountId}:{string.Join(",", identifiers)}";
 | 
			
		||||
        var hashBytes = MD5.HashData(Encoding.UTF8.GetBytes(string.Join(",", identifiers)));
 | 
			
		||||
        var hashIdentifier = Convert.ToHexStringLower(hashBytes);
 | 
			
		||||
 | 
			
		||||
        var cacheKey = $"{SubscriptionCacheKeyPrefix}{accountId}:{hashIdentifier}";
 | 
			
		||||
 | 
			
		||||
        // Try to get the subscription from cache first
 | 
			
		||||
        var (found, cachedSubscription) = await cache.GetAsyncWithStatus<Subscription>(cacheKey);
 | 
			
		||||
@@ -398,4 +403,71 @@ public class SubscriptionService(
 | 
			
		||||
 | 
			
		||||
        return subscription;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private const string SubscriptionPerkCacheKeyPrefix = "subscription:perk:";
 | 
			
		||||
 | 
			
		||||
    private static readonly List<string> PerkIdentifiers =
 | 
			
		||||
        [SubscriptionType.Stellar, SubscriptionType.Nova, SubscriptionType.Supernova];
 | 
			
		||||
 | 
			
		||||
    public async Task<Subscription?> GetPerkSubscriptionAsync(Guid accountId)
 | 
			
		||||
    {
 | 
			
		||||
        var cacheKey = $"{SubscriptionPerkCacheKeyPrefix}{accountId}";
 | 
			
		||||
 | 
			
		||||
        // Try to get the subscription from cache first
 | 
			
		||||
        var (found, cachedSubscription) = await cache.GetAsyncWithStatus<Subscription>(cacheKey);
 | 
			
		||||
        if (found && cachedSubscription != null)
 | 
			
		||||
        {
 | 
			
		||||
            return cachedSubscription;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If not in cache, get from database
 | 
			
		||||
        var subscription = await db.WalletSubscriptions
 | 
			
		||||
            .Where(s => s.AccountId == accountId && PerkIdentifiers.Contains(s.Identifier))
 | 
			
		||||
            .OrderByDescending(s => s.BegunAt)
 | 
			
		||||
            .FirstOrDefaultAsync();
 | 
			
		||||
 | 
			
		||||
        // Cache the result if found (with 30 minutes expiry)
 | 
			
		||||
        if (subscription != null)
 | 
			
		||||
            await cache.SetAsync(cacheKey, subscription, TimeSpan.FromMinutes(30));
 | 
			
		||||
 | 
			
		||||
        return subscription;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public async Task<Dictionary<Guid, Subscription?>> GetPerkSubscriptionsAsync(List<Guid> accountIds)
 | 
			
		||||
    {
 | 
			
		||||
        var result = new Dictionary<Guid, Subscription?>();
 | 
			
		||||
        var missingAccountIds = new List<Guid>();
 | 
			
		||||
 | 
			
		||||
        // Try to get the subscription from cache first
 | 
			
		||||
        foreach (var accountId in accountIds)
 | 
			
		||||
        {
 | 
			
		||||
            var cacheKey = $"{SubscriptionPerkCacheKeyPrefix}{accountId}";
 | 
			
		||||
            var (found, cachedSubscription) = await cache.GetAsyncWithStatus<Subscription>(cacheKey);
 | 
			
		||||
            if (found && cachedSubscription != null)
 | 
			
		||||
                result[accountId] = cachedSubscription;
 | 
			
		||||
            else
 | 
			
		||||
                missingAccountIds.Add(accountId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (missingAccountIds.Count <= 0) return result;
 | 
			
		||||
 | 
			
		||||
        // If not in cache, get from database
 | 
			
		||||
        var subscriptions = await db.WalletSubscriptions
 | 
			
		||||
            .Where(s => missingAccountIds.Contains(s.AccountId))
 | 
			
		||||
            .Where(s => PerkIdentifiers.Contains(s.Identifier))
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
 | 
			
		||||
        // Group the subscriptions by account id
 | 
			
		||||
        foreach (var subscription in subscriptions)
 | 
			
		||||
        {
 | 
			
		||||
            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));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user