♻️ Centralized data models (wip)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using DysonNetwork.Pass.Auth;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -10,7 +11,7 @@ namespace DysonNetwork.Pass.Wallet;
|
||||
public class OrderController(PaymentService payment, AuthService auth, AppDatabase db) : ControllerBase
|
||||
{
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<Order>> GetOrderById(Guid id)
|
||||
public async Task<ActionResult<SnWalletOrder>> GetOrderById(Guid id)
|
||||
{
|
||||
var order = await db.PaymentOrders.FindAsync(id);
|
||||
|
||||
@@ -22,7 +23,7 @@ public class OrderController(PaymentService payment, AuthService auth, AppDataba
|
||||
|
||||
[HttpPost("{id:guid}/pay")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Order>> PayOrder(Guid id, [FromBody] PayOrderRequest request)
|
||||
public async Task<ActionResult<SnWalletOrder>> PayOrder(Guid id, [FromBody] PayOrderRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
|
@@ -1,126 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Globalization;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
|
||||
namespace DysonNetwork.Pass.Wallet;
|
||||
|
||||
public class WalletCurrency
|
||||
{
|
||||
public const string SourcePoint = "points";
|
||||
public const string GoldenPoint = "golds";
|
||||
}
|
||||
|
||||
public enum OrderStatus
|
||||
{
|
||||
Unpaid,
|
||||
Paid,
|
||||
Cancelled,
|
||||
Finished,
|
||||
Expired
|
||||
}
|
||||
|
||||
public class Order : ModelBase
|
||||
{
|
||||
public const string InternalAppIdentifier = "internal";
|
||||
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public OrderStatus Status { get; set; } = OrderStatus.Unpaid;
|
||||
[MaxLength(128)] public string Currency { get; set; } = null!;
|
||||
[MaxLength(4096)] public string? Remarks { get; set; }
|
||||
[MaxLength(4096)] public string? AppIdentifier { get; set; }
|
||||
[MaxLength(4096)] public string? ProductIdentifier { get; set; }
|
||||
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public Instant ExpiredAt { get; set; }
|
||||
|
||||
public Guid? PayeeWalletId { get; set; }
|
||||
public Wallet? PayeeWallet { get; set; } = null!;
|
||||
public Guid? TransactionId { get; set; }
|
||||
public Transaction? Transaction { get; set; }
|
||||
|
||||
public Shared.Proto.Order ToProtoValue() => new()
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
Status = (Shared.Proto.OrderStatus)Status,
|
||||
Currency = Currency,
|
||||
Remarks = Remarks,
|
||||
AppIdentifier = AppIdentifier,
|
||||
ProductIdentifier = ProductIdentifier,
|
||||
Meta = Meta == null
|
||||
? null
|
||||
: Google.Protobuf.ByteString.CopyFrom(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(Meta)),
|
||||
Amount = Amount.ToString(CultureInfo.InvariantCulture),
|
||||
ExpiredAt = ExpiredAt.ToTimestamp(),
|
||||
PayeeWalletId = PayeeWalletId?.ToString(),
|
||||
TransactionId = TransactionId?.ToString(),
|
||||
Transaction = Transaction?.ToProtoValue(),
|
||||
};
|
||||
|
||||
public static Order FromProtoValue(Shared.Proto.Order proto) => new()
|
||||
{
|
||||
Id = Guid.Parse(proto.Id),
|
||||
Status = (OrderStatus)proto.Status,
|
||||
Currency = proto.Currency,
|
||||
Remarks = proto.Remarks,
|
||||
AppIdentifier = proto.AppIdentifier,
|
||||
ProductIdentifier = proto.ProductIdentifier,
|
||||
Meta = proto.HasMeta
|
||||
? System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(proto.Meta.ToByteArray())
|
||||
: null,
|
||||
Amount = decimal.Parse(proto.Amount),
|
||||
ExpiredAt = proto.ExpiredAt.ToInstant(),
|
||||
PayeeWalletId = proto.PayeeWalletId is not null ? Guid.Parse(proto.PayeeWalletId) : null,
|
||||
TransactionId = proto.TransactionId is not null ? Guid.Parse(proto.TransactionId) : null,
|
||||
Transaction = proto.Transaction is not null ? Transaction.FromProtoValue(proto.Transaction) : null,
|
||||
};
|
||||
}
|
||||
|
||||
public enum TransactionType
|
||||
{
|
||||
System,
|
||||
Transfer,
|
||||
Order
|
||||
}
|
||||
|
||||
public class Transaction : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(128)] public string Currency { get; set; } = null!;
|
||||
public decimal Amount { get; set; }
|
||||
[MaxLength(4096)] public string? Remarks { get; set; }
|
||||
public TransactionType Type { get; set; }
|
||||
|
||||
// When the payer is null, it's pay from the system
|
||||
public Guid? PayerWalletId { get; set; }
|
||||
|
||||
public Wallet? PayerWallet { get; set; }
|
||||
|
||||
// When the payee is null, it's pay for the system
|
||||
public Guid? PayeeWalletId { get; set; }
|
||||
public Wallet? PayeeWallet { get; set; }
|
||||
|
||||
public Shared.Proto.Transaction ToProtoValue() => new()
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
Currency = Currency,
|
||||
Amount = Amount.ToString(CultureInfo.InvariantCulture),
|
||||
Remarks = Remarks,
|
||||
Type = (Shared.Proto.TransactionType)Type,
|
||||
PayerWalletId = PayerWalletId?.ToString(),
|
||||
PayeeWalletId = PayeeWalletId?.ToString(),
|
||||
};
|
||||
|
||||
public static Transaction FromProtoValue(Shared.Proto.Transaction proto) => new()
|
||||
{
|
||||
Id = Guid.Parse(proto.Id),
|
||||
Currency = proto.Currency,
|
||||
Amount = decimal.Parse(proto.Amount),
|
||||
Remarks = proto.Remarks,
|
||||
Type = (TransactionType)proto.Type,
|
||||
PayerWalletId = proto.PayerWalletId is not null ? Guid.Parse(proto.PayerWalletId) : null,
|
||||
PayeeWalletId = proto.PayeeWalletId is not null ? Guid.Parse(proto.PayeeWalletId) : null,
|
||||
};
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Pass.Localization;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Stream;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
using NodaTime;
|
||||
|
@@ -1,405 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Globalization;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
|
||||
namespace DysonNetwork.Pass.Wallet;
|
||||
|
||||
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,
|
||||
3
|
||||
),
|
||||
[SubscriptionType.Nova] = new SubscriptionTypeData(
|
||||
SubscriptionType.Nova,
|
||||
SubscriptionType.StellarProgram,
|
||||
WalletCurrency.SourcePoint,
|
||||
2400,
|
||||
6
|
||||
),
|
||||
[SubscriptionType.Supernova] = new SubscriptionTypeData(
|
||||
SubscriptionType.Supernova,
|
||||
SubscriptionType.StellarProgram,
|
||||
WalletCurrency.SourcePoint,
|
||||
3600,
|
||||
9
|
||||
)
|
||||
};
|
||||
|
||||
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"
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The subscription is for the Stellar Program in most cases.
|
||||
/// The paid subscription in another word.
|
||||
/// </summary>
|
||||
[Index(nameof(Identifier))]
|
||||
public class Subscription : 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 PaymentDetails PaymentDetails { get; set; } = null!;
|
||||
public decimal BasePrice { get; set; }
|
||||
public Guid? CouponId { get; set; }
|
||||
public Coupon? Coupon { get; set; }
|
||||
public Instant? RenewalAt { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
public Account.Account Account { get; set; } = null!;
|
||||
|
||||
[NotMapped]
|
||||
public bool IsAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsActive) return false;
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
if (BegunAt > now) return false;
|
||||
if (EndedAt.HasValue && now > EndedAt.Value) return false;
|
||||
if (RenewalAt.HasValue && now > RenewalAt.Value) return false;
|
||||
if (Status != SubscriptionStatus.Active) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
public decimal FinalPrice
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsFreeTrial) return 0;
|
||||
if (Coupon == null) return BasePrice;
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
if (Coupon.AffectedAt.HasValue && now < Coupon.AffectedAt.Value ||
|
||||
Coupon.ExpiredAt.HasValue && now > 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 SubscriptionReferenceObject ToReference()
|
||||
{
|
||||
return new SubscriptionReferenceObject
|
||||
{
|
||||
Id = Id,
|
||||
Identifier = Identifier,
|
||||
BegunAt = BegunAt,
|
||||
EndedAt = EndedAt,
|
||||
IsActive = IsActive,
|
||||
IsAvailable = IsAvailable,
|
||||
IsFreeTrial = IsFreeTrial,
|
||||
Status = Status,
|
||||
BasePrice = BasePrice,
|
||||
FinalPrice = FinalPrice,
|
||||
RenewalAt = RenewalAt,
|
||||
AccountId = AccountId
|
||||
};
|
||||
}
|
||||
|
||||
public Shared.Proto.Subscription ToProtoValue() => new()
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
BegunAt = BegunAt.ToTimestamp(),
|
||||
EndedAt = EndedAt?.ToTimestamp(),
|
||||
Identifier = Identifier,
|
||||
IsActive = IsActive,
|
||||
IsFreeTrial = IsFreeTrial,
|
||||
Status = (Shared.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 Subscription FromProtoValue(Shared.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 = PaymentDetails.FromProtoValue(proto.PaymentDetails),
|
||||
BasePrice = decimal.Parse(proto.BasePrice),
|
||||
CouponId = proto.HasCouponId ? Guid.Parse(proto.CouponId) : null,
|
||||
Coupon = proto.Coupon is not null ? Coupon.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 SubscriptionReferenceObject : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Identifier { get; set; } = null!;
|
||||
public Instant BegunAt { get; set; }
|
||||
public Instant? EndedAt { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public bool IsAvailable { get; set; }
|
||||
public bool IsFreeTrial { get; set; }
|
||||
public SubscriptionStatus Status { get; set; }
|
||||
public decimal BasePrice { get; set; }
|
||||
public decimal FinalPrice { get; set; }
|
||||
public Instant? RenewalAt { get; set; }
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the human-readable name of the subscription type if available.
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public string? DisplayName => SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(Identifier, out var name)
|
||||
? name
|
||||
: null;
|
||||
|
||||
public Shared.Proto.SubscriptionReferenceObject ToProtoValue() => new()
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
Identifier = Identifier,
|
||||
BegunAt = BegunAt.ToTimestamp(),
|
||||
EndedAt = EndedAt?.ToTimestamp(),
|
||||
IsActive = IsActive,
|
||||
IsAvailable = IsAvailable,
|
||||
IsFreeTrial = IsFreeTrial,
|
||||
Status = (Shared.Proto.SubscriptionStatus)Status,
|
||||
BasePrice = BasePrice.ToString(CultureInfo.CurrentCulture),
|
||||
FinalPrice = FinalPrice.ToString(CultureInfo.CurrentCulture),
|
||||
RenewalAt = RenewalAt?.ToTimestamp(),
|
||||
AccountId = AccountId.ToString(),
|
||||
DisplayName = DisplayName,
|
||||
CreatedAt = CreatedAt.ToTimestamp(),
|
||||
UpdatedAt = UpdatedAt.ToTimestamp()
|
||||
};
|
||||
|
||||
public static SubscriptionReferenceObject FromProtoValue(Shared.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 PaymentDetails
|
||||
{
|
||||
public string Currency { get; set; } = null!;
|
||||
public string? OrderId { get; set; }
|
||||
|
||||
public Shared.Proto.PaymentDetails ToProtoValue() => new()
|
||||
{
|
||||
Currency = Currency,
|
||||
OrderId = OrderId,
|
||||
};
|
||||
|
||||
public static PaymentDetails FromProtoValue(Shared.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 Coupon : 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 Shared.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 Coupon FromProtoValue(Shared.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()
|
||||
};
|
||||
}
|
@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Pass.Wallet.PaymentHandlers;
|
||||
using DysonNetwork.Shared.Models;
|
||||
|
||||
namespace DysonNetwork.Pass.Wallet;
|
||||
|
||||
@@ -14,7 +15,7 @@ public class SubscriptionController(SubscriptionService subscriptions, AfdianPay
|
||||
{
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<Subscription>>> ListSubscriptions(
|
||||
public async Task<ActionResult<List<SnSubscription>>> ListSubscriptions(
|
||||
[FromQuery] int offset = 0,
|
||||
[FromQuery] int take = 20
|
||||
)
|
||||
@@ -40,7 +41,7 @@ public class SubscriptionController(SubscriptionService subscriptions, AfdianPay
|
||||
|
||||
[HttpGet("fuzzy/{prefix}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Subscription>> GetSubscriptionFuzzy(string prefix)
|
||||
public async Task<ActionResult<SnSubscription>> GetSubscriptionFuzzy(string prefix)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
@@ -56,7 +57,7 @@ public class SubscriptionController(SubscriptionService subscriptions, AfdianPay
|
||||
|
||||
[HttpGet("{identifier}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Subscription>> GetSubscription(string identifier)
|
||||
public async Task<ActionResult<SnSubscription>> GetSubscription(string identifier)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
@@ -79,7 +80,7 @@ public class SubscriptionController(SubscriptionService subscriptions, AfdianPay
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Subscription>> CreateSubscription(
|
||||
public async Task<ActionResult<SnSubscription>> CreateSubscription(
|
||||
[FromBody] CreateSubscriptionRequest request,
|
||||
[FromHeader(Name = "X-Noop")] bool noop = false
|
||||
)
|
||||
@@ -118,7 +119,7 @@ public class SubscriptionController(SubscriptionService subscriptions, AfdianPay
|
||||
|
||||
[HttpPost("{identifier}/cancel")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Subscription>> CancelSubscription(string identifier)
|
||||
public async Task<ActionResult<SnSubscription>> CancelSubscription(string identifier)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
@@ -135,7 +136,7 @@ public class SubscriptionController(SubscriptionService subscriptions, AfdianPay
|
||||
|
||||
[HttpPost("{identifier}/order")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Order>> CreateSubscriptionOrder(string identifier)
|
||||
public async Task<ActionResult<SnWalletOrder>> CreateSubscriptionOrder(string identifier)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
|
@@ -4,6 +4,7 @@ using System.Text.Json;
|
||||
using DysonNetwork.Pass.Localization;
|
||||
using DysonNetwork.Pass.Wallet.PaymentHandlers;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@@ -1,66 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
|
||||
namespace DysonNetwork.Pass.Wallet;
|
||||
|
||||
public class Wallet : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
public ICollection<WalletPocket> Pockets { get; set; } = new List<WalletPocket>();
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
public Account.Account Account { get; set; } = null!;
|
||||
|
||||
public Shared.Proto.Wallet ToProtoValue()
|
||||
{
|
||||
var proto = new Shared.Proto.Wallet
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
AccountId = AccountId.ToString(),
|
||||
};
|
||||
|
||||
foreach (var pocket in Pockets)
|
||||
{
|
||||
proto.Pockets.Add(pocket.ToProtoValue());
|
||||
}
|
||||
|
||||
return proto;
|
||||
}
|
||||
|
||||
public static Wallet FromProtoValue(Shared.Proto.Wallet proto) => new()
|
||||
{
|
||||
Id = Guid.Parse(proto.Id),
|
||||
AccountId = Guid.Parse(proto.AccountId),
|
||||
Pockets = proto.Pockets.Select(WalletPocket.FromProtoValue).ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
public class WalletPocket : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(128)] public string Currency { get; set; } = null!;
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
public Guid WalletId { get; set; }
|
||||
[JsonIgnore] public Wallet Wallet { get; set; } = null!;
|
||||
|
||||
public Shared.Proto.WalletPocket ToProtoValue() => new()
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
Currency = Currency,
|
||||
Amount = Amount.ToString(CultureInfo.CurrentCulture),
|
||||
WalletId = WalletId.ToString(),
|
||||
};
|
||||
|
||||
public static WalletPocket FromProtoValue(Shared.Proto.WalletPocket proto) => new()
|
||||
{
|
||||
Id = Guid.Parse(proto.Id),
|
||||
Currency = proto.Currency,
|
||||
Amount = decimal.Parse(proto.Amount),
|
||||
WalletId = Guid.Parse(proto.WalletId),
|
||||
};
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Pass.Permission;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -40,7 +41,7 @@ public class WalletController(AppDatabase db, WalletService ws, PaymentService p
|
||||
|
||||
[HttpGet("transactions")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<Transaction>>> GetTransactions(
|
||||
public async Task<ActionResult<List<SnWalletTransaction>>> GetTransactions(
|
||||
[FromQuery] int offset = 0, [FromQuery] int take = 20
|
||||
)
|
||||
{
|
||||
@@ -67,7 +68,7 @@ public class WalletController(AppDatabase db, WalletService ws, PaymentService p
|
||||
|
||||
[HttpGet("orders")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<Order>>> GetOrders(
|
||||
public async Task<ActionResult<List<SnWalletOrder>>> GetOrders(
|
||||
[FromQuery] int offset = 0, [FromQuery] int take = 20
|
||||
)
|
||||
{
|
||||
@@ -104,7 +105,7 @@ public class WalletController(AppDatabase db, WalletService ws, PaymentService p
|
||||
[HttpPost("balance")]
|
||||
[Authorize]
|
||||
[RequiredPermission("maintenance", "wallets.balance.modify")]
|
||||
public async Task<ActionResult<Transaction>> ModifyWalletBalance([FromBody] WalletBalanceRequest request)
|
||||
public async Task<ActionResult<SnWalletTransaction>> ModifyWalletBalance([FromBody] WalletBalanceRequest request)
|
||||
{
|
||||
var wallet = await ws.GetWalletAsync(request.AccountId);
|
||||
if (wallet is null) return NotFound("Wallet was not found.");
|
||||
|
@@ -1,3 +1,4 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Pass.Wallet;
|
||||
@@ -27,7 +28,7 @@ public class WalletService(AppDatabase db)
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public async Task<(WalletPocket wallet, bool isNewlyCreated)> GetOrCreateWalletPocketAsync(
|
||||
public async Task<(SnWalletPocket wallet, bool isNewlyCreated)> GetOrCreateWalletPocketAsync(
|
||||
Guid walletId,
|
||||
string currency,
|
||||
decimal? initialAmount = null
|
||||
@@ -36,7 +37,7 @@ public class WalletService(AppDatabase db)
|
||||
var pocket = await db.WalletPockets.FirstOrDefaultAsync(p => p.Currency == currency && p.WalletId == walletId);
|
||||
if (pocket != null) return (pocket, false);
|
||||
|
||||
pocket = new WalletPocket
|
||||
pocket = new SnWalletPocket
|
||||
{
|
||||
Currency = currency,
|
||||
Amount = initialAmount ?? 0,
|
||||
|
Reference in New Issue
Block a user