using DysonNetwork.Shared.Models; using DysonNetwork.Pass.Wallet; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NodaTime; using System.Text.Json; namespace DysonNetwork.Pass.Lotteries; public class LotteryOrderMetaData { public Guid AccountId { get; set; } public List RegionOneNumbers { get; set; } = new(); public int RegionTwoNumber { get; set; } public int Multiplier { get; set; } = 1; } public class LotteryService( AppDatabase db, PaymentService paymentService, WalletService walletService, ILogger logger) { private static bool ValidateNumbers(List 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 CreateTicketAsync(Guid accountId, List 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> 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 GetTicketAsync(Guid id) { return await db.Lotteries.FirstOrDefaultAsync(l => l.Id == id); } public async Task 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 CreateLotteryOrderAsync(Guid accountId, List 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 && l.DrawStatus == LotteryDrawStatus.Pending ); if (hasPurchasedToday) throw new InvalidOperationException("You can only purchase one lottery per day."); var price = CalculateLotteryPrice(multiplier); var lotteryData = new LotteryOrderMetaData { AccountId = accountId, RegionOneNumbers = region1, RegionTwoNumber = region2, Multiplier = multiplier }; return await paymentService.CreateOrderAsync( null, WalletCurrency.SourcePoint, price, appIdentifier: "lottery", productIdentifier: "lottery", meta: new Dictionary { ["data"] = JsonSerializer.Serialize(lotteryData) }); } public async Task HandleLotteryOrder(SnWalletOrder order) { if (order.Status == OrderStatus.Finished) return; // Already processed if (order.Status != OrderStatus.Paid || !order.Meta.TryGetValue("data", out var dataValue) || dataValue is null || dataValue is not JsonElement { ValueKind: JsonValueKind.String } jsonElem) throw new InvalidOperationException("Invalid order."); var jsonString = jsonElem.GetString(); if (jsonString is null) throw new InvalidOperationException("Invalid order."); var data = JsonSerializer.Deserialize(jsonString); if (data is null) throw new InvalidOperationException("Invalid order data."); await CreateTicketAsync(data.AccountId, data.RegionOneNumbers, data.RegionTwoNumber, data.Multiplier); order.Status = OrderStatus.Finished; await db.SaveChangesAsync(); } private static int CalculateReward(int region1Matches, bool region2Match) { var reward = region1Matches switch { 0 => 0, 1 => 10, 2 => 100, 3 => 500, 4 => 1000, 5 => 10000, _ => 0 }; if (region2Match) reward *= 10; return reward; } private static List GenerateUniqueRandomNumbers(int count, int min, int max) { var numbers = new List(); 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 playerNumbers, List winningNumbers) { return playerNumbers.Intersect(winningNumbers).Count(); } public async Task DrawLotteries() { try { logger.LogInformation("Starting drawing lotteries..."); var now = SystemClock.Instance.GetCurrentInstant(); // All pending lottery tickets var tickets = await db.Lotteries .Where(l => l.DrawStatus == LotteryDrawStatus.Pending) .ToListAsync(); if (tickets.Count == 0) { logger.LogInformation("No pending lottery tickets"); return; } logger.LogInformation("Found {Count} pending lottery tickets for draw", tickets.Count); // Generate winning numbers var winningRegion1 = GenerateUniqueRandomNumbers(5, 0, 99); var winningRegion2 = GenerateUniqueRandomNumbers(1, 0, 99)[0]; logger.LogInformation("Winning numbers generated: Region1 [{Region1}], Region2 [{Region2}]", string.Join(",", winningRegion1), winningRegion2); var drawDate = Instant.FromDateTimeUtc(new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, DateTime.UtcNow.Day, 0, 0, 0, DateTimeKind.Utc).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); // Record match results ticket.MatchedRegionOneNumbers = ticket.RegionOneNumbers.Intersect(winningRegion1).ToList(); ticket.MatchedRegionTwoNumber = region2Match ? (int?)winningRegion2 : null; if (reward > 0) { var wallet = await walletService.GetWalletAsync(ticket.AccountId); if (wallet != null) { await paymentService.CreateTransactionAsync( payerWalletId: null, payeeWalletId: wallet.Id, currency: WalletCurrency.SourcePoint, amount: reward, remarks: $"Lottery prize: {region1Matches} matches{(region2Match ? " + special" : "")}" ); logger.LogInformation( "Awarded {Amount} to account {AccountId} for {Matches} matches{(Special ? \" + special\" : \"\")}", reward, ticket.AccountId, region1Matches, region2Match ? " + special" : ""); totalPrizesAwarded++; totalPrizeAmount += reward; } else { logger.LogWarning("Wallet not found for account {AccountId}, skipping prize award", ticket.AccountId); } } 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(); logger.LogInformation("Daily lottery draw completed: {Prizes} prizes awarded, total amount {Amount}", totalPrizesAwarded, totalPrizeAmount); } catch (Exception ex) { logger.LogError(ex, "An error occurred during the daily lottery draw"); throw; } } }