✨ Wallet funds
This commit is contained in:
28
DysonNetwork.Pass/Wallet/FundExpirationJob.cs
Normal file
28
DysonNetwork.Pass/Wallet/FundExpirationJob.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
|
||||
namespace DysonNetwork.Pass.Wallet;
|
||||
|
||||
public class FundExpirationJob(
|
||||
AppDatabase db,
|
||||
PaymentService paymentService,
|
||||
ILogger<FundExpirationJob> logger
|
||||
) : IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
logger.LogInformation("Starting fund expiration job...");
|
||||
|
||||
try
|
||||
{
|
||||
await paymentService.ProcessExpiredFundsAsync();
|
||||
logger.LogInformation("Successfully processed expired funds");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error processing expired funds");
|
||||
}
|
||||
}
|
||||
}
|
@@ -447,4 +447,315 @@ public class PaymentService(
|
||||
$"Transfer from account {payerAccountId} to {payeeAccountId}",
|
||||
Shared.Models.TransactionType.Transfer);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SnWalletFund> CreateFundAsync(
|
||||
Guid creatorAccountId,
|
||||
List<Guid> recipientAccountIds,
|
||||
string currency,
|
||||
decimal totalAmount,
|
||||
Shared.Models.FundSplitType splitType,
|
||||
string? message = null,
|
||||
Duration? expiration = null)
|
||||
{
|
||||
if (recipientAccountIds.Count == 0)
|
||||
throw new ArgumentException("At least one recipient is required");
|
||||
|
||||
if (totalAmount <= 0)
|
||||
throw new ArgumentException("Total amount must be positive");
|
||||
|
||||
// Validate all recipient accounts exist and have wallets
|
||||
var recipientWallets = new List<SnWallet>();
|
||||
foreach (var accountId in recipientAccountIds)
|
||||
{
|
||||
var wallet = await wat.GetWalletAsync(accountId);
|
||||
if (wallet == null)
|
||||
throw new InvalidOperationException($"Wallet not found for recipient account {accountId}");
|
||||
recipientWallets.Add(wallet);
|
||||
}
|
||||
|
||||
// Check creator has sufficient funds
|
||||
var creatorWallet = await wat.GetWalletAsync(creatorAccountId);
|
||||
if (creatorWallet == null)
|
||||
throw new InvalidOperationException($"Creator wallet not found for account {creatorAccountId}");
|
||||
|
||||
var (creatorPocket, _) = await wat.GetOrCreateWalletPocketAsync(creatorWallet.Id, currency);
|
||||
if (creatorPocket.Amount < totalAmount)
|
||||
throw new InvalidOperationException("Insufficient funds");
|
||||
|
||||
// Calculate amounts for each recipient
|
||||
var recipientAmounts = splitType switch
|
||||
{
|
||||
Shared.Models.FundSplitType.Even => SplitEvenly(totalAmount, recipientAccountIds.Count),
|
||||
Shared.Models.FundSplitType.Random => SplitRandomly(totalAmount, recipientAccountIds.Count),
|
||||
_ => throw new ArgumentException("Invalid split type")
|
||||
};
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var fund = new SnWalletFund
|
||||
{
|
||||
CreatorAccountId = creatorAccountId,
|
||||
Currency = currency,
|
||||
TotalAmount = totalAmount,
|
||||
SplitType = splitType,
|
||||
Message = message,
|
||||
ExpiredAt = now.Plus(expiration ?? Duration.FromHours(24)),
|
||||
Recipients = recipientAccountIds.Select((accountId, index) => new SnWalletFundRecipient
|
||||
{
|
||||
RecipientAccountId = accountId,
|
||||
Amount = recipientAmounts[index]
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
// Deduct from creator's wallet
|
||||
await db.WalletPockets
|
||||
.Where(p => p.Id == creatorPocket.Id)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(p => p.Amount, p => p.Amount - totalAmount));
|
||||
|
||||
db.WalletFunds.Add(fund);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Load the fund with account data including profiles
|
||||
var createdFund = await db.WalletFunds
|
||||
.Include(f => f.Recipients)
|
||||
.ThenInclude(r => r.RecipientAccount)
|
||||
.ThenInclude(a => a.Profile)
|
||||
.Include(f => f.CreatorAccount)
|
||||
.ThenInclude(a => a.Profile)
|
||||
.FirstOrDefaultAsync(f => f.Id == fund.Id);
|
||||
|
||||
return createdFund!;
|
||||
}
|
||||
|
||||
private List<decimal> SplitEvenly(decimal totalAmount, int recipientCount)
|
||||
{
|
||||
var baseAmount = Math.Floor(totalAmount / recipientCount * 100) / 100; // Round down to 2 decimal places
|
||||
var remainder = totalAmount - (baseAmount * recipientCount);
|
||||
|
||||
var amounts = new List<decimal>();
|
||||
for (int i = 0; i < recipientCount; i++)
|
||||
{
|
||||
var amount = baseAmount;
|
||||
if (i < remainder * 100) // Distribute remainder as 0.01 increments
|
||||
amount += 0.01m;
|
||||
amounts.Add(amount);
|
||||
}
|
||||
|
||||
return amounts;
|
||||
}
|
||||
|
||||
private List<decimal> SplitRandomly(decimal totalAmount, int recipientCount)
|
||||
{
|
||||
var random = new Random();
|
||||
var amounts = new List<decimal>();
|
||||
|
||||
// Generate random amounts that sum to total
|
||||
decimal remaining = totalAmount;
|
||||
for (int i = 0; i < recipientCount - 1; i++)
|
||||
{
|
||||
// Ensure each recipient gets at least 0.01 and leave enough for remaining recipients
|
||||
var maxAmount = remaining - (recipientCount - i - 1) * 0.01m;
|
||||
var minAmount = 0.01m;
|
||||
var amount = Math.Round((decimal)random.NextDouble() * (maxAmount - minAmount) + minAmount, 2);
|
||||
amounts.Add(amount);
|
||||
remaining -= amount;
|
||||
}
|
||||
|
||||
// Last recipient gets the remainder
|
||||
amounts.Add(Math.Round(remaining, 2));
|
||||
|
||||
return amounts;
|
||||
}
|
||||
|
||||
public async Task<SnWalletTransaction> ReceiveFundAsync(Guid recipientAccountId, Guid fundId)
|
||||
{
|
||||
var fund = await db.WalletFunds
|
||||
.Include(f => f.Recipients)
|
||||
.FirstOrDefaultAsync(f => f.Id == fundId);
|
||||
|
||||
if (fund == null)
|
||||
throw new InvalidOperationException("Fund not found");
|
||||
|
||||
if (fund.Status == Shared.Models.FundStatus.Expired || fund.Status == Shared.Models.FundStatus.Refunded)
|
||||
throw new InvalidOperationException("Fund is no longer available");
|
||||
|
||||
var recipient = fund.Recipients.FirstOrDefault(r => r.RecipientAccountId == recipientAccountId);
|
||||
if (recipient == null)
|
||||
throw new InvalidOperationException("You are not a recipient of this fund");
|
||||
|
||||
if (recipient.IsReceived)
|
||||
throw new InvalidOperationException("You have already received this fund");
|
||||
|
||||
var recipientWallet = await wat.GetWalletAsync(recipientAccountId);
|
||||
if (recipientWallet == null)
|
||||
throw new InvalidOperationException("Recipient wallet not found");
|
||||
|
||||
// Create transaction to transfer funds to recipient
|
||||
var transaction = await CreateTransactionAsync(
|
||||
payerWalletId: null, // System transfer
|
||||
payeeWalletId: recipientWallet.Id,
|
||||
currency: fund.Currency,
|
||||
amount: recipient.Amount,
|
||||
remarks: $"Received fund portion from {fund.CreatorAccountId}",
|
||||
type: Shared.Models.TransactionType.System,
|
||||
silent: true
|
||||
);
|
||||
|
||||
// Mark as received
|
||||
recipient.IsReceived = true;
|
||||
recipient.ReceivedAt = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
// Update fund status
|
||||
var allReceived = fund.Recipients.All(r => r.IsReceived);
|
||||
if (allReceived)
|
||||
fund.Status = Shared.Models.FundStatus.FullyReceived;
|
||||
else
|
||||
fund.Status = Shared.Models.FundStatus.PartiallyReceived;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return transaction;
|
||||
}
|
||||
|
||||
public async Task ProcessExpiredFundsAsync()
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
var expiredFunds = await db.WalletFunds
|
||||
.Include(f => f.Recipients)
|
||||
.Where(f => f.Status == Shared.Models.FundStatus.Created || f.Status == Shared.Models.FundStatus.PartiallyReceived)
|
||||
.Where(f => f.ExpiredAt < now)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var fund in expiredFunds)
|
||||
{
|
||||
// Calculate unclaimed amount
|
||||
var unclaimedAmount = fund.Recipients
|
||||
.Where(r => !r.IsReceived)
|
||||
.Sum(r => r.Amount);
|
||||
|
||||
if (unclaimedAmount > 0)
|
||||
{
|
||||
// Refund to creator
|
||||
var creatorWallet = await wat.GetWalletAsync(fund.CreatorAccountId);
|
||||
if (creatorWallet != null)
|
||||
{
|
||||
await CreateTransactionAsync(
|
||||
payerWalletId: null, // System refund
|
||||
payeeWalletId: creatorWallet.Id,
|
||||
currency: fund.Currency,
|
||||
amount: unclaimedAmount,
|
||||
remarks: $"Refund for expired fund {fund.Id}",
|
||||
type: Shared.Models.TransactionType.System,
|
||||
silent: true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fund.Status = Shared.Models.FundStatus.Expired;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<WalletOverview> GetWalletOverviewAsync(Guid accountId, DateTime? startDate = null, DateTime? endDate = null)
|
||||
{
|
||||
var wallet = await wat.GetWalletAsync(accountId);
|
||||
if (wallet == null)
|
||||
throw new InvalidOperationException("Wallet not found");
|
||||
|
||||
var query = db.PaymentTransactions
|
||||
.Where(t => t.PayerWalletId == wallet.Id || t.PayeeWalletId == wallet.Id);
|
||||
|
||||
if (startDate.HasValue)
|
||||
query = query.Where(t => t.CreatedAt >= Instant.FromDateTimeUtc(startDate.Value.ToUniversalTime()));
|
||||
if (endDate.HasValue)
|
||||
query = query.Where(t => t.CreatedAt <= Instant.FromDateTimeUtc(endDate.Value.ToUniversalTime()));
|
||||
|
||||
var transactions = await query.ToListAsync();
|
||||
|
||||
var overview = new WalletOverview
|
||||
{
|
||||
AccountId = accountId,
|
||||
StartDate = startDate?.ToString("O"),
|
||||
EndDate = endDate?.ToString("O"),
|
||||
Summary = new Dictionary<string, TransactionSummary>()
|
||||
};
|
||||
|
||||
// Group transactions by type and currency
|
||||
var groupedTransactions = transactions
|
||||
.GroupBy(t => new { t.Type, t.Currency })
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
foreach (var group in groupedTransactions)
|
||||
{
|
||||
var typeName = group.Key.Type.ToString();
|
||||
var currency = group.Key.Currency;
|
||||
|
||||
if (!overview.Summary.ContainsKey(typeName))
|
||||
{
|
||||
overview.Summary[typeName] = new TransactionSummary
|
||||
{
|
||||
Type = typeName,
|
||||
Currencies = new Dictionary<string, CurrencySummary>()
|
||||
};
|
||||
}
|
||||
|
||||
var currencySummary = new CurrencySummary
|
||||
{
|
||||
Currency = currency,
|
||||
Income = 0,
|
||||
Spending = 0,
|
||||
Net = 0
|
||||
};
|
||||
|
||||
foreach (var transaction in group.Value)
|
||||
{
|
||||
if (transaction.PayeeWalletId == wallet.Id)
|
||||
{
|
||||
// Money coming in
|
||||
currencySummary.Income += transaction.Amount;
|
||||
}
|
||||
else if (transaction.PayerWalletId == wallet.Id)
|
||||
{
|
||||
// Money going out
|
||||
currencySummary.Spending += transaction.Amount;
|
||||
}
|
||||
}
|
||||
|
||||
currencySummary.Net = currencySummary.Income - currencySummary.Spending;
|
||||
overview.Summary[typeName].Currencies[currency] = currencySummary;
|
||||
}
|
||||
|
||||
// Calculate totals
|
||||
overview.TotalIncome = overview.Summary.Values.Sum(s => s.Currencies.Values.Sum(c => c.Income));
|
||||
overview.TotalSpending = overview.Summary.Values.Sum(s => s.Currencies.Values.Sum(c => c.Spending));
|
||||
overview.NetTotal = overview.TotalIncome - overview.TotalSpending;
|
||||
|
||||
return overview;
|
||||
}
|
||||
}
|
||||
|
||||
public class WalletOverview
|
||||
{
|
||||
public Guid AccountId { get; set; }
|
||||
public string? StartDate { get; set; }
|
||||
public string? EndDate { get; set; }
|
||||
public Dictionary<string, TransactionSummary> Summary { get; set; } = new();
|
||||
public decimal TotalIncome { get; set; }
|
||||
public decimal TotalSpending { get; set; }
|
||||
public decimal NetTotal { get; set; }
|
||||
}
|
||||
|
||||
public class TransactionSummary
|
||||
{
|
||||
public string Type { get; set; } = null!;
|
||||
public Dictionary<string, CurrencySummary> Currencies { get; set; } = new();
|
||||
}
|
||||
|
||||
public class CurrencySummary
|
||||
{
|
||||
public string Currency { get; set; } = null!;
|
||||
public decimal Income { get; set; }
|
||||
public decimal Spending { get; set; }
|
||||
public decimal Net { get; set; }
|
||||
}
|
||||
|
@@ -696,6 +696,56 @@ public class SubscriptionService(
|
||||
if (subscriptionInfo is null)
|
||||
throw new InvalidOperationException("Invalid gift subscription type.");
|
||||
|
||||
var sameTypeSubscription = await GetSubscriptionAsync(redeemer.Id, gift.SubscriptionIdentifier);
|
||||
if (sameTypeSubscription is not null)
|
||||
{
|
||||
// Extend existing subscription
|
||||
var subscriptionDuration = Duration.FromDays(28);
|
||||
if (sameTypeSubscription.EndedAt.HasValue && sameTypeSubscription.EndedAt.Value > now)
|
||||
{
|
||||
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 = DysonNetwork.Shared.Models.GiftStatus.Redeemed;
|
||||
gift.RedeemedAt = now;
|
||||
gift.RedeemerId = redeemer.Id;
|
||||
gift.SubscriptionId = sameTypeSubscription.Id;
|
||||
gift.UpdatedAt = now;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
await NotifyGiftRedeemed(gift, sameTypeSubscription, redeemer);
|
||||
if (gift.GifterId != redeemer.Id)
|
||||
{
|
||||
var gifter = await db.Accounts.FirstOrDefaultAsync(a => a.Id == gift.GifterId);
|
||||
if (gifter != null) await NotifyGiftClaimedByRecipient(gift, sameTypeSubscription, gifter, redeemer);
|
||||
}
|
||||
|
||||
return (gift, sameTypeSubscription);
|
||||
}
|
||||
|
||||
var subscriptionsInGroup = subscriptionInfo.GroupIdentifier is not null
|
||||
? SubscriptionTypeData.SubscriptionDict
|
||||
.Where(s => s.Value.GroupIdentifier == subscriptionInfo.GroupIdentifier)
|
||||
@@ -710,7 +760,7 @@ public class SubscriptionService(
|
||||
// We do not check account level requirement, since it is a gift
|
||||
|
||||
// Create the subscription from the gift
|
||||
var cycleDuration = Duration.FromDays(28); // Standard 28-day subscription
|
||||
var cycleDuration = Duration.FromDays(28);
|
||||
var subscription = new SnWalletSubscription
|
||||
{
|
||||
BegunAt = now,
|
||||
@@ -730,7 +780,6 @@ public class SubscriptionService(
|
||||
Coupon = gift.Coupon,
|
||||
RenewalAt = now.Plus(cycleDuration),
|
||||
AccountId = redeemer.Id,
|
||||
GiftId = gift.Id
|
||||
};
|
||||
|
||||
// Update the gift status
|
||||
@@ -741,18 +790,18 @@ public class SubscriptionService(
|
||||
gift.UpdatedAt = now;
|
||||
|
||||
// Save both gift and subscription
|
||||
using var transaction = await db.Database.BeginTransactionAsync();
|
||||
using var createTransaction = await db.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
db.WalletSubscriptions.Add(subscription);
|
||||
db.WalletGifts.Update(gift);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await transaction.CommitAsync();
|
||||
await createTransaction.CommitAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
await createTransaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
|
||||
|
@@ -1,15 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Pass.Auth;
|
||||
using DysonNetwork.Pass.Permission;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Pass.Wallet;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/wallets")]
|
||||
public class WalletController(AppDatabase db, WalletService ws, PaymentService payment) : ControllerBase
|
||||
public class WalletController(AppDatabase db, WalletService ws, PaymentService payment, AuthService auth) : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
@@ -102,6 +104,14 @@ public class WalletController(AppDatabase db, WalletService ws, PaymentService p
|
||||
[Required] public Guid AccountId { get; set; }
|
||||
}
|
||||
|
||||
public class WalletTransferRequest
|
||||
{
|
||||
public string? Remark { get; set; }
|
||||
[Required] public decimal Amount { get; set; }
|
||||
[Required] public string Currency { get; set; } = null!;
|
||||
[Required] public Guid PayeeAccountId { get; set; }
|
||||
}
|
||||
|
||||
[HttpPost("balance")]
|
||||
[Authorize]
|
||||
[RequiredPermission("maintenance", "wallets.balance.modify")]
|
||||
@@ -128,4 +138,194 @@ public class WalletController(AppDatabase db, WalletService ws, PaymentService p
|
||||
|
||||
return Ok(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("transfer")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<SnWalletTransaction>> Transfer([FromBody] WalletTransferRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
|
||||
var payerWallet = await ws.GetWalletAsync(currentUser.Id);
|
||||
if (payerWallet is null) return NotFound("Your wallet was not found, please create one first.");
|
||||
|
||||
var payeeWallet = await ws.GetWalletAsync(request.PayeeAccountId);
|
||||
if (payeeWallet is null) return NotFound("Payee wallet was not found.");
|
||||
|
||||
if (currentUser.Id == request.PayeeAccountId) return BadRequest("Cannot transfer to yourself.");
|
||||
|
||||
try
|
||||
{
|
||||
var transaction = await payment.CreateTransactionAsync(
|
||||
payerWalletId: payerWallet.Id,
|
||||
payeeWalletId: payeeWallet.Id,
|
||||
currency: request.Currency,
|
||||
amount: request.Amount,
|
||||
remarks: request.Remark ?? $"Transfer from {currentUser.Id} to {request.PayeeAccountId}",
|
||||
type: TransactionType.Transfer
|
||||
);
|
||||
|
||||
return Ok(transaction);
|
||||
}
|
||||
catch (Exception err)
|
||||
{
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateFundRequest
|
||||
{
|
||||
[Required] public List<Guid> RecipientAccountIds { get; set; } = new();
|
||||
[Required] public string Currency { get; set; } = null!;
|
||||
[Required] public decimal TotalAmount { get; set; }
|
||||
[Required] public FundSplitType SplitType { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public int? ExpirationHours { get; set; } // Optional: hours until expiration
|
||||
[Required] public string PinCode { get; set; } = null!; // Required PIN for fund creation
|
||||
}
|
||||
|
||||
[HttpPost("funds")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<SnWalletFund>> CreateFund([FromBody] CreateFundRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
|
||||
// Validate PIN code
|
||||
if (!await auth.ValidatePinCode(currentUser.Id, request.PinCode))
|
||||
return StatusCode(403, "Invalid PIN Code");
|
||||
|
||||
try
|
||||
{
|
||||
Duration? expiration = null;
|
||||
if (request.ExpirationHours.HasValue)
|
||||
{
|
||||
expiration = Duration.FromHours(request.ExpirationHours.Value);
|
||||
}
|
||||
|
||||
var fund = await payment.CreateFundAsync(
|
||||
creatorAccountId: currentUser.Id,
|
||||
recipientAccountIds: request.RecipientAccountIds,
|
||||
currency: request.Currency,
|
||||
totalAmount: request.TotalAmount,
|
||||
splitType: request.SplitType,
|
||||
message: request.Message,
|
||||
expiration: expiration
|
||||
);
|
||||
|
||||
return Ok(fund);
|
||||
}
|
||||
catch (Exception err)
|
||||
{
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("funds")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<SnWalletFund>>> GetFunds(
|
||||
[FromQuery] int offset = 0,
|
||||
[FromQuery] int take = 20,
|
||||
[FromQuery] FundStatus? status = null
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
|
||||
var query = db.WalletFunds
|
||||
.Include(f => f.Recipients)
|
||||
.ThenInclude(r => r.RecipientAccount)
|
||||
.ThenInclude(a => a.Profile)
|
||||
.Include(f => f.CreatorAccount)
|
||||
.ThenInclude(a => a.Profile)
|
||||
.Where(f => f.CreatorAccountId == currentUser.Id ||
|
||||
f.Recipients.Any(r => r.RecipientAccountId == currentUser.Id))
|
||||
.AsQueryable();
|
||||
|
||||
if (status.HasValue)
|
||||
{
|
||||
query = query.Where(f => f.Status == status.Value);
|
||||
}
|
||||
|
||||
var fundCount = await query.CountAsync();
|
||||
Response.Headers["X-Total"] = fundCount.ToString();
|
||||
|
||||
var funds = await query
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.Skip(offset)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(funds);
|
||||
}
|
||||
|
||||
[HttpGet("funds/{id}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<SnWalletFund>> GetFund(Guid id)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
|
||||
var fund = await db.WalletFunds
|
||||
.Include(f => f.Recipients)
|
||||
.ThenInclude(r => r.RecipientAccount)
|
||||
.ThenInclude(a => a.Profile)
|
||||
.Include(f => f.CreatorAccount)
|
||||
.ThenInclude(a => a.Profile)
|
||||
.FirstOrDefaultAsync(f => f.Id == id);
|
||||
|
||||
if (fund == null)
|
||||
return NotFound("Fund not found");
|
||||
|
||||
// Check if user is creator or recipient
|
||||
var isCreator = fund.CreatorAccountId == currentUser.Id;
|
||||
var isRecipient = fund.Recipients.Any(r => r.RecipientAccountId == currentUser.Id);
|
||||
|
||||
if (!isCreator && !isRecipient)
|
||||
return Forbid("You don't have permission to view this fund");
|
||||
|
||||
return Ok(fund);
|
||||
}
|
||||
|
||||
[HttpPost("funds/{id}/receive")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<SnWalletTransaction>> ReceiveFund(Guid id)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
var transaction = await payment.ReceiveFundAsync(
|
||||
recipientAccountId: currentUser.Id,
|
||||
fundId: id
|
||||
);
|
||||
|
||||
return Ok(transaction);
|
||||
}
|
||||
catch (Exception err)
|
||||
{
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("overview")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<WalletOverview>> GetWalletOverview(
|
||||
[FromQuery] DateTime? startDate = null,
|
||||
[FromQuery] DateTime? endDate = null
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
var overview = await payment.GetWalletOverviewAsync(
|
||||
accountId: currentUser.Id,
|
||||
startDate: startDate,
|
||||
endDate: endDate
|
||||
);
|
||||
|
||||
return Ok(overview);
|
||||
}
|
||||
catch (Exception err)
|
||||
{
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user