✨ Putting the stellar perks back
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using DysonNetwork.Pass.Wallet;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
@@ -31,6 +32,8 @@ public class Account : ModelBase
|
|||||||
|
|
||||||
[JsonIgnore] public ICollection<Relationship> OutgoingRelationships { get; set; } = new List<Relationship>();
|
[JsonIgnore] public ICollection<Relationship> OutgoingRelationships { get; set; } = new List<Relationship>();
|
||||||
[JsonIgnore] public ICollection<Relationship> IncomingRelationships { 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()
|
public Shared.Proto.Account ToProtoValue()
|
||||||
{
|
{
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Pass.Auth;
|
using DysonNetwork.Pass.Auth;
|
||||||
|
using DysonNetwork.Pass.Wallet;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
@@ -12,6 +13,7 @@ public class AccountController(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
AuthService auth,
|
AuthService auth,
|
||||||
AccountService accounts,
|
AccountService accounts,
|
||||||
|
SubscriptionService subscriptions,
|
||||||
AccountEventService events
|
AccountEventService events
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
@@ -25,7 +27,12 @@ public class AccountController(
|
|||||||
.Include(e => e.Profile)
|
.Include(e => e.Profile)
|
||||||
.Where(a => a.Name == name)
|
.Where(a => a.Name == name)
|
||||||
.FirstOrDefaultAsync();
|
.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")]
|
[HttpGet("{name}/badges")]
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Pass.Permission;
|
||||||
|
using DysonNetwork.Pass.Wallet;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -18,6 +19,7 @@ namespace DysonNetwork.Pass.Account;
|
|||||||
public class AccountCurrentController(
|
public class AccountCurrentController(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
AccountService accounts,
|
AccountService accounts,
|
||||||
|
SubscriptionService subscriptions,
|
||||||
AccountEventService events,
|
AccountEventService events,
|
||||||
AuthService auth,
|
AuthService auth,
|
||||||
FileService.FileServiceClient files,
|
FileService.FileServiceClient files,
|
||||||
@@ -36,6 +38,9 @@ public class AccountCurrentController(
|
|||||||
.Include(e => e.Profile)
|
.Include(e => e.Profile)
|
||||||
.Where(e => e.Id == userId)
|
.Where(e => e.Id == userId)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
var perk = await subscriptions.GetPerkSubscriptionAsync(account.Id);
|
||||||
|
account.PerkSubscription = perk?.ToReference();
|
||||||
|
|
||||||
return Ok(account);
|
return Ok(account);
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,16 @@
|
|||||||
|
using DysonNetwork.Pass.Wallet;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using Google.Protobuf.WellKnownTypes;
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Serialization.Protobuf;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Account;
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
public class AccountServiceGrpc(
|
public class AccountServiceGrpc(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
RelationshipService relationships,
|
RelationshipService relationships,
|
||||||
|
SubscriptionService subscriptions,
|
||||||
IClock clock,
|
IClock clock,
|
||||||
ILogger<AccountServiceGrpc> logger
|
ILogger<AccountServiceGrpc> logger
|
||||||
)
|
)
|
||||||
@@ -34,6 +35,9 @@ public class AccountServiceGrpc(
|
|||||||
if (account == null)
|
if (account == null)
|
||||||
throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, $"Account {request.Id} not found"));
|
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();
|
return account.ToProtoValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,13 +55,21 @@ public class AccountServiceGrpc(
|
|||||||
.Where(a => accountIds.Contains(a.Id))
|
.Where(a => accountIds.Contains(a.Id))
|
||||||
.Include(a => a.Profile)
|
.Include(a => a.Profile)
|
||||||
.ToListAsync();
|
.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();
|
var response = new GetAccountBatchResponse();
|
||||||
response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue()));
|
response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue()));
|
||||||
return response;
|
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 accountNames = request.Names.ToList();
|
||||||
var accounts = await _db.Accounts
|
var accounts = await _db.Accounts
|
||||||
@@ -65,6 +77,14 @@ public class AccountServiceGrpc(
|
|||||||
.Where(a => accountNames.Contains(a.Name))
|
.Where(a => accountNames.Contains(a.Name))
|
||||||
.Include(a => a.Profile)
|
.Include(a => a.Profile)
|
||||||
.ToListAsync();
|
.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();
|
var response = new GetAccountBatchResponse();
|
||||||
response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue()));
|
response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue()));
|
||||||
return response;
|
return response;
|
||||||
@@ -101,6 +121,13 @@ public class AccountServiceGrpc(
|
|||||||
.Include(a => a.Profile)
|
.Include(a => a.Profile)
|
||||||
.ToListAsync();
|
.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
|
var response = new ListAccountsResponse
|
||||||
{
|
{
|
||||||
TotalSize = totalCount,
|
TotalSize = totalCount,
|
||||||
|
@@ -176,6 +176,57 @@ public class Subscription : ModelBase
|
|||||||
return BasePrice;
|
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
|
public class PaymentDetails
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Pass.Localization;
|
using DysonNetwork.Pass.Localization;
|
||||||
using DysonNetwork.Pass.Wallet.PaymentHandlers;
|
using DysonNetwork.Pass.Wallet.PaymentHandlers;
|
||||||
@@ -377,7 +379,10 @@ public class SubscriptionService(
|
|||||||
public async Task<Subscription?> GetSubscriptionAsync(Guid accountId, params string[] identifiers)
|
public async Task<Subscription?> GetSubscriptionAsync(Guid accountId, params string[] identifiers)
|
||||||
{
|
{
|
||||||
// Create a unique cache key for this subscription
|
// 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
|
// Try to get the subscription from cache first
|
||||||
var (found, cachedSubscription) = await cache.GetAsyncWithStatus<Subscription>(cacheKey);
|
var (found, cachedSubscription) = await cache.GetAsyncWithStatus<Subscription>(cacheKey);
|
||||||
@@ -398,4 +403,71 @@ public class SubscriptionService(
|
|||||||
|
|
||||||
return subscription;
|
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