402 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			402 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System.ComponentModel.DataAnnotations;
 | |
| using DysonNetwork.Pass.Auth;
 | |
| using DysonNetwork.Pass.Permission;
 | |
| using DysonNetwork.Shared.Cache;
 | |
| 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, AuthService auth, ICacheService cache) : ControllerBase
 | |
| {
 | |
|     [HttpPost]
 | |
|     [Authorize]
 | |
|     public async Task<ActionResult<SnWallet>> CreateWallet()
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not SnAccount 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<SnWallet>> GetWallet()
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not SnAccount 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);
 | |
|     }
 | |
| 
 | |
|     public class WalletStats
 | |
|     {
 | |
|         public Instant PeriodBegin { get; set; }
 | |
|         public Instant PeriodEnd { get; set; }
 | |
|         public int TotalTransactions { get; set; }
 | |
|         public int TotalOrders { get; set; }
 | |
|         public Dictionary<string, decimal> IncomeCatgories { get; set; } = null!;
 | |
|         public Dictionary<string, decimal> OutgoingCategories { get; set; } = null!;
 | |
|         public decimal TotalIncome => IncomeCatgories.Values.Sum();
 | |
|         public decimal TotalOutgoing => OutgoingCategories.Values.Sum();
 | |
|         public decimal Sum => TotalIncome - TotalOutgoing;
 | |
|     }
 | |
| 
 | |
|     [HttpGet("stats")]
 | |
|     [Authorize]
 | |
|     public async Task<ActionResult<WalletStats>> GetWalletStats([FromQuery] int period = 30)
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
 | |
| 
 | |
|         var wallet = await ws.GetWalletAsync(currentUser.Id);
 | |
|         if (wallet is null) return NotFound("Wallet was not found, please create one first.");
 | |
| 
 | |
|         var periodEnd = SystemClock.Instance.GetCurrentInstant();
 | |
|         var periodBegin = periodEnd.Minus(Duration.FromDays(period));
 | |
| 
 | |
|         var cacheKey = $"wallet:stats:{currentUser.Id}:{period}";
 | |
|         var cached = await cache.GetAsync<WalletStats>(cacheKey);
 | |
|         if (cached != null)
 | |
|         {
 | |
|             return Ok(cached);
 | |
|         }
 | |
| 
 | |
|         var transactions = await db.PaymentTransactions
 | |
|             .Where(t => (t.PayerWalletId == wallet.Id || t.PayeeWalletId == wallet.Id) &&
 | |
|                         t.CreatedAt >= periodBegin && t.CreatedAt <= periodEnd)
 | |
|             .ToListAsync();
 | |
| 
 | |
|         var orders = await db.PaymentOrders
 | |
|             .Where(o => o.PayeeWalletId == wallet.Id &&
 | |
|                         o.CreatedAt >= periodBegin && o.CreatedAt <= periodEnd)
 | |
|             .ToListAsync();
 | |
| 
 | |
|         var incomeCategories = transactions
 | |
|             .Where(t => t.PayeeWalletId == wallet.Id)
 | |
|             .GroupBy(t => t.Type.ToString())
 | |
|             .ToDictionary(g => g.Key, g => g.Sum(t => t.Amount));
 | |
| 
 | |
|         var outgoingCategories = transactions
 | |
|             .Where(t => t.PayerWalletId == wallet.Id)
 | |
|             .GroupBy(t => t.Type.ToString())
 | |
|             .ToDictionary(g => g.Key, g => g.Sum(t => t.Amount));
 | |
| 
 | |
|         var stats = new WalletStats
 | |
|         {
 | |
|             PeriodBegin = periodBegin,
 | |
|             PeriodEnd = periodEnd,
 | |
|             TotalTransactions = transactions.Count,
 | |
|             TotalOrders = orders.Count,
 | |
|             IncomeCatgories = incomeCategories,
 | |
|             OutgoingCategories = outgoingCategories
 | |
|         };
 | |
| 
 | |
|         await cache.SetAsync(cacheKey, stats, TimeSpan.FromHours(1));
 | |
|         return Ok(stats);
 | |
|     }
 | |
| 
 | |
|     [HttpGet("transactions")]
 | |
|     [Authorize]
 | |
|     public async Task<ActionResult<List<SnWalletTransaction>>> GetTransactions(
 | |
|         [FromQuery] int offset = 0, [FromQuery] int take = 20
 | |
|     )
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
 | |
| 
 | |
|         var accountWallet = await db.Wallets.Where(w => w.AccountId == currentUser.Id).FirstOrDefaultAsync();
 | |
|         if (accountWallet is null) return NotFound();
 | |
| 
 | |
|         var query = db.PaymentTransactions
 | |
|             .Where(t => t.PayeeWalletId == accountWallet.Id || t.PayerWalletId == accountWallet.Id)
 | |
|             .OrderByDescending(t => t.CreatedAt)
 | |
|             .AsQueryable();
 | |
| 
 | |
|         var transactionCount = await query.CountAsync();
 | |
|         Response.Headers["X-Total"] = transactionCount.ToString();
 | |
| 
 | |
|         var transactions = await query
 | |
|             .Skip(offset)
 | |
|             .Take(take)
 | |
|             .Include(t => t.PayerWallet)
 | |
|             .ThenInclude(w => w.Account)
 | |
|             .ThenInclude(w => w.Profile)
 | |
|             .Include(t => t.PayeeWallet)
 | |
|             .ThenInclude(w => w.Account)
 | |
|             .ThenInclude(w => w.Profile)
 | |
|             .ToListAsync();
 | |
| 
 | |
|         return Ok(transactions);
 | |
|     }
 | |
| 
 | |
|     [HttpGet("orders")]
 | |
|     [Authorize]
 | |
|     public async Task<ActionResult<List<SnWalletOrder>>> GetOrders(
 | |
|         [FromQuery] int offset = 0, [FromQuery] int take = 20
 | |
|     )
 | |
|     {
 | |
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
 | |
| 
 | |
|         var accountWallet = await db.Wallets.Where(w => w.AccountId == currentUser.Id).FirstOrDefaultAsync();
 | |
|         if (accountWallet is null) return NotFound();
 | |
| 
 | |
|         var query = db.PaymentOrders.AsQueryable()
 | |
|             .Include(o => o.Transaction)
 | |
|             .Where(o => o.Transaction != null && (o.Transaction.PayeeWalletId == accountWallet.Id || o.Transaction.PayerWalletId == accountWallet.Id))
 | |
|             .AsQueryable();
 | |
| 
 | |
|         var orderCount = await query.CountAsync();
 | |
|         Response.Headers["X-Total"] = orderCount.ToString();
 | |
| 
 | |
|         var orders = await query
 | |
|             .Skip(offset)
 | |
|             .Take(take)
 | |
|             .OrderByDescending(t => t.CreatedAt)
 | |
|             .ToListAsync();
 | |
| 
 | |
|         return Ok(orders);
 | |
|     }
 | |
| 
 | |
|     public class WalletBalanceRequest
 | |
|     {
 | |
|         public string? Remark { get; set; }
 | |
|         [Required] public decimal Amount { get; set; }
 | |
|         [Required] public string Currency { get; set; } = null!;
 | |
|         [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; }
 | |
|         [Required] public string PinCode { get; set; } = null!;
 | |
|     }
 | |
| 
 | |
|     [HttpPost("balance")]
 | |
|     [Authorize]
 | |
|     [RequiredPermission("maintenance", "wallets.balance.modify")]
 | |
|     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.");
 | |
| 
 | |
|         var transaction = request.Amount >= 0
 | |
|             ? await payment.CreateTransactionAsync(
 | |
|                 payerWalletId: null,
 | |
|                 payeeWalletId: wallet.Id,
 | |
|                 currency: request.Currency,
 | |
|                 amount: request.Amount,
 | |
|                 remarks: request.Remark
 | |
|             )
 | |
|             : await payment.CreateTransactionAsync(
 | |
|                 payerWalletId: wallet.Id,
 | |
|                 payeeWalletId: null,
 | |
|                 currency: request.Currency,
 | |
|                 amount: request.Amount,
 | |
|                 remarks: request.Remark
 | |
|             );
 | |
| 
 | |
|         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();
 | |
| 
 | |
|         // Validate PIN code
 | |
|         if (!await auth.ValidatePinCode(currentUser.Id, request.PinCode))
 | |
|             return StatusCode(403, "Invalid PIN Code");
 | |
| 
 | |
|         if (currentUser.Id == request.PayeeAccountId) return BadRequest("Cannot transfer to yourself.");
 | |
| 
 | |
|         try
 | |
|         {
 | |
|             var transaction = await payment.TransferAsync(
 | |
|                 payerAccountId: currentUser.Id,
 | |
|                 payeeAccountId: request.PayeeAccountId,
 | |
|                 currency: request.Currency,
 | |
|                 amount: request.Amount
 | |
|             );
 | |
| 
 | |
|             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);
 | |
|         }
 | |
|     }
 | |
| }
 |