614 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			614 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System.ComponentModel.DataAnnotations;
 | |
| using System.ComponentModel.DataAnnotations.Schema;
 | |
| using System.Globalization;
 | |
| using System.Text.Json.Serialization;
 | |
| using Microsoft.EntityFrameworkCore;
 | |
| using NodaTime;
 | |
| using NodaTime.Serialization.Protobuf;
 | |
| 
 | |
| namespace DysonNetwork.Shared.Models;
 | |
| 
 | |
| public record class SubscriptionTypeData(
 | |
|     string Identifier,
 | |
|     string? GroupIdentifier,
 | |
|     string Currency,
 | |
|     decimal BasePrice,
 | |
|     int? RequiredLevel = null
 | |
| )
 | |
| {
 | |
|     public static readonly Dictionary<string, SubscriptionTypeData> SubscriptionDict =
 | |
|         new()
 | |
|         {
 | |
|             [SubscriptionType.Twinkle] = new SubscriptionTypeData(
 | |
|                 SubscriptionType.Twinkle,
 | |
|                 SubscriptionType.StellarProgram,
 | |
|                 WalletCurrency.SourcePoint,
 | |
|                 0,
 | |
|                 1
 | |
|             ),
 | |
|             [SubscriptionType.Stellar] = new SubscriptionTypeData(
 | |
|                 SubscriptionType.Stellar,
 | |
|                 SubscriptionType.StellarProgram,
 | |
|                 WalletCurrency.SourcePoint,
 | |
|                 1200,
 | |
|                 20
 | |
|             ),
 | |
|             [SubscriptionType.Nova] = new SubscriptionTypeData(
 | |
|                 SubscriptionType.Nova,
 | |
|                 SubscriptionType.StellarProgram,
 | |
|                 WalletCurrency.SourcePoint,
 | |
|                 2400,
 | |
|                 40
 | |
|             ),
 | |
|             [SubscriptionType.Supernova] = new SubscriptionTypeData(
 | |
|                 SubscriptionType.Supernova,
 | |
|                 SubscriptionType.StellarProgram,
 | |
|                 WalletCurrency.SourcePoint,
 | |
|                 3600,
 | |
|                 60
 | |
|             )
 | |
|         };
 | |
| 
 | |
|     public static readonly Dictionary<string, string> SubscriptionHumanReadable =
 | |
|         new()
 | |
|         {
 | |
|             [SubscriptionType.Twinkle] = "Stellar Program Twinkle",
 | |
|             [SubscriptionType.Stellar] = "Stellar Program",
 | |
|             [SubscriptionType.Nova] = "Stellar Program Nova",
 | |
|             [SubscriptionType.Supernova] = "Stellar Program Supernova"
 | |
|         };
 | |
| }
 | |
| 
 | |
| /// <summary>
 | |
| /// Represents a gifted subscription that can be claimed by another user.
 | |
| /// Support both direct gifts (to specific users) and open gifts (anyone can redeem via link/code).
 | |
| /// </summary>
 | |
| [Index(nameof(GiftCode))]
 | |
| [Index(nameof(GifterId))]
 | |
| [Index(nameof(RecipientId))]
 | |
| public class SnWalletGift : ModelBase
 | |
| {
 | |
|     public Guid Id { get; set; } = Guid.NewGuid();
 | |
| 
 | |
|     /// <summary>
 | |
|     /// The user who purchased/gave the gift.
 | |
|     /// </summary>
 | |
|     public Guid GifterId { get; set; }
 | |
|     public SnAccount Gifter { get; set; } = null!;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// The intended recipient. Null for open gifts that anyone can redeem.
 | |
|     /// </summary>
 | |
|     public Guid? RecipientId { get; set; }
 | |
|     public SnAccount? Recipient { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Unique redemption code/link identifier for the gift.
 | |
|     /// </summary>
 | |
|     [MaxLength(128)]
 | |
|     public string GiftCode { get; set; } = null!;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Optional custom message from the gifter.
 | |
|     /// </summary>
 | |
|     [MaxLength(1000)]
 | |
|     public string? Message { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// The subscription type being gifted.
 | |
|     /// </summary>
 | |
|     [MaxLength(4096)]
 | |
|     public string SubscriptionIdentifier { get; set; } = null!;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// The original price before any discounts.
 | |
|     /// </summary>
 | |
|     public decimal BasePrice { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// The final price paid after discounts.
 | |
|     /// </summary>
 | |
|     public decimal FinalPrice { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Current status of the gift.
 | |
|     /// </summary>
 | |
|     public GiftStatus Status { get; set; } = GiftStatus.Created;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// When the gift was redeemed. Null if not yet redeemed.
 | |
|     /// </summary>
 | |
|     public Instant? RedeemedAt { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// The user who redeemed the gift (if different from recipient).
 | |
|     /// </summary>
 | |
|     public Guid? RedeemerId { get; set; }
 | |
|     public SnAccount? Redeemer { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// The subscription created when the gift is redeemed.
 | |
|     /// </summary>
 | |
|     [JsonIgnore] public SnWalletSubscription? Subscription { get; set; }
 | |
|     public Guid? SubscriptionId { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// When the gift expires and can no longer be redeemed.
 | |
|     /// </summary>
 | |
|     public Instant ExpiresAt { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Whether this gift can be redeemed by anyone (open gift) or only the specified recipient.
 | |
|     /// </summary>
 | |
|     public bool IsOpenGift { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Payment method used by the gifter.
 | |
|     /// </summary>
 | |
|     [MaxLength(4096)]
 | |
|     public string PaymentMethod { get; set; } = null!;
 | |
| 
 | |
|     [Column(TypeName = "jsonb")]
 | |
|     public SnPaymentDetails PaymentDetails { get; set; } = null!;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Coupon used for the gift purchase.
 | |
|     /// </summary>
 | |
|     public Guid? CouponId { get; set; }
 | |
|     public SnWalletCoupon? Coupon { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Checks if the gift can still be redeemed.
 | |
|     /// </summary>
 | |
|     [NotMapped]
 | |
|     public bool IsRedeemable
 | |
|     {
 | |
|         get
 | |
|         {
 | |
|             if (Status != GiftStatus.Sent) return false;
 | |
|             var now = SystemClock.Instance.GetCurrentInstant();
 | |
|             return now <= ExpiresAt;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Checks if the gift has expired.
 | |
|     /// </summary>
 | |
|     [NotMapped]
 | |
|     public bool IsExpired
 | |
|     {
 | |
|         get
 | |
|         {
 | |
|             if (Status == GiftStatus.Redeemed || Status == GiftStatus.Cancelled) return false;
 | |
|             var now = SystemClock.Instance.GetCurrentInstant();
 | |
|             return now > ExpiresAt;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // TODO: Uncomment once protobuf files are regenerated
 | |
|     /*
 | |
|     public Proto.Gift ToProtoValue() => new()
 | |
|     {
 | |
|         Id = Id.ToString(),
 | |
|         GifterId = GifterId.ToString(),
 | |
|         RecipientId = RecipientId?.ToString(),
 | |
|         GiftCode = GiftCode,
 | |
|         Message = Message,
 | |
|         SubscriptionIdentifier = SubscriptionIdentifier,
 | |
|         BasePrice = BasePrice.ToString(CultureInfo.InvariantCulture),
 | |
|         FinalPrice = FinalPrice.ToString(CultureInfo.InvariantCulture),
 | |
|         Status = (Proto.GiftStatus)Status,
 | |
|         RedeemedAt = RedeemedAt?.ToTimestamp(),
 | |
|         RedeemerId = RedeemerId?.ToString(),
 | |
|         SubscriptionId = SubscriptionId?.ToString(),
 | |
|         ExpiresAt = ExpiresAt.ToTimestamp(),
 | |
|         IsOpenGift = IsOpenGift,
 | |
|         PaymentMethod = PaymentMethod,
 | |
|         PaymentDetails = PaymentDetails.ToProtoValue(),
 | |
|         CouponId = CouponId?.ToString(),
 | |
|         Coupon = Coupon?.ToProtoValue(),
 | |
|         IsRedeemable = IsRedeemable,
 | |
|         IsExpired = IsExpired,
 | |
|         CreatedAt = CreatedAt.ToTimestamp(),
 | |
|         UpdatedAt = UpdatedAt.ToTimestamp()
 | |
|     };
 | |
| 
 | |
|     public static SnWalletGift FromProtoValue(Proto.Gift proto) => new()
 | |
|     {
 | |
|         Id = Guid.Parse(proto.Id),
 | |
|         GifterId = Guid.Parse(proto.GifterId),
 | |
|         RecipientId = proto.HasRecipientId ? Guid.Parse(proto.RecipientId) : null,
 | |
|         GiftCode = proto.GiftCode,
 | |
|         Message = proto.Message,
 | |
|         SubscriptionIdentifier = proto.SubscriptionIdentifier,
 | |
|         BasePrice = decimal.Parse(proto.BasePrice),
 | |
|         FinalPrice = decimal.Parse(proto.FinalPrice),
 | |
|         Status = (GiftStatus)proto.Status,
 | |
|         RedeemedAt = proto.RedeemedAt?.ToInstant(),
 | |
|         RedeemerId = proto.HasRedeemerId ? Guid.Parse(proto.RedeemerId) : null,
 | |
|         SubscriptionId = proto.HasSubscriptionId ? Guid.Parse(proto.SubscriptionId) : null,
 | |
|         ExpiresAt = proto.ExpiresAt.ToInstant(),
 | |
|         IsOpenGift = proto.IsOpenGift,
 | |
|         PaymentMethod = proto.PaymentMethod,
 | |
|         PaymentDetails = SnPaymentDetails.FromProtoValue(proto.PaymentDetails),
 | |
|         CouponId = proto.HasCouponId ? Guid.Parse(proto.CouponId) : null,
 | |
|         Coupon = proto.Coupon is not null ? SnWalletCoupon.FromProtoValue(proto.Coupon) : null,
 | |
|         CreatedAt = proto.CreatedAt.ToInstant(),
 | |
|         UpdatedAt = proto.UpdatedAt.ToInstant()
 | |
|     };
 | |
|     */
 | |
| }
 | |
| 
 | |
| public abstract class SubscriptionType
 | |
| {
 | |
|     /// <summary>
 | |
|     /// DO NOT USE THIS TYPE DIRECTLY,
 | |
|     /// this is the prefix of all the stellar program subscriptions.
 | |
|     /// </summary>
 | |
|     public const string StellarProgram = "solian.stellar";
 | |
| 
 | |
|     /// <summary>
 | |
|     /// No actual usage, just tells there is a free level named twinkle.
 | |
|     /// Applies to every registered user by default, so there is no need to create a record in db for that.
 | |
|     /// </summary>
 | |
|     public const string Twinkle = "solian.stellar.twinkle";
 | |
| 
 | |
|     public const string Stellar = "solian.stellar.primary";
 | |
|     public const string Nova = "solian.stellar.nova";
 | |
|     public const string Supernova = "solian.stellar.supernova";
 | |
| }
 | |
| 
 | |
| public abstract class SubscriptionPaymentMethod
 | |
| {
 | |
|     /// <summary>
 | |
|     /// The solar points / solar dollars.
 | |
|     /// </summary>
 | |
|     public const string InAppWallet = "solian.wallet";
 | |
| 
 | |
|     /// <summary>
 | |
|     /// afdian.com
 | |
|     /// aka. China patreon
 | |
|     /// </summary>
 | |
|     public const string Afdian = "afdian";
 | |
| }
 | |
| 
 | |
| public enum SubscriptionStatus
 | |
| {
 | |
|     Unpaid,
 | |
|     Active,
 | |
|     Expired,
 | |
|     Cancelled
 | |
| }
 | |
| 
 | |
| public enum GiftStatus
 | |
| {
 | |
|     Created = 0,
 | |
|     Sent = 1,
 | |
|     Redeemed = 2,
 | |
|     Expired = 3,
 | |
|     Cancelled = 4
 | |
| }
 | |
| 
 | |
| /// <summary>
 | |
| /// The subscription is for the Stellar Program in most cases.
 | |
| /// The paid subscription in another word.
 | |
| /// </summary>
 | |
| [Index(nameof(Identifier))]
 | |
| [Index(nameof(AccountId))]
 | |
| [Index(nameof(Status))]
 | |
| [Index(nameof(AccountId), nameof(Identifier))]
 | |
| [Index(nameof(AccountId), nameof(IsActive))]
 | |
| public class SnWalletSubscription : ModelBase
 | |
| {
 | |
|     public Guid Id { get; set; } = Guid.NewGuid();
 | |
|     public Instant BegunAt { get; set; }
 | |
|     public Instant? EndedAt { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// The type of the subscriptions
 | |
|     /// </summary>
 | |
|     [MaxLength(4096)]
 | |
|     public string Identifier { get; set; } = null!;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// The field is used to override the activation status of the membership.
 | |
|     /// Might be used for refund handling and other special cases.
 | |
|     ///
 | |
|     /// Go see the IsAvailable field if you want to get real the status of the membership.
 | |
|     /// </summary>
 | |
|     public bool IsActive { get; set; } = true;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Indicates is the current user got the membership for free,
 | |
|     /// to prevent giving the same discount for the same user again.
 | |
|     /// </summary>
 | |
|     public bool IsFreeTrial { get; set; }
 | |
| 
 | |
|     public SubscriptionStatus Status { get; set; } = SubscriptionStatus.Unpaid;
 | |
| 
 | |
|     [MaxLength(4096)] public string PaymentMethod { get; set; } = null!;
 | |
|     [Column(TypeName = "jsonb")] public SnPaymentDetails PaymentDetails { get; set; } = null!;
 | |
|     public decimal BasePrice { get; set; }
 | |
|     public Guid? CouponId { get; set; }
 | |
|     public SnWalletCoupon? Coupon { get; set; }
 | |
|     public Instant? RenewalAt { get; set; }
 | |
| 
 | |
|     public Guid AccountId { get; set; }
 | |
|     public SnAccount Account { get; set; } = null!;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// If this subscription was redeemed from a gift, this references the gift record.
 | |
|     /// </summary>
 | |
|     public SnWalletGift? Gift { get; set; }
 | |
| 
 | |
|     [NotMapped]
 | |
|     public bool IsAvailable
 | |
|     {
 | |
|         get => IsAvailableAt(SystemClock.Instance.GetCurrentInstant());
 | |
|     }
 | |
| 
 | |
|     [NotMapped]
 | |
|     public decimal FinalPrice
 | |
|     {
 | |
|         get => CalculateFinalPriceAt(SystemClock.Instance.GetCurrentInstant());
 | |
|     }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Optimized method to check availability at a specific instant (avoids repeated SystemClock calls).
 | |
|     /// </summary>
 | |
|     public bool IsAvailableAt(Instant currentInstant)
 | |
|     {
 | |
|         if (!IsActive) return false;
 | |
|         if (BegunAt > currentInstant) return false;
 | |
|         if (EndedAt.HasValue && currentInstant > EndedAt.Value) return false;
 | |
|         if (RenewalAt.HasValue && currentInstant > RenewalAt.Value) return false;
 | |
|         if (Status != SubscriptionStatus.Active) return false;
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Optimized method to calculate final price at a specific instant (avoids repeated SystemClock calls).
 | |
|     /// </summary>
 | |
|     public decimal CalculateFinalPriceAt(Instant currentInstant)
 | |
|     {
 | |
|         if (IsFreeTrial) return 0;
 | |
|         if (Coupon == null) return BasePrice;
 | |
| 
 | |
|         if (Coupon.AffectedAt.HasValue && currentInstant < Coupon.AffectedAt.Value ||
 | |
|             Coupon.ExpiredAt.HasValue && currentInstant > Coupon.ExpiredAt.Value) return BasePrice;
 | |
| 
 | |
|         if (Coupon.DiscountAmount.HasValue) return BasePrice - Coupon.DiscountAmount.Value;
 | |
|         if (Coupon.DiscountRate.HasValue) return BasePrice * (decimal)(1 - Coupon.DiscountRate.Value);
 | |
|         return BasePrice;
 | |
|     }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Returns a reference object that contains a subset of subscription data
 | |
|     /// suitable for client-side use, with sensitive information removed.
 | |
|     /// </summary>
 | |
|     public SnSubscriptionReferenceObject ToReference()
 | |
|     {
 | |
|         // Cache the current instant once to avoid multiple SystemClock calls
 | |
|         var currentInstant = SystemClock.Instance.GetCurrentInstant();
 | |
| 
 | |
|         return new SnSubscriptionReferenceObject
 | |
|         {
 | |
|             Id = Id,
 | |
|             Identifier = Identifier,
 | |
|             BegunAt = BegunAt,
 | |
|             EndedAt = EndedAt,
 | |
|             IsActive = IsActive,
 | |
|             IsAvailable = IsAvailableAt(currentInstant),
 | |
|             IsFreeTrial = IsFreeTrial,
 | |
|             Status = Status,
 | |
|             BasePrice = BasePrice,
 | |
|             FinalPrice = CalculateFinalPriceAt(currentInstant),
 | |
|             RenewalAt = RenewalAt,
 | |
|             AccountId = AccountId
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     public Proto.Subscription ToProtoValue() => new()
 | |
|     {
 | |
|         Id = Id.ToString(),
 | |
|         BegunAt = BegunAt.ToTimestamp(),
 | |
|         EndedAt = EndedAt?.ToTimestamp(),
 | |
|         Identifier = Identifier,
 | |
|         IsActive = IsActive,
 | |
|         IsFreeTrial = IsFreeTrial,
 | |
|         Status = (Proto.SubscriptionStatus)Status,
 | |
|         PaymentMethod = PaymentMethod,
 | |
|         PaymentDetails = PaymentDetails.ToProtoValue(),
 | |
|         BasePrice = BasePrice.ToString(CultureInfo.InvariantCulture),
 | |
|         CouponId = CouponId?.ToString(),
 | |
|         Coupon = Coupon?.ToProtoValue(),
 | |
|         RenewalAt = RenewalAt?.ToTimestamp(),
 | |
|         AccountId = AccountId.ToString(),
 | |
|         IsAvailable = IsAvailable,
 | |
|         FinalPrice = FinalPrice.ToString(CultureInfo.InvariantCulture),
 | |
|         CreatedAt = CreatedAt.ToTimestamp(),
 | |
|         UpdatedAt = UpdatedAt.ToTimestamp()
 | |
|     };
 | |
| 
 | |
|     public static SnWalletSubscription FromProtoValue(Proto.Subscription proto) => new()
 | |
|     {
 | |
|         Id = Guid.Parse(proto.Id),
 | |
|         BegunAt = proto.BegunAt.ToInstant(),
 | |
|         EndedAt = proto.EndedAt?.ToInstant(),
 | |
|         Identifier = proto.Identifier,
 | |
|         IsActive = proto.IsActive,
 | |
|         IsFreeTrial = proto.IsFreeTrial,
 | |
|         Status = (SubscriptionStatus)proto.Status,
 | |
|         PaymentMethod = proto.PaymentMethod,
 | |
|         PaymentDetails = SnPaymentDetails.FromProtoValue(proto.PaymentDetails),
 | |
|         BasePrice = decimal.Parse(proto.BasePrice),
 | |
|         CouponId = proto.HasCouponId ? Guid.Parse(proto.CouponId) : null,
 | |
|         Coupon = proto.Coupon is not null ? SnWalletCoupon.FromProtoValue(proto.Coupon) : null,
 | |
|         RenewalAt = proto.RenewalAt?.ToInstant(),
 | |
|         AccountId = Guid.Parse(proto.AccountId),
 | |
|         CreatedAt = proto.CreatedAt.ToInstant(),
 | |
|         UpdatedAt = proto.UpdatedAt.ToInstant()
 | |
|     };
 | |
| }
 | |
| 
 | |
| /// <summary>
 | |
| /// A reference object for Subscription that contains only non-sensitive information
 | |
| /// suitable for client-side use.
 | |
| /// </summary>
 | |
| public class SnSubscriptionReferenceObject : ModelBase
 | |
| {
 | |
|     public Guid Id { get; set; }
 | |
|     public string Identifier { get; set; } = null!;
 | |
|     public Instant BegunAt { get; set; }
 | |
|     public Instant? EndedAt { get; set; }
 | |
|     public bool IsActive { get; set; }
 | |
|     public bool IsAvailable { get; set; }
 | |
|     public bool IsFreeTrial { get; set; }
 | |
|     public SubscriptionStatus Status { get; set; }
 | |
|     public decimal BasePrice { get; set; }
 | |
|     public decimal FinalPrice { get; set; }
 | |
|     public Instant? RenewalAt { get; set; }
 | |
|     public Guid AccountId { get; set; }
 | |
| 
 | |
|     private string? _displayName;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Gets the human-readable name of the subscription type if available (cached for performance).
 | |
|     /// </summary>
 | |
|     [NotMapped]
 | |
|     public string? DisplayName => _displayName ??= SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(Identifier, out var name)
 | |
|         ? name
 | |
|         : null;
 | |
| 
 | |
|     public Proto.SubscriptionReferenceObject ToProtoValue() => new()
 | |
|     {
 | |
|         Id = Id.ToString(),
 | |
|         Identifier = Identifier,
 | |
|         BegunAt = BegunAt.ToTimestamp(),
 | |
|         EndedAt = EndedAt?.ToTimestamp(),
 | |
|         IsActive = IsActive,
 | |
|         IsAvailable = IsAvailable,
 | |
|         IsFreeTrial = IsFreeTrial,
 | |
|         Status = (Proto.SubscriptionStatus)Status,
 | |
|         BasePrice = BasePrice.ToString(CultureInfo.InvariantCulture),
 | |
|         FinalPrice = FinalPrice.ToString(CultureInfo.InvariantCulture),
 | |
|         RenewalAt = RenewalAt?.ToTimestamp(),
 | |
|         AccountId = AccountId.ToString(),
 | |
|         DisplayName = DisplayName,
 | |
|         CreatedAt = CreatedAt.ToTimestamp(),
 | |
|         UpdatedAt = UpdatedAt.ToTimestamp()
 | |
|     };
 | |
| 
 | |
|     public static SnSubscriptionReferenceObject FromProtoValue(Proto.SubscriptionReferenceObject proto) => new()
 | |
|     {
 | |
|         Id = Guid.Parse(proto.Id),
 | |
|         Identifier = proto.Identifier,
 | |
|         BegunAt = proto.BegunAt.ToInstant(),
 | |
|         EndedAt = proto.EndedAt?.ToInstant(),
 | |
|         IsActive = proto.IsActive,
 | |
|         IsAvailable = proto.IsAvailable,
 | |
|         IsFreeTrial = proto.IsFreeTrial,
 | |
|         Status = (SubscriptionStatus)proto.Status,
 | |
|         BasePrice = decimal.Parse(proto.BasePrice),
 | |
|         FinalPrice = decimal.Parse(proto.FinalPrice),
 | |
|         RenewalAt = proto.RenewalAt?.ToInstant(),
 | |
|         AccountId = Guid.Parse(proto.AccountId),
 | |
|         CreatedAt = proto.CreatedAt.ToInstant(),
 | |
|         UpdatedAt = proto.UpdatedAt.ToInstant()
 | |
|     };
 | |
| }
 | |
| 
 | |
| public class SnPaymentDetails
 | |
| {
 | |
|     public string Currency { get; set; } = null!;
 | |
|     public string? OrderId { get; set; }
 | |
| 
 | |
|     public Proto.PaymentDetails ToProtoValue() => new()
 | |
|     {
 | |
|         Currency = Currency,
 | |
|         OrderId = OrderId,
 | |
|     };
 | |
| 
 | |
|     public static SnPaymentDetails FromProtoValue(Proto.PaymentDetails proto) => new()
 | |
|     {
 | |
|         Currency = proto.Currency,
 | |
|         OrderId = proto.OrderId,
 | |
|     };
 | |
| }
 | |
| 
 | |
| /// <summary>
 | |
| /// A discount that can applies in purchases among the Solar Network.
 | |
| /// For now, it can be used in the subscription purchase.
 | |
| /// </summary>
 | |
| public class SnWalletCoupon : ModelBase
 | |
| {
 | |
|     public Guid Id { get; set; } = Guid.NewGuid();
 | |
| 
 | |
|     /// <summary>
 | |
|     /// The items that can apply this coupon.
 | |
|     /// Leave it to null to apply to all items.
 | |
|     /// </summary>
 | |
|     [MaxLength(4096)]
 | |
|     public string? Identifier { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// The code that human-readable and memorizable.
 | |
|     /// Leave it blank to use it only with the ID.
 | |
|     /// </summary>
 | |
|     [MaxLength(1024)]
 | |
|     public string? Code { get; set; }
 | |
| 
 | |
|     public Instant? AffectedAt { get; set; }
 | |
|     public Instant? ExpiredAt { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// The amount of the discount.
 | |
|     /// If this field and the rate field are both not null,
 | |
|     /// the amount discount will be applied and the discount rate will be ignored.
 | |
|     /// Formula: <code>final price = base price - discount amount</code>
 | |
|     /// </summary>
 | |
|     public decimal? DiscountAmount { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// The percentage of the discount.
 | |
|     /// If this field and the amount field are both not null,
 | |
|     /// this field will be ignored.
 | |
|     /// Formula: <code>final price = base price * (1 - discount rate)</code>
 | |
|     /// </summary>
 | |
|     public double? DiscountRate { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// The max usage of the current coupon.
 | |
|     /// Leave it to null to use it unlimited.
 | |
|     /// </summary>
 | |
|     public int? MaxUsage { get; set; }
 | |
| 
 | |
|     public Proto.Coupon ToProtoValue() => new()
 | |
|     {
 | |
|         Id = Id.ToString(),
 | |
|         Identifier = Identifier,
 | |
|         Code = Code,
 | |
|         AffectedAt = AffectedAt?.ToTimestamp(),
 | |
|         ExpiredAt = ExpiredAt?.ToTimestamp(),
 | |
|         DiscountAmount = DiscountAmount?.ToString(),
 | |
|         DiscountRate = DiscountRate,
 | |
|         MaxUsage = MaxUsage,
 | |
|         CreatedAt = CreatedAt.ToTimestamp(),
 | |
|         UpdatedAt = UpdatedAt.ToTimestamp()
 | |
|     };
 | |
| 
 | |
|     public static SnWalletCoupon FromProtoValue(Proto.Coupon proto) => new()
 | |
|     {
 | |
|         Id = Guid.Parse(proto.Id),
 | |
|         Identifier = proto.Identifier,
 | |
|         Code = proto.Code,
 | |
|         AffectedAt = proto.AffectedAt?.ToInstant(),
 | |
|         ExpiredAt = proto.ExpiredAt?.ToInstant(),
 | |
|         DiscountAmount = proto.HasDiscountAmount ? decimal.Parse(proto.DiscountAmount) : null,
 | |
|         DiscountRate = proto.DiscountRate,
 | |
|         MaxUsage = proto.MaxUsage,
 | |
|         CreatedAt = proto.CreatedAt.ToInstant(),
 | |
|         UpdatedAt = proto.UpdatedAt.ToInstant()
 | |
|     };
 | |
| }
 |