✨ Lotteries
This commit is contained in:
		
							
								
								
									
										115
									
								
								DysonNetwork.Pass/Lotteries/LotteryController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								DysonNetwork.Pass/Lotteries/LotteryController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Pass.Wallet; | ||||
| using DysonNetwork.Pass.Permission; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Lotteries; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/api/lotteries")] | ||||
| public class LotteryController(AppDatabase db, LotteryService lotteryService) : ControllerBase | ||||
| { | ||||
|     public class CreateLotteryRequest | ||||
|     { | ||||
|         [Required] | ||||
|         public List<int> RegionOneNumbers { get; set; } = null!; | ||||
|         [Required] | ||||
|         [Range(0, 99)] | ||||
|         public int RegionTwoNumber { get; set; } | ||||
|         [Range(1, int.MaxValue)] | ||||
|         public int Multiplier { get; set; } = 1; | ||||
|     } | ||||
|  | ||||
|     [HttpPost] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<SnWalletOrder>> CreateLottery([FromBody] CreateLotteryRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var order = await lotteryService.CreateLotteryOrderAsync( | ||||
|                 accountId: currentUser.Id, | ||||
|                 region1: request.RegionOneNumbers, | ||||
|                 region2: request.RegionTwoNumber, | ||||
|                 multiplier: request.Multiplier); | ||||
|  | ||||
|             return Ok(order); | ||||
|         } | ||||
|         catch (ArgumentException err) | ||||
|         { | ||||
|             return BadRequest(err.Message); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [HttpGet] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<SnLottery>>> GetLotteries( | ||||
|         [FromQuery] int offset = 0, | ||||
|         [FromQuery] int limit = 20) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var lotteries = await lotteryService.GetUserTicketsAsync(currentUser.Id, offset, limit); | ||||
|         var total = await lotteryService.GetUserTicketCountAsync(currentUser.Id); | ||||
|  | ||||
|         Response.Headers["X-Total"] = total.ToString(); | ||||
|  | ||||
|         return Ok(lotteries); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{id}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<SnLottery>> GetLottery(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var lottery = await lotteryService.GetTicketAsync(id); | ||||
|         if (lottery == null || lottery.AccountId != currentUser.Id) | ||||
|             return NotFound(); | ||||
|  | ||||
|         return Ok(lottery); | ||||
|     } | ||||
|  | ||||
|     [HttpPost("draw")] | ||||
|     [Authorize] | ||||
|     [RequiredPermission("maintenance", "lotteries.draw.perform")] | ||||
|     public async Task<IActionResult> PerformLotteryDraw() | ||||
|     { | ||||
|         await lotteryService.PerformDailyDrawAsync(); | ||||
|         return Ok("Lottery draw performed successfully."); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("records")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<SnLotteryRecord>>> GetLotteryRecords( | ||||
|         [FromQuery] Instant? startDate = null, | ||||
|         [FromQuery] Instant? endDate = null, | ||||
|         [FromQuery] int offset = 0, | ||||
|         [FromQuery] int limit = 20) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var query = db.LotteryRecords.AsQueryable(); | ||||
|  | ||||
|         if (startDate.HasValue) | ||||
|             query = query.Where(r => r.DrawDate >= startDate.Value); | ||||
|         if (endDate.HasValue) | ||||
|             query = query.Where(r => r.DrawDate <= endDate.Value); | ||||
|  | ||||
|         var total = await query.CountAsync(); | ||||
|         Response.Headers["X-Total"] = total.ToString(); | ||||
|  | ||||
|         var records = await query | ||||
|             .OrderByDescending(r => r.DrawDate) | ||||
|             .Skip(offset) | ||||
|             .Take(limit) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         return Ok(records); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										21
									
								
								DysonNetwork.Pass/Lotteries/LotteryDrawJob.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								DysonNetwork.Pass/Lotteries/LotteryDrawJob.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| using Quartz; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Lotteries; | ||||
|  | ||||
| public class LotteryDrawJob(LotteryService lotteryService, ILogger<LotteryDrawJob> logger) : IJob | ||||
| { | ||||
|     public async Task Execute(IJobExecutionContext context) | ||||
|     { | ||||
|         logger.LogInformation("Starting daily lottery draw..."); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             await lotteryService.PerformDailyDrawAsync(); | ||||
|             logger.LogInformation("Daily lottery draw completed successfully."); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             logger.LogError(ex, "Error occurred during daily lottery draw."); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										208
									
								
								DysonNetwork.Pass/Lotteries/LotteryService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								DysonNetwork.Pass/Lotteries/LotteryService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,208 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Pass.Wallet; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Lotteries; | ||||
|  | ||||
| public class LotteryService(AppDatabase db, PaymentService paymentService, WalletService walletService) | ||||
| { | ||||
|     private static bool ValidateNumbers(List<int> region1, int region2) | ||||
|     { | ||||
|         if (region1.Count != 5 || region1.Distinct().Count() != 5) | ||||
|             return false; | ||||
|         if (region1.Any(n => n < 0 || n > 99)) | ||||
|             return false; | ||||
|         if (region2 < 0 || region2 > 99) | ||||
|             return false; | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     public async Task<SnLottery> CreateTicketAsync(Guid accountId, List<int> region1, int region2, int multiplier = 1) | ||||
|     { | ||||
|         if (!ValidateNumbers(region1, region2)) | ||||
|             throw new ArgumentException("Invalid lottery numbers"); | ||||
|  | ||||
|         var lottery = new SnLottery | ||||
|         { | ||||
|             AccountId = accountId, | ||||
|             RegionOneNumbers = region1, | ||||
|             RegionTwoNumber = region2, | ||||
|             Multiplier = multiplier | ||||
|         }; | ||||
|  | ||||
|         db.Lotteries.Add(lottery); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         return lottery; | ||||
|     } | ||||
|  | ||||
|     public async Task<List<SnLottery>> GetUserTicketsAsync(Guid accountId, int offset = 0, int limit = 20) | ||||
|     { | ||||
|         return await db.Lotteries | ||||
|             .Where(l => l.AccountId == accountId) | ||||
|             .OrderByDescending(l => l.CreatedAt) | ||||
|             .Skip(offset) | ||||
|             .Take(limit) | ||||
|             .ToListAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<SnLottery?> GetTicketAsync(Guid id) | ||||
|     { | ||||
|         return await db.Lotteries.FirstOrDefaultAsync(l => l.Id == id); | ||||
|     } | ||||
|  | ||||
|     public async Task<int> GetUserTicketCountAsync(Guid accountId) | ||||
|     { | ||||
|         return await db.Lotteries.CountAsync(l => l.AccountId == accountId); | ||||
|     } | ||||
|  | ||||
|     private static decimal CalculateLotteryPrice(int multiplier) | ||||
|     { | ||||
|         return 10 + (multiplier - 1) * 10; | ||||
|     } | ||||
|  | ||||
|     public async Task<SnWalletOrder> CreateLotteryOrderAsync(Guid accountId, List<int> region1, int region2, int multiplier = 1) | ||||
|     { | ||||
|         if (!ValidateNumbers(region1, region2)) | ||||
|             throw new ArgumentException("Invalid lottery numbers"); | ||||
|  | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var todayStart = new LocalDateTime(now.InUtc().Year, now.InUtc().Month, now.InUtc().Day, 0, 0).InUtc().ToInstant(); | ||||
|         var hasPurchasedToday = await db.Lotteries.AnyAsync(l => l.AccountId == accountId && l.CreatedAt >= todayStart); | ||||
|         if (hasPurchasedToday) | ||||
|             throw new InvalidOperationException("You can only purchase one lottery per day."); | ||||
|  | ||||
|         var price = CalculateLotteryPrice(multiplier); | ||||
|  | ||||
|         return await paymentService.CreateOrderAsync( | ||||
|             null, | ||||
|             "isp", | ||||
|             price, | ||||
|             appIdentifier: "lottery", | ||||
|             productIdentifier: "lottery", | ||||
|             meta: new Dictionary<string, object> | ||||
|             { | ||||
|                 ["account_id"] = accountId.ToString(), | ||||
|                 ["region_one_numbers"] = region1, | ||||
|                 ["region_two_number"] = region2, | ||||
|                 ["multiplier"] = multiplier | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     public async Task HandleLotteryOrder(SnWalletOrder order) | ||||
|     { | ||||
|         if (order.Status != OrderStatus.Paid || | ||||
|             !order.Meta.TryGetValue("account_id", out var accountIdValue) || | ||||
|             !order.Meta.TryGetValue("region_one_numbers", out var region1Value) || | ||||
|             !order.Meta.TryGetValue("region_two_number", out var region2Value) || | ||||
|             !order.Meta.TryGetValue("multiplier", out var multiplierValue)) | ||||
|             throw new InvalidOperationException("Invalid order."); | ||||
|  | ||||
|         var accountId = Guid.Parse((string)accountIdValue!); | ||||
|         var region1Json = (System.Text.Json.JsonElement)region1Value; | ||||
|         var region1 = region1Json.EnumerateArray().Select(e => e.GetInt32()).ToList(); | ||||
|         var region2 = Convert.ToInt32((string)region2Value!); | ||||
|         var multiplier = Convert.ToInt32((string)multiplierValue!); | ||||
|  | ||||
|         await CreateTicketAsync(accountId, region1, region2, multiplier); | ||||
|     } | ||||
|  | ||||
|     private static int CalculateReward(int region1Matches, bool region2Match) | ||||
|     { | ||||
|         var reward = region1Matches switch | ||||
|         { | ||||
|             0 => 0, | ||||
|             1 => 10, | ||||
|             2 => 20, | ||||
|             3 => 50, | ||||
|             4 => 100, | ||||
|             5 => 1000, | ||||
|             _ => 0 | ||||
|         }; | ||||
|         if (region2Match) reward *= 10; | ||||
|         return reward; | ||||
|     } | ||||
|  | ||||
|     private static List<int> GenerateUniqueRandomNumbers(int count, int min, int max) | ||||
|     { | ||||
|         var numbers = new List<int>(); | ||||
|         var random = new Random(); | ||||
|         while (numbers.Count < count) | ||||
|         { | ||||
|             var num = random.Next(min, max + 1); | ||||
|             if (!numbers.Contains(num)) numbers.Add(num); | ||||
|         } | ||||
|         return numbers.OrderBy(n => n).ToList(); | ||||
|     } | ||||
|  | ||||
|     private int CountMatches(List<int> playerNumbers, List<int> winningNumbers) | ||||
|     { | ||||
|         return playerNumbers.Intersect(winningNumbers).Count(); | ||||
|     } | ||||
|  | ||||
|     public async Task PerformDailyDrawAsync() | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var yesterdayStart = new LocalDateTime(now.InUtc().Year, now.InUtc().Month, now.InUtc().Day, 0, 0).InUtc().ToInstant().Minus(Duration.FromDays(1)); | ||||
|         var todayStart = new LocalDateTime(now.InUtc().Year, now.InUtc().Month, now.InUtc().Day, 0, 0).InUtc().ToInstant(); | ||||
|  | ||||
|         // Tickets purchased yesterday that are still pending draw | ||||
|         var tickets = await db.Lotteries | ||||
|             .Where(l => l.CreatedAt >= yesterdayStart && l.CreatedAt < todayStart && l.DrawStatus == LotteryDrawStatus.Pending) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         if (!tickets.Any()) return; | ||||
|  | ||||
|         // Generate winning numbers | ||||
|         var winningRegion1 = GenerateUniqueRandomNumbers(5, 0, 99); | ||||
|         var winningRegion2 = GenerateUniqueRandomNumbers(1, 0, 99)[0]; | ||||
|  | ||||
|         var drawDate = Instant.FromDateTimeUtc(DateTime.Today.AddDays(-1)); // Yesterday's date | ||||
|  | ||||
|         var totalPrizesAwarded = 0; | ||||
|         long totalPrizeAmount = 0; | ||||
|  | ||||
|         // Process each ticket | ||||
|         foreach (var ticket in tickets) | ||||
|         { | ||||
|             var region1Matches = CountMatches(ticket.RegionOneNumbers, winningRegion1); | ||||
|             var region2Match = ticket.RegionTwoNumber == winningRegion2; | ||||
|             var reward = CalculateReward(region1Matches, region2Match); | ||||
|  | ||||
|             if (reward > 0) | ||||
|             { | ||||
|                 var wallet = await walletService.GetWalletAsync(ticket.AccountId); | ||||
|                 if (wallet != null) | ||||
|                 { | ||||
|                     await paymentService.CreateTransactionAsync( | ||||
|                         payerWalletId: null, | ||||
|                         payeeWalletId: wallet.Id, | ||||
|                         currency: "isp", | ||||
|                         amount: reward, | ||||
|                         remarks: $"Lottery prize: {region1Matches} matches{(region2Match ? " + special" : "")}" | ||||
|                     ); | ||||
|                     totalPrizesAwarded++; | ||||
|                     totalPrizeAmount += reward; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             ticket.DrawStatus = LotteryDrawStatus.Drawn; | ||||
|             ticket.DrawDate = drawDate; | ||||
|         } | ||||
|  | ||||
|         // Save the draw record | ||||
|         var lotteryRecord = new SnLotteryRecord | ||||
|         { | ||||
|             DrawDate = drawDate, | ||||
|             WinningRegionOneNumbers = winningRegion1, | ||||
|             WinningRegionTwoNumber = winningRegion2, | ||||
|             TotalTickets = tickets.Count, | ||||
|             TotalPrizesAwarded = totalPrizesAwarded, | ||||
|             TotalPrizeAmount = totalPrizeAmount | ||||
|         }; | ||||
|  | ||||
|         db.LotteryRecords.Add(lotteryRecord); | ||||
|         await db.SaveChangesAsync(); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user