♻️ Subscription overhaul
This commit is contained in:
@@ -45,7 +45,7 @@ public class QuotaService(
|
||||
var basedQuota = 1L;
|
||||
if (perkSubscription != null)
|
||||
{
|
||||
var privilege = PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(perkSubscription.Identifier);
|
||||
var privilege = perkSubscription.PerkLevel;
|
||||
basedQuota = privilege switch
|
||||
{
|
||||
1 => 5L,
|
||||
|
||||
@@ -256,7 +256,7 @@ public class FileUploadController(
|
||||
|
||||
var privilege = currentUser.PerkSubscription is null
|
||||
? 0
|
||||
: PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(currentUser.PerkSubscription.Identifier);
|
||||
: currentUser.PerkSubscription.PerkLevel;
|
||||
|
||||
if (privilege < pool.PolicyConfig.RequirePrivilege)
|
||||
{
|
||||
|
||||
@@ -122,9 +122,7 @@ public class ThoughtController(
|
||||
return BadRequest("Sorry, SN-chan currently does not support requests with files attached.");
|
||||
|
||||
if (serviceInfo.PerkLevel > 0 && !currentUser.IsSuperuser)
|
||||
if (currentUser.PerkSubscription is null ||
|
||||
PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(currentUser.PerkSubscription.Identifier) <
|
||||
serviceInfo.PerkLevel)
|
||||
if (currentUser.PerkLevel < serviceInfo.PerkLevel)
|
||||
return StatusCode(403, "Not enough perk level");
|
||||
|
||||
var kernel = service.GetSnChanKernel();
|
||||
@@ -669,4 +667,4 @@ public class ThoughtController(
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning restore SKEXP0050
|
||||
#pragma warning restore SKEXP0050
|
||||
|
||||
@@ -181,7 +181,7 @@ public class AccountServiceGrpc(
|
||||
|
||||
var perk = SnWalletSubscription.FromProtoValue(subscription).ToReference();
|
||||
account.PerkSubscription = perk;
|
||||
account.PerkLevel = PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(perk.Identifier);
|
||||
account.PerkLevel = perk.PerkLevel;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -209,7 +209,7 @@ public class AccountServiceGrpc(
|
||||
if (subscriptionMap.TryGetValue(account.Id, out var perk))
|
||||
{
|
||||
account.PerkSubscription = perk;
|
||||
account.PerkLevel = PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(perk.Identifier);
|
||||
account.PerkLevel = perk.PerkLevel;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -486,7 +486,7 @@ public class AuthService(
|
||||
|
||||
var perk = SnWalletSubscription.FromProtoValue(subscription).ToReference();
|
||||
account.PerkSubscription = perk;
|
||||
account.PerkLevel = PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(perk.Identifier);
|
||||
account.PerkLevel = perk.PerkLevel;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -19,11 +19,11 @@ public class ExperienceService(AppDatabase db, RemoteSubscriptionService subscri
|
||||
var perkSubscription = await subscriptions.GetPerkSubscription(accountId);
|
||||
if (perkSubscription is not null)
|
||||
{
|
||||
record.BonusMultiplier = perkSubscription.Identifier switch
|
||||
record.BonusMultiplier = perkSubscription.PerkLevel switch
|
||||
{
|
||||
SubscriptionType.Stellar => 1.5,
|
||||
SubscriptionType.Nova => 2,
|
||||
SubscriptionType.Supernova => 2.5,
|
||||
1 => 1.5,
|
||||
2 => 2,
|
||||
3 => 2.5,
|
||||
_ => 1
|
||||
};
|
||||
if (record.Delta >= 0)
|
||||
|
||||
@@ -273,6 +273,11 @@ public abstract class SubscriptionPaymentMethod
|
||||
/// paddle.com
|
||||
/// </summary>
|
||||
public const string Paddle = "paddle";
|
||||
|
||||
/// <summary>
|
||||
/// Internal gift redemption marker.
|
||||
/// </summary>
|
||||
public const string Gift = "gift";
|
||||
}
|
||||
|
||||
public enum SubscriptionStatus
|
||||
@@ -313,6 +318,10 @@ public class SnWalletSubscription : ModelBase
|
||||
[MaxLength(4096)]
|
||||
public string Identifier { get; set; } = null!;
|
||||
|
||||
[MaxLength(4096)] public string? GroupIdentifier { get; set; }
|
||||
[MaxLength(4096)] public string? DisplayName { get; set; }
|
||||
public int PerkLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The field is used to override the activation status of the membership.
|
||||
/// Might be used for refund handling and other special cases.
|
||||
@@ -398,6 +407,9 @@ public class SnWalletSubscription : ModelBase
|
||||
{
|
||||
Id = Id,
|
||||
Identifier = Identifier,
|
||||
GroupIdentifier = GroupIdentifier,
|
||||
DisplayName = DisplayName,
|
||||
PerkLevel = PerkLevel,
|
||||
BegunAt = BegunAt,
|
||||
EndedAt = EndedAt,
|
||||
IsActive = IsActive,
|
||||
@@ -419,6 +431,9 @@ public class SnWalletSubscription : ModelBase
|
||||
BegunAt = BegunAt.ToTimestamp(),
|
||||
EndedAt = EndedAt?.ToTimestamp(),
|
||||
Identifier = Identifier,
|
||||
GroupIdentifier = GroupIdentifier ?? string.Empty,
|
||||
DisplayName = DisplayName ?? string.Empty,
|
||||
PerkLevel = PerkLevel,
|
||||
IsActive = IsActive,
|
||||
IsFreeTrial = IsFreeTrial,
|
||||
Status = (DySubscriptionStatus)Status,
|
||||
@@ -452,6 +467,9 @@ public class SnWalletSubscription : ModelBase
|
||||
BegunAt = proto.BegunAt.ToInstant(),
|
||||
EndedAt = proto.EndedAt?.ToInstant(),
|
||||
Identifier = proto.Identifier,
|
||||
GroupIdentifier = string.IsNullOrWhiteSpace(proto.GroupIdentifier) ? null : proto.GroupIdentifier,
|
||||
DisplayName = string.IsNullOrWhiteSpace(proto.DisplayName) ? null : proto.DisplayName,
|
||||
PerkLevel = proto.PerkLevel,
|
||||
IsActive = proto.IsActive,
|
||||
IsFreeTrial = proto.IsFreeTrial,
|
||||
Status = (SubscriptionStatus)proto.Status,
|
||||
@@ -475,6 +493,9 @@ public class SnSubscriptionReferenceObject : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Identifier { get; set; } = null!;
|
||||
public string? GroupIdentifier { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
public int PerkLevel { get; set; }
|
||||
public Instant BegunAt { get; set; }
|
||||
public Instant? EndedAt { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
@@ -489,13 +510,13 @@ public class SnSubscriptionReferenceObject : ModelBase
|
||||
/// <summary>
|
||||
/// Gets the human-readable name of the subscription type if available (cached for performance).
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public string? DisplayName => field ??= SubscriptionTypeData.SubscriptionHumanReadable.GetValueOrDefault(Identifier);
|
||||
|
||||
public DySubscriptionReferenceObject ToProtoValue() => new()
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
Identifier = Identifier,
|
||||
GroupIdentifier = GroupIdentifier ?? string.Empty,
|
||||
DisplayName = DisplayName ?? string.Empty,
|
||||
PerkLevel = PerkLevel,
|
||||
BegunAt = BegunAt.ToTimestamp(),
|
||||
EndedAt = EndedAt?.ToTimestamp(),
|
||||
IsActive = IsActive,
|
||||
@@ -506,7 +527,6 @@ public class SnSubscriptionReferenceObject : ModelBase
|
||||
FinalPrice = FinalPrice.ToString(CultureInfo.InvariantCulture),
|
||||
RenewalAt = RenewalAt?.ToTimestamp(),
|
||||
AccountId = AccountId.ToString(),
|
||||
DisplayName = DisplayName,
|
||||
CreatedAt = CreatedAt.ToTimestamp(),
|
||||
UpdatedAt = UpdatedAt.ToTimestamp()
|
||||
};
|
||||
@@ -515,6 +535,9 @@ public class SnSubscriptionReferenceObject : ModelBase
|
||||
{
|
||||
Id = Guid.Parse(proto.Id),
|
||||
Identifier = proto.Identifier,
|
||||
GroupIdentifier = string.IsNullOrWhiteSpace(proto.GroupIdentifier) ? null : proto.GroupIdentifier,
|
||||
DisplayName = string.IsNullOrWhiteSpace(proto.DisplayName) ? null : proto.DisplayName,
|
||||
PerkLevel = proto.PerkLevel,
|
||||
BegunAt = proto.BegunAt.ToInstant(),
|
||||
EndedAt = proto.EndedAt?.ToInstant(),
|
||||
IsActive = proto.IsActive,
|
||||
|
||||
126
DysonNetwork.Shared/Models/SubscriptionCatalog.cs
Normal file
126
DysonNetwork.Shared/Models/SubscriptionCatalog.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
[Index(nameof(Identifier), IsUnique = true)]
|
||||
public class SnWalletSubscriptionDefinition : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[MaxLength(4096)] public string Identifier { get; set; } = null!;
|
||||
[MaxLength(4096)] public string? GroupIdentifier { get; set; }
|
||||
[MaxLength(4096)] public string DisplayName { get; set; } = null!;
|
||||
[MaxLength(128)] public string Currency { get; set; } = null!;
|
||||
public decimal BasePrice { get; set; }
|
||||
public int PerkLevel { get; set; }
|
||||
public int? MinimumAccountLevel { get; set; }
|
||||
public decimal? ExperienceMultiplier { get; set; }
|
||||
public int? GoldenPointReward { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")] public SubscriptionPaymentPolicy PaymentPolicy { get; set; } = new();
|
||||
[Column(TypeName = "jsonb")] public SubscriptionGiftPolicy? GiftPolicy { get; set; }
|
||||
[Column(TypeName = "jsonb")] public Dictionary<string, List<string>> ProviderMappings { get; set; } = new();
|
||||
|
||||
public bool IsPaymentMethodAllowed(string paymentMethod)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(paymentMethod)) return false;
|
||||
|
||||
var normalized = paymentMethod.Trim().ToLowerInvariant();
|
||||
if (PaymentPolicy.AllowedMethods.Count > 0 &&
|
||||
!PaymentPolicy.AllowedMethods.Any(m => string.Equals(m, normalized, StringComparison.OrdinalIgnoreCase)))
|
||||
return false;
|
||||
|
||||
if (normalized == SubscriptionPaymentMethod.InAppWallet)
|
||||
return PaymentPolicy.AllowInternalWallet;
|
||||
if (normalized == SubscriptionPaymentMethod.Gift)
|
||||
return PaymentPolicy.AllowedMethods.Count == 0 ||
|
||||
PaymentPolicy.AllowedMethods.Any(m => string.Equals(m, normalized, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return PaymentPolicy.AllowExternal;
|
||||
}
|
||||
}
|
||||
|
||||
public class SnWalletSubscriptionCatalogSettings : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Column(TypeName = "jsonb")] public SubscriptionGiftPolicy GiftPolicyDefaults { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SubscriptionPaymentPolicy
|
||||
{
|
||||
public bool AllowInternalWallet { get; set; } = true;
|
||||
public bool AllowExternal { get; set; } = true;
|
||||
public bool AllowInternalWalletRenewal { get; set; } = false;
|
||||
public List<string> AllowedMethods { get; set; } = [];
|
||||
}
|
||||
|
||||
public class SubscriptionGiftPolicy
|
||||
{
|
||||
public bool AllowPurchase { get; set; } = true;
|
||||
public int? MinimumAccountLevel { get; set; }
|
||||
public bool AllowPerkSubscriptionBypass { get; set; } = true;
|
||||
public int? RollingPurchaseLimit { get; set; }
|
||||
public int? RollingWindowDays { get; set; }
|
||||
public int? GiftDurationDays { get; set; }
|
||||
public int? SubscriptionDurationDays { get; set; }
|
||||
|
||||
public SubscriptionGiftPolicy Merge(SubscriptionGiftPolicy? overrides)
|
||||
{
|
||||
if (overrides is null) return this.Clone();
|
||||
|
||||
return new SubscriptionGiftPolicy
|
||||
{
|
||||
AllowPurchase = overrides.AllowPurchase,
|
||||
MinimumAccountLevel = overrides.MinimumAccountLevel ?? MinimumAccountLevel,
|
||||
AllowPerkSubscriptionBypass = overrides.AllowPerkSubscriptionBypass,
|
||||
RollingPurchaseLimit = overrides.RollingPurchaseLimit ?? RollingPurchaseLimit,
|
||||
RollingWindowDays = overrides.RollingWindowDays ?? RollingWindowDays,
|
||||
GiftDurationDays = overrides.GiftDurationDays ?? GiftDurationDays,
|
||||
SubscriptionDurationDays = overrides.SubscriptionDurationDays ?? SubscriptionDurationDays
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class SubscriptionCatalogPolicyExtensions
|
||||
{
|
||||
public static SubscriptionGiftPolicy Clone(this SubscriptionGiftPolicy policy) => new()
|
||||
{
|
||||
AllowPurchase = policy.AllowPurchase,
|
||||
MinimumAccountLevel = policy.MinimumAccountLevel,
|
||||
AllowPerkSubscriptionBypass = policy.AllowPerkSubscriptionBypass,
|
||||
RollingPurchaseLimit = policy.RollingPurchaseLimit,
|
||||
RollingWindowDays = policy.RollingWindowDays,
|
||||
GiftDurationDays = policy.GiftDurationDays,
|
||||
SubscriptionDurationDays = policy.SubscriptionDurationDays
|
||||
};
|
||||
}
|
||||
|
||||
public class SubscriptionCatalogSeedOptions
|
||||
{
|
||||
public SubscriptionCatalogSeedSettings Settings { get; set; } = new();
|
||||
public List<SubscriptionCatalogSeedDefinition> Definitions { get; set; } = [];
|
||||
}
|
||||
|
||||
public class SubscriptionCatalogSeedSettings
|
||||
{
|
||||
public SubscriptionGiftPolicy GiftPolicyDefaults { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SubscriptionCatalogSeedDefinition
|
||||
{
|
||||
public string Identifier { get; set; } = null!;
|
||||
public string? GroupIdentifier { get; set; }
|
||||
public string DisplayName { get; set; } = null!;
|
||||
public string Currency { get; set; } = null!;
|
||||
public decimal BasePrice { get; set; }
|
||||
public int PerkLevel { get; set; }
|
||||
public int? MinimumAccountLevel { get; set; }
|
||||
public decimal? ExperienceMultiplier { get; set; }
|
||||
public int? GoldenPointReward { get; set; }
|
||||
public SubscriptionPaymentPolicy PaymentPolicy { get; set; } = new();
|
||||
public SubscriptionGiftPolicy? GiftPolicy { get; set; }
|
||||
public Dictionary<string, List<string>> ProviderMappings { get; set; } = new();
|
||||
}
|
||||
@@ -1880,6 +1880,9 @@ namespace DysonNetwork.Shared.Proto {
|
||||
begunAt_ = other.begunAt_ != null ? other.begunAt_.Clone() : null;
|
||||
endedAt_ = other.endedAt_ != null ? other.endedAt_.Clone() : null;
|
||||
identifier_ = other.identifier_;
|
||||
groupIdentifier_ = other.groupIdentifier_;
|
||||
displayName_ = other.displayName_;
|
||||
perkLevel_ = other.perkLevel_;
|
||||
isActive_ = other.isActive_;
|
||||
isFreeTrial_ = other.isFreeTrial_;
|
||||
status_ = other.status_;
|
||||
@@ -1951,6 +1954,42 @@ namespace DysonNetwork.Shared.Proto {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Field number for the "group_identifier" field.</summary>
|
||||
public const int GroupIdentifierFieldNumber = 19;
|
||||
private string groupIdentifier_ = "";
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public string GroupIdentifier {
|
||||
get { return groupIdentifier_; }
|
||||
set {
|
||||
groupIdentifier_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Field number for the "display_name" field.</summary>
|
||||
public const int DisplayNameFieldNumber = 20;
|
||||
private string displayName_ = "";
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public string DisplayName {
|
||||
get { return displayName_; }
|
||||
set {
|
||||
displayName_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Field number for the "perk_level" field.</summary>
|
||||
public const int PerkLevelFieldNumber = 21;
|
||||
private int perkLevel_;
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public int PerkLevel {
|
||||
get { return perkLevel_; }
|
||||
set {
|
||||
perkLevel_ = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Field number for the "is_active" field.</summary>
|
||||
public const int IsActiveFieldNumber = 5;
|
||||
private bool isActive_;
|
||||
@@ -2152,6 +2191,9 @@ namespace DysonNetwork.Shared.Proto {
|
||||
if (!object.Equals(BegunAt, other.BegunAt)) return false;
|
||||
if (!object.Equals(EndedAt, other.EndedAt)) return false;
|
||||
if (Identifier != other.Identifier) return false;
|
||||
if (GroupIdentifier != other.GroupIdentifier) return false;
|
||||
if (DisplayName != other.DisplayName) return false;
|
||||
if (PerkLevel != other.PerkLevel) return false;
|
||||
if (IsActive != other.IsActive) return false;
|
||||
if (IsFreeTrial != other.IsFreeTrial) return false;
|
||||
if (Status != other.Status) return false;
|
||||
@@ -2177,6 +2219,9 @@ namespace DysonNetwork.Shared.Proto {
|
||||
if (begunAt_ != null) hash ^= BegunAt.GetHashCode();
|
||||
if (endedAt_ != null) hash ^= EndedAt.GetHashCode();
|
||||
if (Identifier.Length != 0) hash ^= Identifier.GetHashCode();
|
||||
if (GroupIdentifier.Length != 0) hash ^= GroupIdentifier.GetHashCode();
|
||||
if (DisplayName.Length != 0) hash ^= DisplayName.GetHashCode();
|
||||
if (PerkLevel != 0) hash ^= PerkLevel.GetHashCode();
|
||||
if (IsActive != false) hash ^= IsActive.GetHashCode();
|
||||
if (IsFreeTrial != false) hash ^= IsFreeTrial.GetHashCode();
|
||||
if (Status != global::DysonNetwork.Shared.Proto.DySubscriptionStatus.Unspecified) hash ^= Status.GetHashCode();
|
||||
@@ -2225,6 +2270,18 @@ namespace DysonNetwork.Shared.Proto {
|
||||
output.WriteRawTag(34);
|
||||
output.WriteString(Identifier);
|
||||
}
|
||||
if (GroupIdentifier.Length != 0) {
|
||||
output.WriteRawTag(154, 1);
|
||||
output.WriteString(GroupIdentifier);
|
||||
}
|
||||
if (DisplayName.Length != 0) {
|
||||
output.WriteRawTag(162, 1);
|
||||
output.WriteString(DisplayName);
|
||||
}
|
||||
if (PerkLevel != 0) {
|
||||
output.WriteRawTag(168, 1);
|
||||
output.WriteInt32(PerkLevel);
|
||||
}
|
||||
if (IsActive != false) {
|
||||
output.WriteRawTag(40);
|
||||
output.WriteBool(IsActive);
|
||||
@@ -2307,6 +2364,18 @@ namespace DysonNetwork.Shared.Proto {
|
||||
output.WriteRawTag(34);
|
||||
output.WriteString(Identifier);
|
||||
}
|
||||
if (GroupIdentifier.Length != 0) {
|
||||
output.WriteRawTag(154, 1);
|
||||
output.WriteString(GroupIdentifier);
|
||||
}
|
||||
if (DisplayName.Length != 0) {
|
||||
output.WriteRawTag(162, 1);
|
||||
output.WriteString(DisplayName);
|
||||
}
|
||||
if (PerkLevel != 0) {
|
||||
output.WriteRawTag(168, 1);
|
||||
output.WriteInt32(PerkLevel);
|
||||
}
|
||||
if (IsActive != false) {
|
||||
output.WriteRawTag(40);
|
||||
output.WriteBool(IsActive);
|
||||
@@ -2385,6 +2454,15 @@ namespace DysonNetwork.Shared.Proto {
|
||||
if (Identifier.Length != 0) {
|
||||
size += 1 + pb::CodedOutputStream.ComputeStringSize(Identifier);
|
||||
}
|
||||
if (GroupIdentifier.Length != 0) {
|
||||
size += 2 + pb::CodedOutputStream.ComputeStringSize(GroupIdentifier);
|
||||
}
|
||||
if (DisplayName.Length != 0) {
|
||||
size += 2 + pb::CodedOutputStream.ComputeStringSize(DisplayName);
|
||||
}
|
||||
if (PerkLevel != 0) {
|
||||
size += 2 + pb::CodedOutputStream.ComputeInt32Size(PerkLevel);
|
||||
}
|
||||
if (IsActive != false) {
|
||||
size += 1 + 1;
|
||||
}
|
||||
@@ -2457,6 +2535,15 @@ namespace DysonNetwork.Shared.Proto {
|
||||
if (other.Identifier.Length != 0) {
|
||||
Identifier = other.Identifier;
|
||||
}
|
||||
if (other.GroupIdentifier.Length != 0) {
|
||||
GroupIdentifier = other.GroupIdentifier;
|
||||
}
|
||||
if (other.DisplayName.Length != 0) {
|
||||
DisplayName = other.DisplayName;
|
||||
}
|
||||
if (other.PerkLevel != 0) {
|
||||
PerkLevel = other.PerkLevel;
|
||||
}
|
||||
if (other.IsActive != false) {
|
||||
IsActive = other.IsActive;
|
||||
}
|
||||
@@ -2555,6 +2642,18 @@ namespace DysonNetwork.Shared.Proto {
|
||||
Identifier = input.ReadString();
|
||||
break;
|
||||
}
|
||||
case 154: {
|
||||
GroupIdentifier = input.ReadString();
|
||||
break;
|
||||
}
|
||||
case 162: {
|
||||
DisplayName = input.ReadString();
|
||||
break;
|
||||
}
|
||||
case 168: {
|
||||
PerkLevel = input.ReadInt32();
|
||||
break;
|
||||
}
|
||||
case 40: {
|
||||
IsActive = input.ReadBool();
|
||||
break;
|
||||
@@ -2667,6 +2766,18 @@ namespace DysonNetwork.Shared.Proto {
|
||||
Identifier = input.ReadString();
|
||||
break;
|
||||
}
|
||||
case 154: {
|
||||
GroupIdentifier = input.ReadString();
|
||||
break;
|
||||
}
|
||||
case 162: {
|
||||
DisplayName = input.ReadString();
|
||||
break;
|
||||
}
|
||||
case 168: {
|
||||
PerkLevel = input.ReadInt32();
|
||||
break;
|
||||
}
|
||||
case 40: {
|
||||
IsActive = input.ReadBool();
|
||||
break;
|
||||
@@ -2782,6 +2893,8 @@ namespace DysonNetwork.Shared.Proto {
|
||||
public DySubscriptionReferenceObject(DySubscriptionReferenceObject other) : this() {
|
||||
id_ = other.id_;
|
||||
identifier_ = other.identifier_;
|
||||
groupIdentifier_ = other.groupIdentifier_;
|
||||
perkLevel_ = other.perkLevel_;
|
||||
begunAt_ = other.begunAt_ != null ? other.begunAt_.Clone() : null;
|
||||
endedAt_ = other.endedAt_ != null ? other.endedAt_.Clone() : null;
|
||||
isActive_ = other.isActive_;
|
||||
@@ -2828,6 +2941,30 @@ namespace DysonNetwork.Shared.Proto {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Field number for the "group_identifier" field.</summary>
|
||||
public const int GroupIdentifierFieldNumber = 16;
|
||||
private string groupIdentifier_ = "";
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public string GroupIdentifier {
|
||||
get { return groupIdentifier_; }
|
||||
set {
|
||||
groupIdentifier_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Field number for the "perk_level" field.</summary>
|
||||
public const int PerkLevelFieldNumber = 17;
|
||||
private int perkLevel_;
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public int PerkLevel {
|
||||
get { return perkLevel_; }
|
||||
set {
|
||||
perkLevel_ = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Field number for the "begun_at" field.</summary>
|
||||
public const int BegunAtFieldNumber = 3;
|
||||
private global::Google.Protobuf.WellKnownTypes.Timestamp begunAt_;
|
||||
@@ -3015,6 +3152,8 @@ namespace DysonNetwork.Shared.Proto {
|
||||
}
|
||||
if (Id != other.Id) return false;
|
||||
if (Identifier != other.Identifier) return false;
|
||||
if (GroupIdentifier != other.GroupIdentifier) return false;
|
||||
if (PerkLevel != other.PerkLevel) return false;
|
||||
if (!object.Equals(BegunAt, other.BegunAt)) return false;
|
||||
if (!object.Equals(EndedAt, other.EndedAt)) return false;
|
||||
if (IsActive != other.IsActive) return false;
|
||||
@@ -3037,6 +3176,8 @@ namespace DysonNetwork.Shared.Proto {
|
||||
int hash = 1;
|
||||
if (Id.Length != 0) hash ^= Id.GetHashCode();
|
||||
if (Identifier.Length != 0) hash ^= Identifier.GetHashCode();
|
||||
if (GroupIdentifier.Length != 0) hash ^= GroupIdentifier.GetHashCode();
|
||||
if (PerkLevel != 0) hash ^= PerkLevel.GetHashCode();
|
||||
if (begunAt_ != null) hash ^= BegunAt.GetHashCode();
|
||||
if (endedAt_ != null) hash ^= EndedAt.GetHashCode();
|
||||
if (IsActive != false) hash ^= IsActive.GetHashCode();
|
||||
@@ -3076,6 +3217,14 @@ namespace DysonNetwork.Shared.Proto {
|
||||
output.WriteRawTag(18);
|
||||
output.WriteString(Identifier);
|
||||
}
|
||||
if (GroupIdentifier.Length != 0) {
|
||||
output.WriteRawTag(130, 1);
|
||||
output.WriteString(GroupIdentifier);
|
||||
}
|
||||
if (PerkLevel != 0) {
|
||||
output.WriteRawTag(136, 1);
|
||||
output.WriteInt32(PerkLevel);
|
||||
}
|
||||
if (begunAt_ != null) {
|
||||
output.WriteRawTag(26);
|
||||
output.WriteMessage(BegunAt);
|
||||
@@ -3146,6 +3295,14 @@ namespace DysonNetwork.Shared.Proto {
|
||||
output.WriteRawTag(18);
|
||||
output.WriteString(Identifier);
|
||||
}
|
||||
if (GroupIdentifier.Length != 0) {
|
||||
output.WriteRawTag(130, 1);
|
||||
output.WriteString(GroupIdentifier);
|
||||
}
|
||||
if (PerkLevel != 0) {
|
||||
output.WriteRawTag(136, 1);
|
||||
output.WriteInt32(PerkLevel);
|
||||
}
|
||||
if (begunAt_ != null) {
|
||||
output.WriteRawTag(26);
|
||||
output.WriteMessage(BegunAt);
|
||||
@@ -3214,6 +3371,12 @@ namespace DysonNetwork.Shared.Proto {
|
||||
if (Identifier.Length != 0) {
|
||||
size += 1 + pb::CodedOutputStream.ComputeStringSize(Identifier);
|
||||
}
|
||||
if (GroupIdentifier.Length != 0) {
|
||||
size += 2 + pb::CodedOutputStream.ComputeStringSize(GroupIdentifier);
|
||||
}
|
||||
if (PerkLevel != 0) {
|
||||
size += 2 + pb::CodedOutputStream.ComputeInt32Size(PerkLevel);
|
||||
}
|
||||
if (begunAt_ != null) {
|
||||
size += 1 + pb::CodedOutputStream.ComputeMessageSize(BegunAt);
|
||||
}
|
||||
@@ -3271,6 +3434,12 @@ namespace DysonNetwork.Shared.Proto {
|
||||
if (other.Identifier.Length != 0) {
|
||||
Identifier = other.Identifier;
|
||||
}
|
||||
if (other.GroupIdentifier.Length != 0) {
|
||||
GroupIdentifier = other.GroupIdentifier;
|
||||
}
|
||||
if (other.PerkLevel != 0) {
|
||||
PerkLevel = other.PerkLevel;
|
||||
}
|
||||
if (other.begunAt_ != null) {
|
||||
if (begunAt_ == null) {
|
||||
BegunAt = new global::Google.Protobuf.WellKnownTypes.Timestamp();
|
||||
@@ -3352,6 +3521,14 @@ namespace DysonNetwork.Shared.Proto {
|
||||
Identifier = input.ReadString();
|
||||
break;
|
||||
}
|
||||
case 130: {
|
||||
GroupIdentifier = input.ReadString();
|
||||
break;
|
||||
}
|
||||
case 136: {
|
||||
PerkLevel = input.ReadInt32();
|
||||
break;
|
||||
}
|
||||
case 26: {
|
||||
if (begunAt_ == null) {
|
||||
BegunAt = new global::Google.Protobuf.WellKnownTypes.Timestamp();
|
||||
@@ -3446,6 +3623,14 @@ namespace DysonNetwork.Shared.Proto {
|
||||
Identifier = input.ReadString();
|
||||
break;
|
||||
}
|
||||
case 130: {
|
||||
GroupIdentifier = input.ReadString();
|
||||
break;
|
||||
}
|
||||
case 136: {
|
||||
PerkLevel = input.ReadInt32();
|
||||
break;
|
||||
}
|
||||
case 26: {
|
||||
if (begunAt_ == null) {
|
||||
BegunAt = new global::Google.Protobuf.WellKnownTypes.Timestamp();
|
||||
|
||||
@@ -22,6 +22,8 @@ public class AppDatabase(
|
||||
public DbSet<SnWalletFund> WalletFunds { get; set; } = null!;
|
||||
public DbSet<SnWalletFundRecipient> WalletFundRecipients { get; set; } = null!;
|
||||
public DbSet<SnWalletSubscription> WalletSubscriptions { get; set; } = null!;
|
||||
public DbSet<SnWalletSubscriptionDefinition> WalletSubscriptionDefinitions { get; set; } = null!;
|
||||
public DbSet<SnWalletSubscriptionCatalogSettings> WalletSubscriptionCatalogSettings { get; set; } = null!;
|
||||
public DbSet<SnWalletGift> WalletGifts { get; set; } = null!;
|
||||
public DbSet<SnWalletCoupon> WalletCoupons { get; set; } = null!;
|
||||
|
||||
|
||||
956
DysonNetwork.Wallet/Migrations/20260314112018_AddSubscriptionCatalog.Designer.cs
generated
Normal file
956
DysonNetwork.Wallet/Migrations/20260314112018_AddSubscriptionCatalog.Designer.cs
generated
Normal file
@@ -0,0 +1,956 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Wallet;
|
||||
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.Wallet.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20260314112018_AddSubscriptionCatalog")]
|
||||
partial class AddSubscriptionCatalog
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnLottery", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Instant?>("DrawDate")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("draw_date");
|
||||
|
||||
b.Property<int>("DrawStatus")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("draw_status");
|
||||
|
||||
b.PrimitiveCollection<string>("MatchedRegionOneNumbers")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("matched_region_one_numbers");
|
||||
|
||||
b.Property<int?>("MatchedRegionTwoNumber")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("matched_region_two_number");
|
||||
|
||||
b.Property<int>("Multiplier")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("multiplier");
|
||||
|
||||
b.PrimitiveCollection<string>("RegionOneNumbers")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("region_one_numbers");
|
||||
|
||||
b.Property<int>("RegionTwoNumber")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("region_two_number");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_lotteries");
|
||||
|
||||
b.ToTable("lotteries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnLotteryRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Instant>("DrawDate")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("draw_date");
|
||||
|
||||
b.Property<long>("TotalPrizeAmount")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("total_prize_amount");
|
||||
|
||||
b.Property<int>("TotalPrizesAwarded")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("total_prizes_awarded");
|
||||
|
||||
b.Property<int>("TotalTickets")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("total_tickets");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.PrimitiveCollection<string>("WinningRegionOneNumbers")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("winning_region_one_numbers");
|
||||
|
||||
b.Property<int>("WinningRegionTwoNumber")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("winning_region_two_number");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_lottery_records");
|
||||
|
||||
b.ToTable("lottery_records", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_wallets");
|
||||
|
||||
b.ToTable("wallets", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletCoupon", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant?>("AffectedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("affected_at");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("code");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<decimal?>("DiscountAmount")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("discount_amount");
|
||||
|
||||
b.Property<double?>("DiscountRate")
|
||||
.HasColumnType("double precision")
|
||||
.HasColumnName("discount_rate");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Identifier")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("identifier");
|
||||
|
||||
b.Property<int?>("MaxUsage")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("max_usage");
|
||||
|
||||
b.Property<Instant>("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.SnWalletFund", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<int>("AmountOfSplits")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("amount_of_splits");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Guid>("CreatorAccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("creator_account_id");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("currency");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Instant>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<bool>("IsOpen")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_open");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<decimal>("RemainingAmount")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("remaining_amount");
|
||||
|
||||
b.Property<int>("SplitType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("split_type");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<decimal>("TotalAmount")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("total_amount");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_wallet_funds");
|
||||
|
||||
b.ToTable("wallet_funds", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletFundRecipient", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("amount");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Guid>("FundId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("fund_id");
|
||||
|
||||
b.Property<bool>("IsReceived")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_received");
|
||||
|
||||
b.Property<Instant?>("ReceivedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("received_at");
|
||||
|
||||
b.Property<Guid>("RecipientAccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("recipient_account_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_wallet_fund_recipients");
|
||||
|
||||
b.HasIndex("FundId")
|
||||
.HasDatabaseName("ix_wallet_fund_recipients_fund_id");
|
||||
|
||||
b.ToTable("wallet_fund_recipients", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletGift", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<decimal>("BasePrice")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("base_price");
|
||||
|
||||
b.Property<Guid?>("CouponId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("coupon_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Instant>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires_at");
|
||||
|
||||
b.Property<decimal>("FinalPrice")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("final_price");
|
||||
|
||||
b.Property<string>("GiftCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("gift_code");
|
||||
|
||||
b.Property<Guid>("GifterId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("gifter_id");
|
||||
|
||||
b.Property<bool>("IsOpenGift")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_open_gift");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<SnPaymentDetails>("PaymentDetails")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("payment_details");
|
||||
|
||||
b.Property<string>("PaymentMethod")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("payment_method");
|
||||
|
||||
b.Property<Guid?>("RecipientId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("recipient_id");
|
||||
|
||||
b.Property<Instant?>("RedeemedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("redeemed_at");
|
||||
|
||||
b.Property<Guid?>("RedeemerId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("redeemer_id");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<Guid?>("SubscriptionId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("subscription_id");
|
||||
|
||||
b.Property<string>("SubscriptionIdentifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("subscription_identifier");
|
||||
|
||||
b.Property<Instant>("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("SubscriptionId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_wallet_gifts_subscription_id");
|
||||
|
||||
b.ToTable("wallet_gifts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletOrder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("amount");
|
||||
|
||||
b.Property<string>("AppIdentifier")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("app_identifier");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("currency");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Instant>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Meta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("meta");
|
||||
|
||||
b.Property<Guid?>("PayeeWalletId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("payee_wallet_id");
|
||||
|
||||
b.Property<string>("ProductIdentifier")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("product_identifier");
|
||||
|
||||
b.Property<string>("Remarks")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("remarks");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<Guid?>("TransactionId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("transaction_id");
|
||||
|
||||
b.Property<Instant>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("amount");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("currency");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<decimal>("BasePrice")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("base_price");
|
||||
|
||||
b.Property<Instant>("BegunAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("begun_at");
|
||||
|
||||
b.Property<Guid?>("CouponId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("coupon_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<Instant?>("EndedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("ended_at");
|
||||
|
||||
b.Property<string>("GroupIdentifier")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("group_identifier");
|
||||
|
||||
b.Property<string>("Identifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("identifier");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_active");
|
||||
|
||||
b.Property<bool>("IsFreeTrial")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_free_trial");
|
||||
|
||||
b.Property<SnPaymentDetails>("PaymentDetails")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("payment_details");
|
||||
|
||||
b.Property<string>("PaymentMethod")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("payment_method");
|
||||
|
||||
b.Property<int>("PerkLevel")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("perk_level");
|
||||
|
||||
b.Property<Instant?>("RenewalAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("renewal_at");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_wallet_subscriptions");
|
||||
|
||||
b.HasIndex("AccountId")
|
||||
.HasDatabaseName("ix_wallet_subscriptions_account_id");
|
||||
|
||||
b.HasIndex("CouponId")
|
||||
.HasDatabaseName("ix_wallet_subscriptions_coupon_id");
|
||||
|
||||
b.HasIndex("Identifier")
|
||||
.HasDatabaseName("ix_wallet_subscriptions_identifier");
|
||||
|
||||
b.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.SnWalletSubscriptionCatalogSettings", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<SubscriptionGiftPolicy>("GiftPolicyDefaults")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("gift_policy_defaults");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_wallet_subscription_catalog_settings");
|
||||
|
||||
b.ToTable("wallet_subscription_catalog_settings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletSubscriptionDefinition", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<decimal>("BasePrice")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("base_price");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("currency");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<decimal?>("ExperienceMultiplier")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("experience_multiplier");
|
||||
|
||||
b.Property<SubscriptionGiftPolicy>("GiftPolicy")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("gift_policy");
|
||||
|
||||
b.Property<int?>("GoldenPointReward")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("golden_point_reward");
|
||||
|
||||
b.Property<string>("GroupIdentifier")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("group_identifier");
|
||||
|
||||
b.Property<string>("Identifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("identifier");
|
||||
|
||||
b.Property<int?>("MinimumAccountLevel")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("minimum_account_level");
|
||||
|
||||
b.Property<SubscriptionPaymentPolicy>("PaymentPolicy")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("payment_policy");
|
||||
|
||||
b.Property<int>("PerkLevel")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("perk_level");
|
||||
|
||||
b.Property<Dictionary<string, List<string>>>("ProviderMappings")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("provider_mappings");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_wallet_subscription_definitions");
|
||||
|
||||
b.HasIndex("Identifier")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_wallet_subscription_definitions_identifier");
|
||||
|
||||
b.ToTable("wallet_subscription_definitions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletTransaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("amount");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("currency");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Guid?>("PayeeWalletId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("payee_wallet_id");
|
||||
|
||||
b.Property<Guid?>("PayerWalletId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("payer_wallet_id");
|
||||
|
||||
b.Property<string>("Remarks")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("remarks");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("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.SnWalletFundRecipient", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnWalletFund", "Fund")
|
||||
.WithMany("Recipients")
|
||||
.HasForeignKey("FundId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_wallet_fund_recipients_wallet_funds_fund_id");
|
||||
|
||||
b.Navigation("Fund");
|
||||
});
|
||||
|
||||
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.SnWalletSubscription", "Subscription")
|
||||
.WithOne("Gift")
|
||||
.HasForeignKey("DysonNetwork.Shared.Models.SnWalletGift", "SubscriptionId")
|
||||
.HasConstraintName("fk_wallet_gifts_wallet_subscriptions_subscription_id");
|
||||
|
||||
b.Navigation("Coupon");
|
||||
|
||||
b.Navigation("Subscription");
|
||||
});
|
||||
|
||||
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.SnWalletCoupon", "Coupon")
|
||||
.WithMany()
|
||||
.HasForeignKey("CouponId")
|
||||
.HasConstraintName("fk_wallet_subscriptions_wallet_coupons_coupon_id");
|
||||
|
||||
b.Navigation("Coupon");
|
||||
});
|
||||
|
||||
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.SnWallet", b =>
|
||||
{
|
||||
b.Navigation("Pockets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletFund", b =>
|
||||
{
|
||||
b.Navigation("Recipients");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletSubscription", b =>
|
||||
{
|
||||
b.Navigation("Gift");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Wallet.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSubscriptionCatalog : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "display_name",
|
||||
table: "wallet_subscriptions",
|
||||
type: "character varying(4096)",
|
||||
maxLength: 4096,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "group_identifier",
|
||||
table: "wallet_subscriptions",
|
||||
type: "character varying(4096)",
|
||||
maxLength: 4096,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "perk_level",
|
||||
table: "wallet_subscriptions",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "wallet_subscription_catalog_settings",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
gift_policy_defaults = table.Column<SubscriptionGiftPolicy>(type: "jsonb", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_wallet_subscription_catalog_settings", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "wallet_subscription_definitions",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
identifier = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
|
||||
group_identifier = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
|
||||
display_name = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
|
||||
currency = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
base_price = table.Column<decimal>(type: "numeric", nullable: false),
|
||||
perk_level = table.Column<int>(type: "integer", nullable: false),
|
||||
minimum_account_level = table.Column<int>(type: "integer", nullable: true),
|
||||
experience_multiplier = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
golden_point_reward = table.Column<int>(type: "integer", nullable: true),
|
||||
payment_policy = table.Column<SubscriptionPaymentPolicy>(type: "jsonb", nullable: false),
|
||||
gift_policy = table.Column<SubscriptionGiftPolicy>(type: "jsonb", nullable: true),
|
||||
provider_mappings = table.Column<Dictionary<string, List<string>>>(type: "jsonb", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_wallet_subscription_definitions", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_wallet_subscription_definitions_identifier",
|
||||
table: "wallet_subscription_definitions",
|
||||
column: "identifier",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "wallet_subscription_catalog_settings");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "wallet_subscription_definitions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "display_name",
|
||||
table: "wallet_subscriptions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "group_identifier",
|
||||
table: "wallet_subscriptions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "perk_level",
|
||||
table: "wallet_subscriptions");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -595,10 +595,20 @@ namespace DysonNetwork.Wallet.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<Instant?>("EndedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("ended_at");
|
||||
|
||||
b.Property<string>("GroupIdentifier")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("group_identifier");
|
||||
|
||||
b.Property<string>("Identifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
@@ -624,6 +634,10 @@ namespace DysonNetwork.Wallet.Migrations
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("payment_method");
|
||||
|
||||
b.Property<int>("PerkLevel")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("perk_level");
|
||||
|
||||
b.Property<Instant?>("RenewalAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("renewal_at");
|
||||
@@ -660,6 +674,122 @@ namespace DysonNetwork.Wallet.Migrations
|
||||
b.ToTable("wallet_subscriptions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletSubscriptionCatalogSettings", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<SubscriptionGiftPolicy>("GiftPolicyDefaults")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("gift_policy_defaults");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_wallet_subscription_catalog_settings");
|
||||
|
||||
b.ToTable("wallet_subscription_catalog_settings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletSubscriptionDefinition", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<decimal>("BasePrice")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("base_price");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("currency");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<decimal?>("ExperienceMultiplier")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("experience_multiplier");
|
||||
|
||||
b.Property<SubscriptionGiftPolicy>("GiftPolicy")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("gift_policy");
|
||||
|
||||
b.Property<int?>("GoldenPointReward")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("golden_point_reward");
|
||||
|
||||
b.Property<string>("GroupIdentifier")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("group_identifier");
|
||||
|
||||
b.Property<string>("Identifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("identifier");
|
||||
|
||||
b.Property<int?>("MinimumAccountLevel")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("minimum_account_level");
|
||||
|
||||
b.Property<SubscriptionPaymentPolicy>("PaymentPolicy")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("payment_policy");
|
||||
|
||||
b.Property<int>("PerkLevel")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("perk_level");
|
||||
|
||||
b.Property<Dictionary<string, List<string>>>("ProviderMappings")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("provider_mappings");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_wallet_subscription_definitions");
|
||||
|
||||
b.HasIndex("Identifier")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_wallet_subscription_definitions_identifier");
|
||||
|
||||
b.ToTable("wallet_subscription_definitions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletTransaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
||||
118
DysonNetwork.Wallet/Payment/SubscriptionCatalogService.cs
Normal file
118
DysonNetwork.Wallet/Payment/SubscriptionCatalogService.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Wallet.Payment;
|
||||
|
||||
public class SubscriptionCatalogService(
|
||||
AppDatabase db,
|
||||
IConfiguration configuration,
|
||||
ILogger<SubscriptionCatalogService> logger
|
||||
)
|
||||
{
|
||||
private readonly AppDatabase _db = db;
|
||||
private readonly IConfiguration _configuration = configuration;
|
||||
private readonly ILogger<SubscriptionCatalogService> _logger = logger;
|
||||
|
||||
public async Task EnsureSeededAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var options = _configuration.GetSection("Payment:SubscriptionCatalog").Get<SubscriptionCatalogSeedOptions>();
|
||||
if (options is null) return;
|
||||
|
||||
var existingSettings = await _db.WalletSubscriptionCatalogSettings.FirstOrDefaultAsync(cancellationToken);
|
||||
if (existingSettings is null)
|
||||
{
|
||||
_db.WalletSubscriptionCatalogSettings.Add(new SnWalletSubscriptionCatalogSettings
|
||||
{
|
||||
GiftPolicyDefaults = options.Settings.GiftPolicyDefaults.Clone()
|
||||
});
|
||||
}
|
||||
|
||||
var identifiers = options.Definitions.Select(x => x.Identifier).ToList();
|
||||
var existingDefinitions = await _db.WalletSubscriptionDefinitions
|
||||
.Where(x => identifiers.Contains(x.Identifier))
|
||||
.Select(x => x.Identifier)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var definition in options.Definitions.Where(x => !existingDefinitions.Contains(x.Identifier)))
|
||||
{
|
||||
_db.WalletSubscriptionDefinitions.Add(new SnWalletSubscriptionDefinition
|
||||
{
|
||||
Identifier = definition.Identifier,
|
||||
GroupIdentifier = definition.GroupIdentifier,
|
||||
DisplayName = definition.DisplayName,
|
||||
Currency = definition.Currency,
|
||||
BasePrice = definition.BasePrice,
|
||||
PerkLevel = definition.PerkLevel,
|
||||
MinimumAccountLevel = definition.MinimumAccountLevel,
|
||||
ExperienceMultiplier = definition.ExperienceMultiplier,
|
||||
GoldenPointReward = definition.GoldenPointReward,
|
||||
PaymentPolicy = new SubscriptionPaymentPolicy
|
||||
{
|
||||
AllowInternalWallet = definition.PaymentPolicy.AllowInternalWallet,
|
||||
AllowExternal = definition.PaymentPolicy.AllowExternal,
|
||||
AllowInternalWalletRenewal = definition.PaymentPolicy.AllowInternalWalletRenewal,
|
||||
AllowedMethods = definition.PaymentPolicy.AllowedMethods.ToList()
|
||||
},
|
||||
GiftPolicy = definition.GiftPolicy is null ? null : new SubscriptionGiftPolicy
|
||||
{
|
||||
AllowPurchase = definition.GiftPolicy.AllowPurchase,
|
||||
MinimumAccountLevel = definition.GiftPolicy.MinimumAccountLevel,
|
||||
AllowPerkSubscriptionBypass = definition.GiftPolicy.AllowPerkSubscriptionBypass,
|
||||
RollingPurchaseLimit = definition.GiftPolicy.RollingPurchaseLimit,
|
||||
RollingWindowDays = definition.GiftPolicy.RollingWindowDays,
|
||||
GiftDurationDays = definition.GiftPolicy.GiftDurationDays,
|
||||
SubscriptionDurationDays = definition.GiftPolicy.SubscriptionDurationDays
|
||||
},
|
||||
ProviderMappings = definition.ProviderMappings.ToDictionary(
|
||||
kv => kv.Key,
|
||||
kv => kv.Value.ToList(),
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
if (_db.ChangeTracker.HasChanges())
|
||||
{
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("Seeded subscription catalog definitions from configuration.");
|
||||
}
|
||||
}
|
||||
|
||||
public Task<SnWalletSubscriptionDefinition?> GetDefinitionAsync(string identifier, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _db.WalletSubscriptionDefinitions.FirstOrDefaultAsync(x => x.Identifier == identifier, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<SnWalletSubscriptionDefinition?> ResolveDefinitionAsync(
|
||||
string provider,
|
||||
string externalId,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var definitions = await _db.WalletSubscriptionDefinitions.ToListAsync(cancellationToken);
|
||||
return definitions.FirstOrDefault(def =>
|
||||
def.ProviderMappings.TryGetValue(provider, out var mapped) &&
|
||||
mapped.Any(id => string.Equals(id, externalId, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetGroupIdentifiersAsync(string? groupIdentifier, string fallbackIdentifier, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(groupIdentifier))
|
||||
return [fallbackIdentifier];
|
||||
|
||||
return await _db.WalletSubscriptionDefinitions
|
||||
.Where(x => x.GroupIdentifier == groupIdentifier)
|
||||
.Select(x => x.Identifier)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<SubscriptionGiftPolicy> GetGiftPolicyAsync(
|
||||
SnWalletSubscriptionDefinition definition,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var settings = await _db.WalletSubscriptionCatalogSettings.FirstOrDefaultAsync(cancellationToken);
|
||||
var defaults = settings?.GiftPolicyDefaults.Clone() ?? new SubscriptionGiftPolicy();
|
||||
return defaults.Merge(definition.GiftPolicy);
|
||||
}
|
||||
}
|
||||
@@ -124,32 +124,7 @@ public class SubscriptionGiftController(
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check if user already has this subscription type
|
||||
var subscriptionInfo = SubscriptionTypeData
|
||||
.SubscriptionDict.TryGetValue(gift.SubscriptionIdentifier, out var template)
|
||||
? template
|
||||
: null;
|
||||
|
||||
if (subscriptionInfo != null)
|
||||
{
|
||||
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 subscriptions.GetSubscriptionAsync(Guid.Parse(currentUser.Id), subscriptionsInGroup);
|
||||
if (existingSubscription is not null)
|
||||
{
|
||||
error = "You already have an active subscription of this type.";
|
||||
}
|
||||
else
|
||||
{
|
||||
canRedeem = true;
|
||||
}
|
||||
}
|
||||
canRedeem = true;
|
||||
}
|
||||
|
||||
return new GiftCheckResponse
|
||||
@@ -183,8 +158,6 @@ public class SubscriptionGiftController(
|
||||
public int? SubscriptionDurationDays { get; set; } = 30; // Subscription lasts 30 days when redeemed
|
||||
}
|
||||
|
||||
const int MinimumAccountLevel = 60;
|
||||
|
||||
/// <summary>
|
||||
/// Purchases a gift subscription.
|
||||
/// </summary>
|
||||
@@ -194,12 +167,6 @@ public class SubscriptionGiftController(
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not DyAccount currentUser) return Unauthorized();
|
||||
|
||||
if (currentUser.Profile.Level < MinimumAccountLevel)
|
||||
{
|
||||
if (currentUser.PerkSubscription is null)
|
||||
return StatusCode(403, "Account level must be at least 60 or a member of the Stellar Program to purchase a gift.");
|
||||
}
|
||||
|
||||
Duration? giftDuration = null;
|
||||
if (request.GiftDurationDays.HasValue)
|
||||
giftDuration = Duration.FromDays(request.GiftDurationDays.Value);
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace DysonNetwork.Wallet.Payment;
|
||||
|
||||
public class SubscriptionRenewalJob(
|
||||
AppDatabase db,
|
||||
SubscriptionCatalogService catalog,
|
||||
SubscriptionService subscriptionService,
|
||||
PaymentService paymentService,
|
||||
WalletService walletService,
|
||||
@@ -26,6 +27,23 @@ public class SubscriptionRenewalJob(
|
||||
var processedCount = 0;
|
||||
var renewedCount = 0;
|
||||
var failedCount = 0;
|
||||
var activatedCount = 0;
|
||||
|
||||
var queuedSubscriptions = await db.WalletSubscriptions
|
||||
.Where(s => s.IsActive)
|
||||
.Where(s => s.Status == SubscriptionStatus.Active)
|
||||
.Where(s => s.BegunAt <= now)
|
||||
.Where(s => s.EndedAt == null || s.EndedAt > now)
|
||||
.Where(s => s.RenewalAt == null || s.RenewalAt > now)
|
||||
.Where(s => s.CreatedAt != s.UpdatedAt)
|
||||
.Take(batchSize)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var subscription in queuedSubscriptions.Where(s => s.BegunAt > s.CreatedAt))
|
||||
{
|
||||
activatedCount++;
|
||||
await subscriptionService.UpdateExpiredSubscriptionsAsync();
|
||||
}
|
||||
|
||||
// Find subscriptions that need renewal (due for renewal and are still active)
|
||||
var subscriptionsToRenew = await db.WalletSubscriptions
|
||||
@@ -33,6 +51,7 @@ public class SubscriptionRenewalJob(
|
||||
.Where(s => s.Status == SubscriptionStatus.Active) // Only paid subscriptions
|
||||
.Where(s => s.IsActive) // Only active subscriptions
|
||||
.Where(s => !s.IsFreeTrial) // Exclude free trials
|
||||
.Where(s => s.BegunAt <= now)
|
||||
.OrderBy(s => s.RenewalAt) // Process oldest first
|
||||
.Take(batchSize)
|
||||
.Include(s => s.Coupon) // Include coupon information
|
||||
@@ -50,6 +69,14 @@ public class SubscriptionRenewalJob(
|
||||
"Processing renewal for subscription {SubscriptionId} (Identifier: {Identifier}) for account {AccountId}",
|
||||
subscription.Id, subscription.Identifier, subscription.AccountId);
|
||||
|
||||
var definition = await catalog.GetDefinitionAsync(subscription.Identifier, context.CancellationToken);
|
||||
if (definition is null)
|
||||
{
|
||||
logger.LogWarning("Subscription definition missing for {Identifier}", subscription.Identifier);
|
||||
failedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (subscription.RenewalAt is null)
|
||||
{
|
||||
logger.LogWarning(
|
||||
@@ -64,26 +91,25 @@ public class SubscriptionRenewalJob(
|
||||
// Calculate next cycle duration based on current cycle
|
||||
var currentCycle = subscription.EndedAt!.Value - subscription.BegunAt;
|
||||
|
||||
// Create an order for the renewal payment
|
||||
var order = await paymentService.CreateOrderAsync(
|
||||
null,
|
||||
WalletCurrency.GoldenPoint,
|
||||
subscription.FinalPrice,
|
||||
appIdentifier: "internal",
|
||||
productIdentifier: subscription.Identifier,
|
||||
meta: new Dictionary<string, object>()
|
||||
{
|
||||
["subscription_id"] = subscription.Id.ToString(),
|
||||
["subscription_identifier"] = subscription.Identifier,
|
||||
["is_renewal"] = true
|
||||
}
|
||||
);
|
||||
|
||||
// Try to process the payment automatically
|
||||
if (subscription.PaymentMethod == SubscriptionPaymentMethod.InAppWallet)
|
||||
if (subscription.PaymentMethod == SubscriptionPaymentMethod.InAppWallet &&
|
||||
definition.PaymentPolicy.AllowInternalWalletRenewal)
|
||||
{
|
||||
try
|
||||
{
|
||||
var order = await paymentService.CreateOrderAsync(
|
||||
null,
|
||||
definition.Currency,
|
||||
subscription.FinalPrice,
|
||||
appIdentifier: "internal",
|
||||
productIdentifier: subscription.Identifier,
|
||||
meta: new Dictionary<string, object>()
|
||||
{
|
||||
["subscription_id"] = subscription.Id.ToString(),
|
||||
["subscription_identifier"] = subscription.Identifier,
|
||||
["is_renewal"] = true
|
||||
}
|
||||
);
|
||||
|
||||
var wallet = await walletService.GetAccountWalletAsync(subscription.AccountId);
|
||||
if (wallet is null) continue;
|
||||
|
||||
@@ -111,10 +137,11 @@ public class SubscriptionRenewalJob(
|
||||
}
|
||||
else
|
||||
{
|
||||
// For other payment methods, mark as pending payment
|
||||
logger.LogInformation("Subscription {SubscriptionId} requires manual payment via {PaymentMethod}",
|
||||
subscription.Id, subscription.PaymentMethod);
|
||||
failedCount++;
|
||||
logger.LogInformation(
|
||||
"Skipping renewal for subscription {SubscriptionId}; provider {PaymentMethod} must extend via webhook or renewal is disabled",
|
||||
subscription.Id,
|
||||
subscription.PaymentMethod
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -133,7 +160,7 @@ public class SubscriptionRenewalJob(
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
"Completed subscription renewal job. Processed: {ProcessedCount}, Renewed: {RenewedCount}, Failed: {FailedCount}",
|
||||
processedCount, renewedCount, failedCount);
|
||||
"Completed subscription renewal job. Activated: {ActivatedCount}, Processed: {ProcessedCount}, Renewed: {RenewedCount}, Failed: {FailedCount}",
|
||||
activatedCount, processedCount, renewedCount, failedCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ namespace DysonNetwork.Wallet.Payment;
|
||||
public class SubscriptionService(
|
||||
AppDatabase db,
|
||||
PaymentService payment,
|
||||
SubscriptionCatalogService catalog,
|
||||
DyProfileService.DyProfileServiceClient accounts,
|
||||
DyRingService.DyRingServiceClient pusher,
|
||||
ILocalizationService localizer,
|
||||
IConfiguration configuration,
|
||||
ICacheService cache,
|
||||
ILogger<SubscriptionService> logger
|
||||
)
|
||||
@@ -43,27 +43,21 @@ public class SubscriptionService(
|
||||
bool noop = false
|
||||
)
|
||||
{
|
||||
var subscriptionInfo = SubscriptionTypeData
|
||||
.SubscriptionDict.TryGetValue(identifier, out var template)
|
||||
? template
|
||||
: null;
|
||||
if (subscriptionInfo is null)
|
||||
var definition = await catalog.GetDefinitionAsync(identifier);
|
||||
if (definition 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)
|
||||
.Select(s => s.Value.Identifier)
|
||||
.ToArray()
|
||||
: [identifier];
|
||||
if (!definition.IsPaymentMethodAllowed(paymentMethod))
|
||||
throw new InvalidOperationException($"Payment method {paymentMethod} is not allowed for subscription {identifier}.");
|
||||
if (definition.MinimumAccountLevel.HasValue && account.Profile.Level < definition.MinimumAccountLevel.Value)
|
||||
throw new InvalidOperationException(
|
||||
$"Account level must be at least {definition.MinimumAccountLevel.Value} to purchase {identifier}."
|
||||
);
|
||||
|
||||
cycleDuration ??= Duration.FromDays(30);
|
||||
|
||||
var accountId = Guid.Parse(account.Id);
|
||||
var existingSubscription = await GetSubscriptionAsync(accountId, subscriptionsInGroup);
|
||||
if (existingSubscription is not null && !noop)
|
||||
throw new InvalidOperationException($"Active subscription with identifier {identifier} already exists.");
|
||||
if (existingSubscription is not null)
|
||||
var existingSubscription = await GetSubscriptionAsync(accountId, identifier);
|
||||
if (existingSubscription is not null && noop)
|
||||
return existingSubscription;
|
||||
|
||||
// Batch database queries for coupon and free trial check
|
||||
@@ -95,15 +89,20 @@ public class SubscriptionService(
|
||||
BegunAt = now,
|
||||
EndedAt = now.Plus(cycleDuration.Value),
|
||||
Identifier = identifier,
|
||||
GroupIdentifier = definition.GroupIdentifier,
|
||||
DisplayName = definition.DisplayName,
|
||||
PerkLevel = definition.PerkLevel,
|
||||
IsActive = true,
|
||||
IsFreeTrial = isFreeTrial,
|
||||
Status = Shared.Models.SubscriptionStatus.Unpaid,
|
||||
PaymentMethod = paymentMethod,
|
||||
PaymentDetails = paymentDetails,
|
||||
BasePrice = subscriptionInfo.BasePrice,
|
||||
BasePrice = definition.BasePrice,
|
||||
CouponId = couponData?.Id,
|
||||
Coupon = couponData,
|
||||
RenewalAt = (isFreeTrial || !isAutoRenewal) ? null : now.Plus(cycleDuration.Value),
|
||||
RenewalAt = (isFreeTrial || !isAutoRenewal || !definition.PaymentPolicy.AllowInternalWalletRenewal)
|
||||
? null
|
||||
: now.Plus(cycleDuration.Value),
|
||||
AccountId = accountId,
|
||||
};
|
||||
|
||||
@@ -115,35 +114,14 @@ public class SubscriptionService(
|
||||
|
||||
public async Task<SnWalletSubscription> CreateSubscriptionFromOrder(ISubscriptionOrder order)
|
||||
{
|
||||
var cfgSection = configuration.GetSection("Payment:Subscriptions");
|
||||
var provider = order.Provider;
|
||||
if (string.IsNullOrWhiteSpace(order.SubscriptionId))
|
||||
throw new InvalidOperationException("Subscription identifier was missing from the payment payload.");
|
||||
|
||||
var currency = "irl";
|
||||
var subscriptionIdentifier = order.SubscriptionId;
|
||||
switch (provider)
|
||||
{
|
||||
case SubscriptionPaymentMethod.Afdian:
|
||||
// Get the Afdian section first, then bind it to a dictionary
|
||||
var afdianPlans = cfgSection.GetSection("Afdian").Get<Dictionary<string, string>>();
|
||||
logger.LogInformation("Afdian plans configuration: {Plans}", JsonSerializer.Serialize(afdianPlans));
|
||||
if (afdianPlans != null && afdianPlans.TryGetValue(subscriptionIdentifier, out var planName))
|
||||
subscriptionIdentifier = planName;
|
||||
currency = "cny";
|
||||
break;
|
||||
case SubscriptionPaymentMethod.Paddle:
|
||||
var paddlePlans = cfgSection.GetSection("Paddle").Get<Dictionary<string, string>>();
|
||||
logger.LogInformation("Paddle plans configuration: {Plans}", JsonSerializer.Serialize(paddlePlans));
|
||||
if (paddlePlans != null && paddlePlans.TryGetValue(subscriptionIdentifier, out var paddlePlanName))
|
||||
subscriptionIdentifier = paddlePlanName;
|
||||
currency = "usd";
|
||||
break;
|
||||
}
|
||||
|
||||
var subscriptionTemplate = SubscriptionTypeData
|
||||
.SubscriptionDict.GetValueOrDefault(subscriptionIdentifier);
|
||||
if (subscriptionTemplate is null)
|
||||
throw new ArgumentOutOfRangeException(nameof(subscriptionIdentifier),
|
||||
$"Subscription {subscriptionIdentifier} was not found.");
|
||||
var definition = await ResolveDefinitionForOrderAsync(order);
|
||||
if (definition is null)
|
||||
throw new ArgumentOutOfRangeException(nameof(order.SubscriptionId),
|
||||
$"Subscription mapping {order.SubscriptionId} was not found for provider {provider}.");
|
||||
|
||||
SnAccount? account = null;
|
||||
if (!string.IsNullOrEmpty(provider))
|
||||
@@ -165,58 +143,18 @@ public class SubscriptionService(
|
||||
|
||||
if (account is null)
|
||||
throw new InvalidOperationException($"Account was not found with identifier {order.AccountId}");
|
||||
if (string.IsNullOrWhiteSpace(order.SubscriptionId))
|
||||
throw new InvalidOperationException("Subscription identifier was missing from the payment payload.");
|
||||
|
||||
var cycleDuration = order.Duration;
|
||||
|
||||
var existingSubscription = await GetSubscriptionAsync(account.Id, subscriptionIdentifier);
|
||||
if (existingSubscription is not null && existingSubscription.PaymentMethod != provider)
|
||||
throw new InvalidOperationException(
|
||||
$"Active subscription with identifier {subscriptionIdentifier} already exists.");
|
||||
if (existingSubscription?.PaymentDetails.OrderId == order.Id)
|
||||
return existingSubscription;
|
||||
if (existingSubscription is not null)
|
||||
{
|
||||
// Same provider, but different order, renew the subscription
|
||||
existingSubscription.PaymentDetails.OrderId = order.Id;
|
||||
existingSubscription.EndedAt = order.BegunAt.Plus(cycleDuration);
|
||||
existingSubscription.RenewalAt = order.BegunAt.Plus(cycleDuration);
|
||||
existingSubscription.Status = SubscriptionStatus.Active;
|
||||
|
||||
db.Update(existingSubscription);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return existingSubscription;
|
||||
}
|
||||
|
||||
var subscription = new SnWalletSubscription
|
||||
{
|
||||
BegunAt = order.BegunAt,
|
||||
EndedAt = order.BegunAt.Plus(cycleDuration),
|
||||
IsActive = true,
|
||||
Status = SubscriptionStatus.Active,
|
||||
Identifier = subscriptionIdentifier,
|
||||
PaymentMethod = provider,
|
||||
PaymentDetails = new SnPaymentDetails
|
||||
return await ApplyPaidSubscriptionAsync(
|
||||
account.Id,
|
||||
definition,
|
||||
provider,
|
||||
new SnPaymentDetails
|
||||
{
|
||||
Currency = currency,
|
||||
Currency = definition.Currency,
|
||||
OrderId = order.Id,
|
||||
},
|
||||
BasePrice = subscriptionTemplate.BasePrice,
|
||||
RenewalAt = order.BegunAt.Plus(cycleDuration),
|
||||
AccountId = account.Id,
|
||||
};
|
||||
|
||||
db.WalletSubscriptions.Add(subscription);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await NotifySubscriptionBegun(subscription);
|
||||
|
||||
await HandleSponsorCurrencyUpdateAsync(subscription);
|
||||
await HandleSponsorBadgeSubscriptionAsync(subscription);
|
||||
|
||||
return subscription;
|
||||
order.BegunAt,
|
||||
order.Duration
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -268,16 +206,12 @@ public class SubscriptionService(
|
||||
.OrderByDescending(s => s.BegunAt)
|
||||
.FirstOrDefaultAsync();
|
||||
if (subscription is null) throw new InvalidOperationException("No matching subscription found.");
|
||||
|
||||
var subscriptionInfo = SubscriptionTypeData.SubscriptionDict
|
||||
.TryGetValue(subscription.Identifier, out var template)
|
||||
? template
|
||||
: null;
|
||||
if (subscriptionInfo is null) throw new InvalidOperationException("No matching subscription found.");
|
||||
var definition = await catalog.GetDefinitionAsync(subscription.Identifier);
|
||||
if (definition is null) throw new InvalidOperationException("No matching subscription found.");
|
||||
|
||||
return await payment.CreateOrderAsync(
|
||||
null,
|
||||
subscriptionInfo.Currency,
|
||||
definition.Currency,
|
||||
subscription.FinalPrice,
|
||||
appIdentifier: "internal",
|
||||
productIdentifier: identifier,
|
||||
@@ -304,16 +238,12 @@ public class SubscriptionService(
|
||||
.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.");
|
||||
var definition = await catalog.GetDefinitionAsync(gift.SubscriptionIdentifier);
|
||||
if (definition is null) throw new InvalidOperationException("No matching subscription found.");
|
||||
|
||||
return await payment.CreateOrderAsync(
|
||||
null,
|
||||
subscriptionInfo.Currency,
|
||||
definition.Currency,
|
||||
gift.FinalPrice,
|
||||
appIdentifier: "gift",
|
||||
productIdentifier: gift.SubscriptionIdentifier,
|
||||
@@ -341,26 +271,25 @@ public class SubscriptionService(
|
||||
.FirstOrDefaultAsync();
|
||||
if (subscription is null)
|
||||
throw new InvalidOperationException("Invalid order.");
|
||||
var definition = await catalog.GetDefinitionAsync(subscription.Identifier)
|
||||
?? throw new InvalidOperationException("Invalid order.");
|
||||
var cycleDuration = subscription.EndedAt.HasValue
|
||||
? subscription.EndedAt.Value - subscription.BegunAt
|
||||
: Duration.FromDays(30);
|
||||
|
||||
if (subscription.Status == Shared.Models.SubscriptionStatus.Expired)
|
||||
{
|
||||
// Calculate original cycle duration and extend from the current ended date
|
||||
Duration originalCycle = subscription.EndedAt.Value - subscription.BegunAt;
|
||||
|
||||
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;
|
||||
|
||||
db.Update(subscription);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await NotifySubscriptionBegun(subscription);
|
||||
|
||||
return subscription;
|
||||
return await ApplyPaidSubscriptionAsync(
|
||||
subscription.AccountId,
|
||||
definition,
|
||||
subscription.PaymentMethod,
|
||||
new SnPaymentDetails
|
||||
{
|
||||
Currency = definition.Currency,
|
||||
OrderId = order.Id.ToString()
|
||||
},
|
||||
SystemClock.Instance.GetCurrentInstant(),
|
||||
cycleDuration,
|
||||
placeholderSubscription: subscription
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<SnWalletGift> HandleGiftOrder(SnWalletOrder order)
|
||||
@@ -430,12 +359,152 @@ public class SubscriptionService(
|
||||
return expiredSubscriptions.Count;
|
||||
}
|
||||
|
||||
private async Task<SnWalletSubscriptionDefinition?> ResolveDefinitionForOrderAsync(ISubscriptionOrder order)
|
||||
{
|
||||
var direct = await catalog.GetDefinitionAsync(order.SubscriptionId);
|
||||
if (direct is not null) return direct;
|
||||
|
||||
return await catalog.ResolveDefinitionAsync(order.Provider, order.SubscriptionId);
|
||||
}
|
||||
|
||||
private async Task EnforceGiftPurchaseLimitAsync(
|
||||
Guid gifterId,
|
||||
string subscriptionIdentifier,
|
||||
SubscriptionGiftPolicy giftPolicy,
|
||||
Instant now
|
||||
)
|
||||
{
|
||||
if (!giftPolicy.RollingPurchaseLimit.HasValue || giftPolicy.RollingPurchaseLimit <= 0)
|
||||
return;
|
||||
|
||||
var windowStart = now.Minus(Duration.FromDays(giftPolicy.RollingWindowDays ?? 30));
|
||||
var currentCount = await db.WalletGifts
|
||||
.Where(g => g.GifterId == gifterId)
|
||||
.Where(g => g.SubscriptionIdentifier == subscriptionIdentifier)
|
||||
.Where(g => g.CreatedAt >= windowStart)
|
||||
.CountAsync();
|
||||
|
||||
if (currentCount >= giftPolicy.RollingPurchaseLimit.Value)
|
||||
throw new InvalidOperationException("Gift purchase limit reached for this subscription.");
|
||||
}
|
||||
|
||||
private async Task<SnWalletSubscription> ApplyPaidSubscriptionAsync(
|
||||
Guid accountId,
|
||||
SnWalletSubscriptionDefinition definition,
|
||||
string paymentMethod,
|
||||
SnPaymentDetails paymentDetails,
|
||||
Instant effectiveFrom,
|
||||
Duration cycleDuration,
|
||||
SnWalletSubscription? placeholderSubscription = null,
|
||||
Guid? couponId = null,
|
||||
SnWalletCoupon? coupon = null
|
||||
)
|
||||
{
|
||||
var groupIdentifiers = await catalog.GetGroupIdentifiersAsync(definition.GroupIdentifier, definition.Identifier);
|
||||
var activeOrQueued = await db.WalletSubscriptions
|
||||
.Where(s => s.AccountId == accountId)
|
||||
.Where(s => s.IsActive)
|
||||
.Where(s => s.Status == SubscriptionStatus.Active)
|
||||
.Where(s => groupIdentifiers.Contains(s.Identifier))
|
||||
.Where(s => placeholderSubscription == null || s.Id != placeholderSubscription.Id)
|
||||
.OrderBy(s => s.BegunAt)
|
||||
.ToListAsync();
|
||||
|
||||
var sameIdentifierTail = activeOrQueued
|
||||
.Where(s => s.Identifier == definition.Identifier)
|
||||
.OrderByDescending(s => s.EndedAt ?? s.BegunAt)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (sameIdentifierTail?.PaymentDetails.OrderId == paymentDetails.OrderId)
|
||||
return sameIdentifierTail;
|
||||
|
||||
if (sameIdentifierTail is not null)
|
||||
{
|
||||
var extensionBase = sameIdentifierTail.EndedAt.HasValue && sameIdentifierTail.EndedAt.Value > effectiveFrom
|
||||
? sameIdentifierTail.EndedAt.Value
|
||||
: effectiveFrom;
|
||||
|
||||
sameIdentifierTail.EndedAt = extensionBase.Plus(cycleDuration);
|
||||
sameIdentifierTail.PaymentMethod = paymentMethod;
|
||||
sameIdentifierTail.PaymentDetails = paymentDetails;
|
||||
sameIdentifierTail.CouponId = couponId ?? sameIdentifierTail.CouponId;
|
||||
sameIdentifierTail.Coupon = coupon ?? sameIdentifierTail.Coupon;
|
||||
sameIdentifierTail.BasePrice = definition.BasePrice;
|
||||
sameIdentifierTail.GroupIdentifier = definition.GroupIdentifier;
|
||||
sameIdentifierTail.DisplayName = definition.DisplayName;
|
||||
sameIdentifierTail.PerkLevel = definition.PerkLevel;
|
||||
sameIdentifierTail.Status = SubscriptionStatus.Active;
|
||||
sameIdentifierTail.RenewalAt = definition.PaymentPolicy.AllowInternalWalletRenewal
|
||||
? sameIdentifierTail.EndedAt
|
||||
: null;
|
||||
|
||||
if (placeholderSubscription is not null && placeholderSubscription.Id != sameIdentifierTail.Id)
|
||||
{
|
||||
placeholderSubscription.IsActive = false;
|
||||
placeholderSubscription.Status = SubscriptionStatus.Cancelled;
|
||||
db.WalletSubscriptions.Update(placeholderSubscription);
|
||||
}
|
||||
|
||||
db.WalletSubscriptions.Update(sameIdentifierTail);
|
||||
await db.SaveChangesAsync();
|
||||
await InvalidateSubscriptionCaches(accountId, definition.Identifier);
|
||||
return sameIdentifierTail;
|
||||
}
|
||||
|
||||
var groupTail = activeOrQueued
|
||||
.OrderByDescending(s => s.EndedAt ?? s.BegunAt)
|
||||
.FirstOrDefault();
|
||||
var begunAt = groupTail?.EndedAt is { } endedAt && endedAt > effectiveFrom
|
||||
? endedAt
|
||||
: effectiveFrom;
|
||||
|
||||
var subscription = placeholderSubscription ?? new SnWalletSubscription();
|
||||
subscription.BegunAt = begunAt;
|
||||
subscription.EndedAt = begunAt.Plus(cycleDuration);
|
||||
subscription.Identifier = definition.Identifier;
|
||||
subscription.GroupIdentifier = definition.GroupIdentifier;
|
||||
subscription.DisplayName = definition.DisplayName;
|
||||
subscription.PerkLevel = definition.PerkLevel;
|
||||
subscription.IsActive = true;
|
||||
subscription.IsFreeTrial = false;
|
||||
subscription.Status = SubscriptionStatus.Active;
|
||||
subscription.PaymentMethod = paymentMethod;
|
||||
subscription.PaymentDetails = paymentDetails;
|
||||
subscription.BasePrice = definition.BasePrice;
|
||||
subscription.CouponId = couponId;
|
||||
subscription.Coupon = coupon;
|
||||
subscription.RenewalAt = definition.PaymentPolicy.AllowInternalWalletRenewal
|
||||
? subscription.EndedAt
|
||||
: null;
|
||||
subscription.AccountId = accountId;
|
||||
|
||||
if (placeholderSubscription is null)
|
||||
db.WalletSubscriptions.Add(subscription);
|
||||
else
|
||||
db.WalletSubscriptions.Update(subscription);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
await InvalidateSubscriptionCaches(accountId, definition.Identifier);
|
||||
|
||||
if (begunAt <= SystemClock.Instance.GetCurrentInstant())
|
||||
{
|
||||
await NotifySubscriptionBegun(subscription);
|
||||
await HandleSponsorCurrencyUpdateAsync(subscription);
|
||||
await HandleSponsorBadgeSubscriptionAsync(subscription);
|
||||
}
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
private async Task InvalidateSubscriptionCaches(Guid accountId, string identifier)
|
||||
{
|
||||
await cache.RemoveAsync($"{SubscriptionCacheKeyPrefix}{accountId}:{identifier}");
|
||||
await cache.RemoveAsync($"{SubscriptionPerkCacheKeyPrefix}{accountId}");
|
||||
}
|
||||
|
||||
private async Task NotifySubscriptionBegun(SnWalletSubscription subscription)
|
||||
{
|
||||
var humanReadableName =
|
||||
SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(subscription.Identifier, out var humanReadable)
|
||||
? humanReadable
|
||||
: subscription.Identifier;
|
||||
var humanReadableName = subscription.DisplayName ?? subscription.Identifier;
|
||||
var duration = subscription.EndedAt is not null
|
||||
? subscription.EndedAt.Value.Minus(subscription.BegunAt).Days.ToString()
|
||||
: "infinite";
|
||||
@@ -496,9 +565,6 @@ public class SubscriptionService(
|
||||
|
||||
private const string SubscriptionPerkCacheKeyPrefix = "subscription:perk:";
|
||||
|
||||
private static readonly List<string> PerkIdentifiers =
|
||||
[SubscriptionType.Stellar, SubscriptionType.Nova, SubscriptionType.Supernova];
|
||||
|
||||
public async Task<SnWalletSubscription?> GetPerkSubscriptionAsync(Guid accountId)
|
||||
{
|
||||
var cacheKey = $"{SubscriptionPerkCacheKeyPrefix}{accountId}";
|
||||
@@ -513,10 +579,11 @@ public class SubscriptionService(
|
||||
// If not in cache, get from database
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var subscription = await db.WalletSubscriptions
|
||||
.Where(s => s.AccountId == accountId && PerkIdentifiers.Contains(s.Identifier))
|
||||
.Where(s => s.AccountId == accountId && s.PerkLevel > 0)
|
||||
.Where(s => s.Status == Shared.Models.SubscriptionStatus.Active)
|
||||
.Where(s => s.EndedAt == null || s.EndedAt > now)
|
||||
.OrderByDescending(s => s.BegunAt)
|
||||
.OrderByDescending(s => s.PerkLevel)
|
||||
.ThenByDescending(s => s.BegunAt)
|
||||
.FirstOrDefaultAsync();
|
||||
if (subscription is { IsAvailable: false }) subscription = null;
|
||||
|
||||
@@ -556,10 +623,11 @@ public class SubscriptionService(
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var subscriptions = await db.WalletSubscriptions
|
||||
.Where(s => missingAccountIds.Contains(s.AccountId))
|
||||
.Where(s => PerkIdentifiers.Contains(s.Identifier))
|
||||
.Where(s => s.PerkLevel > 0)
|
||||
.Where(s => s.Status == Shared.Models.SubscriptionStatus.Active)
|
||||
.Where(s => s.EndedAt == null || s.EndedAt > now)
|
||||
.OrderByDescending(s => s.BegunAt)
|
||||
.OrderByDescending(s => s.PerkLevel)
|
||||
.ThenByDescending(s => s.BegunAt)
|
||||
.ToListAsync();
|
||||
|
||||
// Group by account and select latest available subscription
|
||||
@@ -607,11 +675,25 @@ public class SubscriptionService(
|
||||
Duration? cycleDuration = null)
|
||||
{
|
||||
// Validate subscription exists
|
||||
var subscriptionInfo = SubscriptionTypeData
|
||||
.SubscriptionDict.GetValueOrDefault(subscriptionIdentifier);
|
||||
if (subscriptionInfo is null)
|
||||
var definition = await catalog.GetDefinitionAsync(subscriptionIdentifier);
|
||||
if (definition is null)
|
||||
throw new ArgumentOutOfRangeException(nameof(subscriptionIdentifier),
|
||||
$@"Subscription {subscriptionIdentifier} was not found.");
|
||||
if (!definition.IsPaymentMethodAllowed(paymentMethod))
|
||||
throw new InvalidOperationException(
|
||||
$"Payment method {paymentMethod} is not allowed for gift purchase of {subscriptionIdentifier}."
|
||||
);
|
||||
var giftPolicy = await catalog.GetGiftPolicyAsync(definition);
|
||||
if (!giftPolicy.AllowPurchase)
|
||||
throw new InvalidOperationException("Gift purchase is disabled for this subscription.");
|
||||
if (giftPolicy.MinimumAccountLevel.HasValue && gifter.Profile.Level < giftPolicy.MinimumAccountLevel.Value)
|
||||
{
|
||||
var canBypass = giftPolicy.AllowPerkSubscriptionBypass && gifter.PerkLevel > 0;
|
||||
if (!canBypass)
|
||||
throw new InvalidOperationException(
|
||||
$"Account level must be at least {giftPolicy.MinimumAccountLevel.Value} to purchase this gift."
|
||||
);
|
||||
}
|
||||
|
||||
// Check if recipient account exists (if specified)
|
||||
DyAccount? recipient = null;
|
||||
@@ -636,10 +718,11 @@ public class SubscriptionService(
|
||||
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
|
||||
giftDuration ??= Duration.FromDays(giftPolicy.GiftDurationDays ?? 30);
|
||||
cycleDuration ??= Duration.FromDays(giftPolicy.SubscriptionDurationDays ?? 30);
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
await EnforceGiftPurchaseLimitAsync(Guid.Parse(gifter.Id), subscriptionIdentifier, giftPolicy, now);
|
||||
|
||||
// Generate unique gift code
|
||||
var giftCode = await GenerateUniqueGiftCodeAsync();
|
||||
@@ -647,7 +730,7 @@ public class SubscriptionService(
|
||||
// Calculate final price (with potential coupon discount)
|
||||
var tempSubscription = new SnWalletSubscription
|
||||
{
|
||||
BasePrice = subscriptionInfo.BasePrice,
|
||||
BasePrice = definition.BasePrice,
|
||||
CouponId = couponData?.Id,
|
||||
Coupon = couponData,
|
||||
BegunAt = now // Need for price calculation
|
||||
@@ -662,7 +745,7 @@ public class SubscriptionService(
|
||||
GiftCode = giftCode,
|
||||
Message = message,
|
||||
SubscriptionIdentifier = subscriptionIdentifier,
|
||||
BasePrice = subscriptionInfo.BasePrice,
|
||||
BasePrice = definition.BasePrice,
|
||||
FinalPrice = finalPrice,
|
||||
Status = DysonNetwork.Shared.Models.GiftStatus.Created,
|
||||
ExpiresAt = now.Plus(giftDuration.Value),
|
||||
@@ -714,118 +797,35 @@ public class SubscriptionService(
|
||||
if (!gift.IsOpenGift && gift.RecipientId != redeemerId)
|
||||
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)
|
||||
var definition = await catalog.GetDefinitionAsync(gift.SubscriptionIdentifier);
|
||||
if (definition is null)
|
||||
throw new InvalidOperationException("Invalid gift subscription type.");
|
||||
|
||||
var sameTypeSubscription = await GetSubscriptionAsync(redeemerId, gift.SubscriptionIdentifier);
|
||||
if (sameTypeSubscription is not null)
|
||||
{
|
||||
// Extend existing subscription
|
||||
var subscriptionDuration = Duration.FromDays(28);
|
||||
if (sameTypeSubscription.EndedAt.HasValue && sameTypeSubscription.EndedAt.Value > now)
|
||||
var giftPolicy = await catalog.GetGiftPolicyAsync(definition);
|
||||
var cycleDuration = Duration.FromDays(giftPolicy.SubscriptionDurationDays ?? 30);
|
||||
var subscription = await ApplyPaidSubscriptionAsync(
|
||||
redeemerId,
|
||||
definition,
|
||||
SubscriptionPaymentMethod.Gift,
|
||||
new SnPaymentDetails
|
||||
{
|
||||
sameTypeSubscription.EndedAt = sameTypeSubscription.EndedAt.Value.Plus(subscriptionDuration);
|
||||
}
|
||||
else
|
||||
{
|
||||
sameTypeSubscription.EndedAt = now.Plus(subscriptionDuration);
|
||||
}
|
||||
|
||||
if (sameTypeSubscription.RenewalAt.HasValue)
|
||||
{
|
||||
sameTypeSubscription.RenewalAt = sameTypeSubscription.RenewalAt.Value.Plus(subscriptionDuration);
|
||||
}
|
||||
|
||||
// Update gift status and link
|
||||
gift.Status = Shared.Models.GiftStatus.Redeemed;
|
||||
gift.RedeemedAt = now;
|
||||
gift.RedeemerId = redeemerId;
|
||||
gift.SubscriptionId = sameTypeSubscription.Id;
|
||||
gift.UpdatedAt = now;
|
||||
|
||||
await using var transaction = await db.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
db.WalletSubscriptions.Update(sameTypeSubscription);
|
||||
db.WalletGifts.Update(gift);
|
||||
await db.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
|
||||
if (gift.GifterId == redeemerId) return (gift, sameTypeSubscription);
|
||||
await NotifyGiftClaimedByRecipient(gift, sameTypeSubscription, gift.GifterId, redeemer);
|
||||
|
||||
return (gift, sameTypeSubscription);
|
||||
}
|
||||
|
||||
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(redeemerId, subscriptionsInGroup);
|
||||
if (existingSubscription is not null)
|
||||
throw new InvalidOperationException("You already have an active subscription of this type.");
|
||||
|
||||
// We do not check account level requirement, since it is a gift
|
||||
|
||||
// Create the subscription from the gift
|
||||
var cycleDuration = Duration.FromDays(28);
|
||||
var subscription = new SnWalletSubscription
|
||||
{
|
||||
BegunAt = now,
|
||||
EndedAt = now.Plus(cycleDuration),
|
||||
Identifier = gift.SubscriptionIdentifier,
|
||||
IsActive = true,
|
||||
IsFreeTrial = false,
|
||||
Status = Shared.Models.SubscriptionStatus.Active,
|
||||
PaymentMethod = "gift", // Special payment method indicating gift redemption
|
||||
PaymentDetails = new Shared.Models.SnPaymentDetails
|
||||
{
|
||||
Currency = "gift",
|
||||
Currency = definition.Currency,
|
||||
OrderId = gift.Id.ToString()
|
||||
},
|
||||
BasePrice = gift.BasePrice,
|
||||
CouponId = gift.CouponId,
|
||||
Coupon = gift.Coupon,
|
||||
RenewalAt = now.Plus(cycleDuration),
|
||||
AccountId = redeemerId,
|
||||
};
|
||||
now,
|
||||
cycleDuration,
|
||||
couponId: gift.CouponId,
|
||||
coupon: gift.Coupon
|
||||
);
|
||||
|
||||
// Update the gift status
|
||||
gift.Status = DysonNetwork.Shared.Models.GiftStatus.Redeemed;
|
||||
gift.RedeemedAt = now;
|
||||
gift.RedeemerId = redeemerId;
|
||||
gift.Subscription = subscription;
|
||||
gift.SubscriptionId = subscription.Id;
|
||||
gift.UpdatedAt = now;
|
||||
|
||||
// Save both gift and subscription
|
||||
using var createTransaction = await db.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
db.WalletSubscriptions.Add(subscription);
|
||||
db.WalletGifts.Update(gift);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await createTransaction.CommitAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await createTransaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
db.WalletGifts.Update(gift);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Send notification to gifter if different from redeemer
|
||||
if (gift.GifterId == redeemerId) return (gift, subscription);
|
||||
@@ -937,10 +937,7 @@ public class SubscriptionService(
|
||||
private async Task NotifyGiftClaimedByRecipient(SnWalletGift gift, SnWalletSubscription subscription, Guid gifterId,
|
||||
DyAccount redeemer)
|
||||
{
|
||||
var humanReadableName =
|
||||
SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(subscription.Identifier, out var humanReadable)
|
||||
? humanReadable
|
||||
: subscription.Identifier;
|
||||
var humanReadableName = subscription.DisplayName ?? subscription.Identifier;
|
||||
|
||||
var locale = System.Globalization.CultureInfo.CurrentUICulture.Name;
|
||||
var notification = new DyPushNotification
|
||||
@@ -969,7 +966,10 @@ public class SubscriptionService(
|
||||
|
||||
private async Task HandleSponsorCurrencyUpdateAsync(SnWalletSubscription subscription)
|
||||
{
|
||||
var amount = PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(subscription.Identifier) * 10;
|
||||
var amount = subscription.PerkLevel > 0
|
||||
? subscription.PerkLevel * 10
|
||||
: (await catalog.GetDefinitionAsync(subscription.Identifier))?.GoldenPointReward ?? 0;
|
||||
if (amount <= 0) return;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1009,7 +1009,7 @@ public class SubscriptionService(
|
||||
.OrderByDescending(b => b.ActivatedAt)
|
||||
.ToList();
|
||||
|
||||
var newLevel = PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(subscription.Identifier);
|
||||
var newLevel = subscription.PerkLevel;
|
||||
if (sponsorBadges.Count > 0)
|
||||
{
|
||||
// Increment the level from the existing badge
|
||||
|
||||
@@ -2,6 +2,7 @@ using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Networking;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using DysonNetwork.Wallet;
|
||||
using DysonNetwork.Wallet.Payment;
|
||||
using DysonNetwork.Wallet.Startup;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -38,6 +39,8 @@ using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
||||
await db.Database.MigrateAsync();
|
||||
var subscriptionCatalog = scope.ServiceProvider.GetRequiredService<SubscriptionCatalogService>();
|
||||
await subscriptionCatalog.EnsureSeededAsync();
|
||||
}
|
||||
|
||||
// Configure application middleware pipeline
|
||||
@@ -48,4 +51,4 @@ app.ConfigureGrpcServices();
|
||||
|
||||
app.UseSwaggerManifest("DysonNetwork.Wallet");
|
||||
|
||||
app.Run();
|
||||
app.Run();
|
||||
|
||||
@@ -101,6 +101,7 @@ public static class ServiceCollectionExtensions
|
||||
// Register Wallet services
|
||||
services.AddScoped<WalletService>();
|
||||
services.AddScoped<PaymentService>();
|
||||
services.AddScoped<SubscriptionCatalogService>();
|
||||
services.AddScoped<SubscriptionService>();
|
||||
services.AddScoped<AfdianPaymentHandler>();
|
||||
services.AddScoped<PaddlePaymentHandler>();
|
||||
@@ -125,10 +126,7 @@ public static class ServiceCollectionExtensions
|
||||
return;
|
||||
|
||||
// Handle subscription orders
|
||||
if (
|
||||
evt.ProductIdentifier.StartsWith(SubscriptionType.StellarProgram) &&
|
||||
evt.Meta.TryGetValue("gift_id", out _)
|
||||
)
|
||||
if (evt.Meta.TryGetValue("gift_id", out _))
|
||||
{
|
||||
logger.LogInformation("Handling gift order: {OrderId}", evt.OrderId);
|
||||
|
||||
@@ -146,7 +144,7 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
logger.LogInformation("Gift for order {OrderId} handled successfully.", evt.OrderId);
|
||||
}
|
||||
else if (evt.ProductIdentifier.StartsWith(SubscriptionType.StellarProgram))
|
||||
else if (evt.Meta.TryGetValue("subscription_id", out _))
|
||||
{
|
||||
logger.LogInformation("Handling stellar program order: {OrderId}", evt.OrderId);
|
||||
|
||||
|
||||
@@ -39,17 +39,83 @@
|
||||
"WebhookSecret": "pdl_ntfset_xxx"
|
||||
}
|
||||
},
|
||||
"Subscriptions": {
|
||||
"Afdian": {
|
||||
"7d17aae23c9611f0b5705254001e7c00": "solian.stellar.primary",
|
||||
"7dfae4743c9611f0b3a55254001e7c00": "solian.stellar.nova",
|
||||
"141713ee3d6211f085b352540025c377": "solian.stellar.supernova"
|
||||
"SubscriptionCatalog": {
|
||||
"Settings": {
|
||||
"GiftPolicyDefaults": {
|
||||
"AllowPurchase": true,
|
||||
"MinimumAccountLevel": 60,
|
||||
"AllowPerkSubscriptionBypass": true,
|
||||
"RollingPurchaseLimit": 5,
|
||||
"RollingWindowDays": 30,
|
||||
"GiftDurationDays": 30,
|
||||
"SubscriptionDurationDays": 30
|
||||
}
|
||||
},
|
||||
"Paddle": {
|
||||
"pri_your_monthly_plan": "solian.stellar.primary",
|
||||
"pri_your_nova_plan": "solian.stellar.nova",
|
||||
"pri_your_supernova_plan": "solian.stellar.supernova"
|
||||
}
|
||||
"Definitions": [
|
||||
{
|
||||
"Identifier": "solian.stellar.primary",
|
||||
"GroupIdentifier": "solian.stellar",
|
||||
"DisplayName": "Stellar Program",
|
||||
"Currency": "points",
|
||||
"BasePrice": 1200,
|
||||
"PerkLevel": 1,
|
||||
"MinimumAccountLevel": 20,
|
||||
"ExperienceMultiplier": 1.5,
|
||||
"GoldenPointReward": 10,
|
||||
"PaymentPolicy": {
|
||||
"AllowInternalWallet": true,
|
||||
"AllowExternal": true,
|
||||
"AllowInternalWalletRenewal": true,
|
||||
"AllowedMethods": [ "solian.wallet", "afdian", "paddle", "gift" ]
|
||||
},
|
||||
"ProviderMappings": {
|
||||
"Afdian": [ "7d17aae23c9611f0b5705254001e7c00" ],
|
||||
"Paddle": [ "pri_your_monthly_plan", "pro_stellar" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Identifier": "solian.stellar.nova",
|
||||
"GroupIdentifier": "solian.stellar",
|
||||
"DisplayName": "Stellar Program Nova",
|
||||
"Currency": "points",
|
||||
"BasePrice": 2400,
|
||||
"PerkLevel": 2,
|
||||
"MinimumAccountLevel": 40,
|
||||
"ExperienceMultiplier": 2.0,
|
||||
"GoldenPointReward": 20,
|
||||
"PaymentPolicy": {
|
||||
"AllowInternalWallet": true,
|
||||
"AllowExternal": true,
|
||||
"AllowInternalWalletRenewal": true,
|
||||
"AllowedMethods": [ "solian.wallet", "afdian", "paddle", "gift" ]
|
||||
},
|
||||
"ProviderMappings": {
|
||||
"Afdian": [ "7dfae4743c9611f0b3a55254001e7c00" ],
|
||||
"Paddle": [ "pri_your_nova_plan", "pro_nova" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Identifier": "solian.stellar.supernova",
|
||||
"GroupIdentifier": "solian.stellar",
|
||||
"DisplayName": "Stellar Program Supernova",
|
||||
"Currency": "points",
|
||||
"BasePrice": 3600,
|
||||
"PerkLevel": 3,
|
||||
"MinimumAccountLevel": 60,
|
||||
"ExperienceMultiplier": 2.5,
|
||||
"GoldenPointReward": 30,
|
||||
"PaymentPolicy": {
|
||||
"AllowInternalWallet": true,
|
||||
"AllowExternal": true,
|
||||
"AllowInternalWalletRenewal": true,
|
||||
"AllowedMethods": [ "solian.wallet", "afdian", "paddle", "gift" ]
|
||||
},
|
||||
"ProviderMappings": {
|
||||
"Afdian": [ "141713ee3d6211f085b352540025c377" ],
|
||||
"Paddle": [ "pri_your_supernova_plan", "pro_supernova" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Cache": {
|
||||
|
||||
@@ -18,7 +18,7 @@ public class PublicationSiteService(
|
||||
{
|
||||
var account = await remoteAccounts.GetAccount(accountId);
|
||||
var perkLevel = account.PerkSubscription is not null
|
||||
? PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(account.PerkSubscription.Identifier)
|
||||
? account.PerkSubscription.PerkLevel
|
||||
: 0;
|
||||
|
||||
var sites = await db.PublicationSites
|
||||
|
||||
Reference in New Issue
Block a user