✨ Check in rewards NSD & payment
This commit is contained in:
parent
d7d4fde06a
commit
0db003abc2
@ -318,10 +318,10 @@ public class AccountController(
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
|
||||
var today = SystemClock.Instance.GetCurrentInstant().InUtc().Date;
|
||||
var localTime = new TimeOnly(0, 0);
|
||||
var startOfDay = today.ToDateOnly().ToDateTime(localTime).ToUniversalTime().ToInstant();
|
||||
var endOfDay = today.PlusDays(1).ToDateOnly().ToDateTime(localTime).ToUniversalTime().ToInstant();
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var today = now.InUtc().Date;
|
||||
var startOfDay = today.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant();
|
||||
var endOfDay = today.PlusDays(1).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant();
|
||||
|
||||
var result = await db.AccountCheckInResults
|
||||
.Where(x => x.AccountId == userId)
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Globalization;
|
||||
using DysonNetwork.Sphere.Activity;
|
||||
using DysonNetwork.Sphere.Connection;
|
||||
using DysonNetwork.Sphere.Wallet;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Localization;
|
||||
@ -13,6 +14,7 @@ public class AccountEventService(
|
||||
ActivityService act,
|
||||
WebSocketService ws,
|
||||
IMemoryCache cache,
|
||||
PaymentService payment,
|
||||
IStringLocalizer<Localization.AccountEventResource> localizer
|
||||
)
|
||||
{
|
||||
@ -163,12 +165,29 @@ public class AccountEventService(
|
||||
{
|
||||
Tips = tips,
|
||||
Level = (CheckInResultLevel)Random.Next(Enum.GetValues<CheckInResultLevel>().Length),
|
||||
AccountId = user.Id
|
||||
AccountId = user.Id,
|
||||
RewardExperience = 100,
|
||||
RewardPoints = 10,
|
||||
};
|
||||
|
||||
db.AccountCheckInResults.Add(result);
|
||||
await db.SaveChangesAsync();
|
||||
var now = SystemClock.Instance.GetCurrentInstant().InUtc().Date;
|
||||
try
|
||||
{
|
||||
if (result.RewardPoints.HasValue)
|
||||
await payment.CreateTransactionWithAccountAsync(
|
||||
null,
|
||||
user.Id,
|
||||
WalletCurrency.SourcePoint,
|
||||
result.RewardPoints.Value,
|
||||
$"Check-in reward on {now:yyyy/MM/dd}"
|
||||
);
|
||||
}
|
||||
catch
|
||||
{
|
||||
result.RewardPoints = null;
|
||||
}
|
||||
|
||||
db.AccountCheckInResults.Add(result);
|
||||
await act.CreateActivity(
|
||||
user,
|
||||
"accounts.check-in",
|
||||
|
@ -39,6 +39,8 @@ public class CheckInResult : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public CheckInResultLevel Level { get; set; }
|
||||
public decimal? RewardPoints { get; set; }
|
||||
public int? RewardExperience { get; set; }
|
||||
[Column(TypeName = "jsonb")] public ICollection<FortuneTip> Tips { get; set; } = new List<FortuneTip>();
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
|
@ -74,6 +74,7 @@ public class AppDatabase(
|
||||
public DbSet<Wallet.Transaction> PaymentTransactions { get; set; }
|
||||
|
||||
public DbSet<Developer.CustomApp> CustomApps { get; set; }
|
||||
public DbSet<Developer.CustomAppSecret> CustomAppSecrets { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
|
@ -66,7 +66,7 @@ public class AuthController(
|
||||
return challenge;
|
||||
}
|
||||
|
||||
[HttpGet("challenge/{id}/factors")]
|
||||
[HttpGet("challenge/{id:guid}/factors")]
|
||||
public async Task<ActionResult<List<AccountAuthFactor>>> GetChallengeFactors([FromRoute] Guid id)
|
||||
{
|
||||
var challenge = await db.AuthChallenges
|
||||
@ -78,7 +78,7 @@ public class AuthController(
|
||||
: challenge.Account.AuthFactors.ToList();
|
||||
}
|
||||
|
||||
[HttpPost("challenge/{id}/factors/{factorId:guid}")]
|
||||
[HttpPost("challenge/{id:guid}/factors/{factorId:guid}")]
|
||||
public async Task<ActionResult> RequestFactorCode([FromRoute] Guid id, [FromRoute] Guid factorId)
|
||||
{
|
||||
var challenge = await db.AuthChallenges
|
||||
@ -97,11 +97,11 @@ public class AuthController(
|
||||
|
||||
public class PerformChallengeRequest
|
||||
{
|
||||
[Required] public long FactorId { get; set; }
|
||||
[Required] public Guid FactorId { get; set; }
|
||||
[Required] public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[HttpPatch("challenge/{id}")]
|
||||
[HttpPatch("challenge/{id:guid}")]
|
||||
public async Task<ActionResult<Challenge>> DoChallenge(
|
||||
[FromRoute] Guid id,
|
||||
[FromBody] PerformChallengeRequest request
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Newtonsoft.Json;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Developer;
|
||||
@ -20,6 +21,19 @@ public class CustomApp : ModelBase
|
||||
public Instant? VerifiedAt { get; set; }
|
||||
[MaxLength(4096)] public string? VerifiedAs { get; set; }
|
||||
|
||||
[JsonIgnore] private ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
|
||||
|
||||
public Guid PublisherId { get; set; }
|
||||
public Publisher.Publisher Developer { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class CustomAppSecret : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(1024)] public string Secret { get; set; } = null!;
|
||||
[MaxLength(4096)] public string? Remarks { get; set; } = null!;
|
||||
public Instant? ExpiredAt { get; set; }
|
||||
|
||||
public Guid AppId { get; set; }
|
||||
public CustomApp App { get; set; } = null!;
|
||||
}
|
3282
DysonNetwork.Sphere/Migrations/20250514164520_AddCustomAppsAndSecrets.Designer.cs
generated
Normal file
3282
DysonNetwork.Sphere/Migrations/20250514164520_AddCustomAppsAndSecrets.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Sphere.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddCustomAppsAndSecrets : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "issuer_app_id",
|
||||
table: "payment_orders",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "custom_app_secrets",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
secret = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||
remarks = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
|
||||
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
app_id = table.Column<Guid>(type: "uuid", 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_custom_app_secrets", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_custom_app_secrets_custom_apps_app_id",
|
||||
column: x => x.app_id,
|
||||
principalTable: "custom_apps",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_payment_orders_issuer_app_id",
|
||||
table: "payment_orders",
|
||||
column: "issuer_app_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_custom_app_secrets_app_id",
|
||||
table: "custom_app_secrets",
|
||||
column: "app_id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_payment_orders_custom_apps_issuer_app_id",
|
||||
table: "payment_orders",
|
||||
column: "issuer_app_id",
|
||||
principalTable: "custom_apps",
|
||||
principalColumn: "id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_payment_orders_custom_apps_issuer_app_id",
|
||||
table: "payment_orders");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "custom_app_secrets");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_payment_orders_issuer_app_id",
|
||||
table: "payment_orders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "issuer_app_id",
|
||||
table: "payment_orders");
|
||||
}
|
||||
}
|
||||
}
|
3290
DysonNetwork.Sphere/Migrations/20250514175019_AddRewardToCheckIn.Designer.cs
generated
Normal file
3290
DysonNetwork.Sphere/Migrations/20250514175019_AddRewardToCheckIn.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Sphere.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddRewardToCheckIn : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "reward_experience",
|
||||
table: "account_check_in_results",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "reward_points",
|
||||
table: "account_check_in_results",
|
||||
type: "numeric",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "reward_experience",
|
||||
table: "account_check_in_results");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "reward_points",
|
||||
table: "account_check_in_results");
|
||||
}
|
||||
}
|
||||
}
|
@ -249,6 +249,14 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("level");
|
||||
|
||||
b.Property<int?>("RewardExperience")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("reward_experience");
|
||||
|
||||
b.Property<decimal?>("RewardPoints")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("reward_points");
|
||||
|
||||
b.Property<ICollection<FortuneTip>>("Tips")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
@ -1197,6 +1205,53 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
b.ToTable("custom_apps", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Developer.CustomAppSecret", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AppId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("app_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?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Remarks")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("remarks");
|
||||
|
||||
b.Property<string>("Secret")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("secret");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_custom_app_secrets");
|
||||
|
||||
b.HasIndex("AppId")
|
||||
.HasDatabaseName("ix_custom_app_secrets_app_id");
|
||||
|
||||
b.ToTable("custom_app_secrets", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionGroup", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -2177,6 +2232,10 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Guid?>("IssuerAppId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("issuer_app_id");
|
||||
|
||||
b.Property<Guid>("PayeeWalletId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("payee_wallet_id");
|
||||
@ -2201,6 +2260,9 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_payment_orders");
|
||||
|
||||
b.HasIndex("IssuerAppId")
|
||||
.HasDatabaseName("ix_payment_orders_issuer_app_id");
|
||||
|
||||
b.HasIndex("PayeeWalletId")
|
||||
.HasDatabaseName("ix_payment_orders_payee_wallet_id");
|
||||
|
||||
@ -2742,6 +2804,18 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
b.Navigation("Developer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Developer.CustomAppSecret", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Sphere.Developer.CustomApp", "App")
|
||||
.WithMany()
|
||||
.HasForeignKey("AppId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_custom_app_secrets_custom_apps_app_id");
|
||||
|
||||
b.Navigation("App");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionGroupMember", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Sphere.Permission.PermissionGroup", "Group")
|
||||
@ -3021,6 +3095,11 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Order", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Sphere.Developer.CustomApp", "IssuerApp")
|
||||
.WithMany()
|
||||
.HasForeignKey("IssuerAppId")
|
||||
.HasConstraintName("fk_payment_orders_custom_apps_issuer_app_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Sphere.Wallet.Wallet", "PayeeWallet")
|
||||
.WithMany()
|
||||
.HasForeignKey("PayeeWalletId")
|
||||
@ -3033,6 +3112,8 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasForeignKey("TransactionId")
|
||||
.HasConstraintName("fk_payment_orders_payment_transactions_transaction_id");
|
||||
|
||||
b.Navigation("IssuerApp");
|
||||
|
||||
b.Navigation("PayeeWallet");
|
||||
|
||||
b.Navigation("Transaction");
|
||||
|
@ -1,8 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Sphere.Developer;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Wallet;
|
||||
|
||||
public class WalletCurrency
|
||||
{
|
||||
public const string SourcePoint = "points";
|
||||
}
|
||||
|
||||
public enum OrderStatus
|
||||
{
|
||||
Unpaid,
|
||||
@ -25,6 +31,8 @@ public class Order : ModelBase
|
||||
public Wallet PayeeWallet { get; set; } = null!;
|
||||
public Guid? TransactionId { get; set; }
|
||||
public Transaction? Transaction { get; set; }
|
||||
public Guid? IssuerAppId { get; set; }
|
||||
public CustomApp? IssuerApp { get; set; }
|
||||
}
|
||||
|
||||
public enum TransactionType
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Wallet;
|
||||
@ -20,6 +21,36 @@ public class PaymentService(AppDatabase db, WalletService wat)
|
||||
return order;
|
||||
}
|
||||
|
||||
public async Task<Transaction> CreateTransactionWithAccountAsync(
|
||||
Guid? payerAccountId,
|
||||
Guid? payeeAccountId,
|
||||
string currency,
|
||||
decimal amount,
|
||||
string? remarks = null,
|
||||
TransactionType type = TransactionType.System
|
||||
)
|
||||
{
|
||||
Wallet? payer = null, payee = null;
|
||||
if (payerAccountId.HasValue)
|
||||
payer = await db.Wallets.FirstOrDefaultAsync(e => e.AccountId == payerAccountId.Value);
|
||||
if (payeeAccountId.HasValue)
|
||||
payee = await db.Wallets.FirstOrDefaultAsync(e => e.AccountId == payeeAccountId.Value);
|
||||
|
||||
if (payer == null && payerAccountId.HasValue)
|
||||
throw new ArgumentException("Payer account was specified, but wallet was not found");
|
||||
if (payee == null && payeeAccountId.HasValue)
|
||||
throw new ArgumentException("Payee account was specified, but wallet was not found");
|
||||
|
||||
return await CreateTransactionAsync(
|
||||
payer?.Id,
|
||||
payee?.Id,
|
||||
currency,
|
||||
amount,
|
||||
remarks,
|
||||
type
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<Transaction> CreateTransactionAsync(
|
||||
Guid? payerWalletId,
|
||||
Guid? payeeWalletId,
|
||||
@ -29,6 +60,10 @@ public class PaymentService(AppDatabase db, WalletService wat)
|
||||
TransactionType type = TransactionType.System
|
||||
)
|
||||
{
|
||||
if (payerWalletId == null && payeeWalletId == null)
|
||||
throw new ArgumentException("At least one wallet must be specified.");
|
||||
if (amount <= 0) throw new ArgumentException("Cannot create transaction with negative or zero amount.");
|
||||
|
||||
var transaction = new Transaction
|
||||
{
|
||||
PayerWalletId = payerWalletId,
|
||||
@ -41,26 +76,28 @@ public class PaymentService(AppDatabase db, WalletService wat)
|
||||
|
||||
if (payerWalletId.HasValue)
|
||||
{
|
||||
var payerPocket = await wat.GetOrCreateWalletPocketAsync(
|
||||
(await db.Wallets.FindAsync(payerWalletId.Value))!.AccountId,
|
||||
currency);
|
||||
var (payerPocket, isNewlyCreated) =
|
||||
await wat.GetOrCreateWalletPocketAsync(payerWalletId.Value, currency);
|
||||
|
||||
if (payerPocket.Amount < amount)
|
||||
{
|
||||
if (isNewlyCreated || payerPocket.Amount < amount)
|
||||
throw new InvalidOperationException("Insufficient funds");
|
||||
}
|
||||
|
||||
payerPocket.Amount -= amount;
|
||||
await db.WalletPockets
|
||||
.Where(p => p.Id == payerPocket.Id && p.Amount >= amount)
|
||||
.ExecuteUpdateAsync(s =>
|
||||
s.SetProperty(p => p.Amount, p => p.Amount - amount));
|
||||
}
|
||||
|
||||
if (payeeWalletId.HasValue)
|
||||
{
|
||||
var payeeWallet = await db.Wallets.FindAsync(payeeWalletId.Value);
|
||||
var payeePocket = await wat.GetOrCreateWalletPocketAsync(
|
||||
payeeWallet!.AccountId,
|
||||
currency);
|
||||
var (payeePocket, isNewlyCreated) =
|
||||
await wat.GetOrCreateWalletPocketAsync(payeeWalletId.Value, currency, amount);
|
||||
|
||||
payeePocket.Amount += amount;
|
||||
if (!isNewlyCreated)
|
||||
await db.WalletPockets
|
||||
.Where(p => p.Id == payeePocket.Id)
|
||||
.ExecuteUpdateAsync(s =>
|
||||
s.SetProperty(p => p.Amount, p => p.Amount + amount));
|
||||
}
|
||||
|
||||
db.PaymentTransactions.Add(transaction);
|
||||
@ -159,7 +196,8 @@ public class PaymentService(AppDatabase db, WalletService wat)
|
||||
return (order, refundTransaction);
|
||||
}
|
||||
|
||||
public async Task<Transaction> TransferAsync(Guid payerAccountId, Guid payeeAccountId, string currency, decimal amount)
|
||||
public async Task<Transaction> TransferAsync(Guid payerAccountId, Guid payeeAccountId, string currency,
|
||||
decimal amount)
|
||||
{
|
||||
var payerWallet = await wat.GetWalletAsync(payerAccountId);
|
||||
if (payerWallet == null)
|
||||
|
63
DysonNetwork.Sphere/Wallet/WalletController.cs
Normal file
63
DysonNetwork.Sphere/Wallet/WalletController.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Sphere.Wallet;
|
||||
|
||||
[ApiController]
|
||||
[Route("/wallets")]
|
||||
public class WalletController(AppDatabase db, WalletService ws) : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Wallet>> CreateWallet()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
var wallet = await ws.CreateWalletAsync(currentUser.Id);
|
||||
return Ok(wallet);
|
||||
}
|
||||
catch (Exception err)
|
||||
{
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Wallet>> GetWallet()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
var wallet = await ws.GetWalletAsync(currentUser.Id);
|
||||
if (wallet is null) return NotFound("Wallet was not found, please create one first.");
|
||||
return Ok(wallet);
|
||||
}
|
||||
|
||||
[HttpGet("transactions")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<Transaction>>> GetTransactions(
|
||||
[FromQuery] int offset = 0, [FromQuery] int take = 20
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
var query = db.PaymentTransactions.AsQueryable()
|
||||
.Include(t => t.PayeeWallet)
|
||||
.Include(t => t.PayerWallet)
|
||||
.Where(t => (t.PayeeWallet != null && t.PayeeWallet.AccountId == currentUser.Id) ||
|
||||
(t.PayerWallet != null && t.PayerWallet.AccountId == currentUser.Id));
|
||||
|
||||
var transactionCount = await query.CountAsync();
|
||||
var transactions = await query
|
||||
.Skip(offset)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
|
||||
Response.Headers["X-Total"] = transactionCount.ToString();
|
||||
|
||||
return Ok(transactions);
|
||||
}
|
||||
}
|
@ -13,10 +13,13 @@ public class WalletService(AppDatabase db)
|
||||
|
||||
public async Task<Wallet> CreateWalletAsync(Guid accountId)
|
||||
{
|
||||
var wallet = new Wallet
|
||||
var existingWallet = await db.Wallets.FirstOrDefaultAsync(w => w.AccountId == accountId);
|
||||
if (existingWallet != null)
|
||||
{
|
||||
AccountId = accountId
|
||||
};
|
||||
throw new InvalidOperationException($"Wallet already exists for account {accountId}");
|
||||
}
|
||||
|
||||
var wallet = new Wallet { AccountId = accountId };
|
||||
|
||||
db.Wallets.Add(wallet);
|
||||
await db.SaveChangesAsync();
|
||||
@ -24,31 +27,23 @@ public class WalletService(AppDatabase db)
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public async Task<WalletPocket> GetOrCreateWalletPocketAsync(Guid accountId, string currency)
|
||||
public async Task<(WalletPocket wallet, bool isNewlyCreated)> GetOrCreateWalletPocketAsync(
|
||||
Guid walletId,
|
||||
string currency,
|
||||
decimal? initialAmount = null
|
||||
)
|
||||
{
|
||||
var wallet = await db.Wallets
|
||||
.Include(w => w.Pockets)
|
||||
.FirstOrDefaultAsync(w => w.AccountId == accountId);
|
||||
|
||||
if (wallet == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Wallet not found for account {accountId}");
|
||||
}
|
||||
|
||||
var pocket = wallet.Pockets.FirstOrDefault(p => p.Currency == currency);
|
||||
|
||||
if (pocket != null) return pocket;
|
||||
var pocket = await db.WalletPockets.FirstOrDefaultAsync(p => p.Currency == currency && p.WalletId == walletId);
|
||||
if (pocket != null) return (pocket, false);
|
||||
|
||||
pocket = new WalletPocket
|
||||
{
|
||||
Currency = currency,
|
||||
Amount = 0,
|
||||
WalletId = wallet.Id
|
||||
Amount = initialAmount ?? 0,
|
||||
WalletId = walletId
|
||||
};
|
||||
|
||||
wallet.Pockets.Add(pocket);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return pocket;
|
||||
db.WalletPockets.Add(pocket);
|
||||
return (pocket, true);
|
||||
}
|
||||
}
|
@ -43,6 +43,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANotFoundResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F28_003F290250f5_003FNotFoundResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANotFound_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ff2c049af93e430aac427e8ff3cc9edd8763d5c9f006d7121ed1c5921585cba_003FNotFound_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANpgsqlEntityTypeBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fccb1faacaea4420db96b09857fc56178a1600_003Fd9_003F9acf9507_003FNpgsqlEntityTypeBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANullable_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003F6a_003Fea17bf26_003FNullable_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOk_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F01d30b32e2ff422cb80129ca2a441c4242600_003F3b_003F237bf104_003FOk_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOptionsConfigurationServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6622dea924b14dc7aa3ee69d7c84e5735000_003Fe0_003F024ba0b7_003FOptionsConfigurationServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fd3_003F7b05b2bd_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
@ -56,6 +57,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionContainerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc0e30e11d8f5456cb7a11b21ebee6c5a35c00_003F60_003F78b485f5_003FServiceCollectionContainerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASetPropertyCalls_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F458b5f22476b4599b87176214d5e4026c2327b148f4d3f885ee92362b4dac3_003FSetPropertyCalls_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASourceCustom_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fdaa8d9c408cd4b4286bbef7e35f1a42e31c00_003F45_003F5839ca6c_003FSourceCustom_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fdf_003F3fcdc4d2_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStatusCodeResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F7c_003F8b7572ae_003FStatusCodeResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATagging_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36f4c2e6baa65ba603de42eedad12ea36845aa35a910a6a82d82baf688e3e1_003FTagging_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003F12_003Fe0a28ad6_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
|
Loading…
x
Reference in New Issue
Block a user