✨ Lotteries
This commit is contained in:
@@ -57,6 +57,9 @@ public class AppDatabase(
|
|||||||
public DbSet<SnSocialCreditRecord> SocialCreditRecords { get; set; } = null!;
|
public DbSet<SnSocialCreditRecord> SocialCreditRecords { get; set; } = null!;
|
||||||
public DbSet<SnExperienceRecord> ExperienceRecords { get; set; } = null!;
|
public DbSet<SnExperienceRecord> ExperienceRecords { get; set; } = null!;
|
||||||
|
|
||||||
|
public DbSet<SnLottery> Lotteries { get; set; } = null!;
|
||||||
|
public DbSet<SnLotteryRecord> LotteryRecords { get; set; } = null!;
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
optionsBuilder.UseNpgsql(
|
optionsBuilder.UseNpgsql(
|
||||||
|
|||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
2612
DysonNetwork.Pass/Migrations/20251023173204_AddLotteries.Designer.cs
generated
Normal file
2612
DysonNetwork.Pass/Migrations/20251023173204_AddLotteries.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
78
DysonNetwork.Pass/Migrations/20251023173204_AddLotteries.cs
Normal file
78
DysonNetwork.Pass/Migrations/20251023173204_AddLotteries.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddLotteries : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "lotteries",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
account_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
region_one_numbers = table.Column<List<int>>(type: "jsonb", nullable: false),
|
||||||
|
region_two_number = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
multiplier = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
draw_status = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
draw_date = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
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_lotteries", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_lotteries_accounts_account_id",
|
||||||
|
column: x => x.account_id,
|
||||||
|
principalTable: "accounts",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "lottery_records",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
draw_date = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
winning_region_one_numbers = table.Column<List<int>>(type: "jsonb", nullable: false),
|
||||||
|
winning_region_two_number = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
total_tickets = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
total_prizes_awarded = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
total_prize_amount = table.Column<long>(type: "bigint", 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_lottery_records", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_lotteries_account_id",
|
||||||
|
table: "lotteries",
|
||||||
|
column: "account_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "lotteries");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "lottery_records");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1059,6 +1059,109 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
b.ToTable("experience_records", (string)null);
|
b.ToTable("experience_records", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnLottery", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_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?>("DrawDate")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("draw_date");
|
||||||
|
|
||||||
|
b.Property<int>("DrawStatus")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("draw_status");
|
||||||
|
|
||||||
|
b.Property<int>("Multiplier")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("multiplier");
|
||||||
|
|
||||||
|
b.Property<List<int>>("RegionOneNumbers")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("region_one_numbers");
|
||||||
|
|
||||||
|
b.Property<int>("RegionTwoNumber")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("region_two_number");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_lotteries");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId")
|
||||||
|
.HasDatabaseName("ix_lotteries_account_id");
|
||||||
|
|
||||||
|
b.ToTable("lotteries", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnLotteryRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("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>("DrawDate")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("draw_date");
|
||||||
|
|
||||||
|
b.Property<long>("TotalPrizeAmount")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("total_prize_amount");
|
||||||
|
|
||||||
|
b.Property<int>("TotalPrizesAwarded")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("total_prizes_awarded");
|
||||||
|
|
||||||
|
b.Property<int>("TotalTickets")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("total_tickets");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<List<int>>("WinningRegionOneNumbers")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("winning_region_one_numbers");
|
||||||
|
|
||||||
|
b.Property<int>("WinningRegionTwoNumber")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("winning_region_two_number");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_lottery_records");
|
||||||
|
|
||||||
|
b.ToTable("lottery_records", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b =>
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -2234,6 +2337,18 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
b.Navigation("Account");
|
b.Navigation("Account");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnLottery", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_lotteries_accounts_account_id");
|
||||||
|
|
||||||
|
b.Navigation("Account");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b =>
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
|
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
|
||||||
|
|||||||
@@ -104,9 +104,33 @@ public class BroadcastEventHandler(
|
|||||||
logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId);
|
logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId);
|
||||||
await msg.AckAsync(cancellationToken: stoppingToken);
|
await msg.AckAsync(cancellationToken: stoppingToken);
|
||||||
}
|
}
|
||||||
|
else if (evt.ProductIdentifier == "lottery")
|
||||||
|
{
|
||||||
|
logger.LogInformation("Handling lottery order: {OrderId}", evt.OrderId);
|
||||||
|
|
||||||
|
await using var scope = serviceProvider.CreateAsyncScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
||||||
|
var lotteries = scope.ServiceProvider.GetRequiredService<Lotteries.LotteryService>();
|
||||||
|
|
||||||
|
var order = await db.PaymentOrders.FindAsync(
|
||||||
|
[evt.OrderId],
|
||||||
|
cancellationToken: stoppingToken
|
||||||
|
);
|
||||||
|
if (order == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Order with ID {OrderId} not found. Redelivering.", evt.OrderId);
|
||||||
|
await msg.NakAsync(cancellationToken: stoppingToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await lotteries.HandleLotteryOrder(order);
|
||||||
|
|
||||||
|
logger.LogInformation("Lottery ticket for order {OrderId} created successfully.", evt.OrderId);
|
||||||
|
await msg.AckAsync(cancellationToken: stoppingToken);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Not a subscription or gift order, skip
|
// Not a subscription, gift, or lottery order, skip
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ public static class ScheduledJobsConfiguration
|
|||||||
.WithIntervalInHours(1)
|
.WithIntervalInHours(1)
|
||||||
.RepeatForever())
|
.RepeatForever())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
var lotteryDrawJob = new JobKey("LotteryDraw");
|
||||||
|
q.AddJob<Lotteries.LotteryDrawJob>(opts => opts.WithIdentity(lotteryDrawJob));
|
||||||
|
q.AddTrigger(opts => opts
|
||||||
|
.ForJob(lotteryDrawJob)
|
||||||
|
.WithIdentity("LotteryDrawTrigger")
|
||||||
|
.WithCronSchedule("0 0 0 * * ?"));
|
||||||
});
|
});
|
||||||
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<SocialCreditService>();
|
services.AddScoped<SocialCreditService>();
|
||||||
services.AddScoped<ExperienceService>();
|
services.AddScoped<ExperienceService>();
|
||||||
services.AddScoped<RealmService>();
|
services.AddScoped<RealmService>();
|
||||||
|
services.AddScoped<Lotteries.LotteryService>();
|
||||||
|
|
||||||
services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider"));
|
services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider"));
|
||||||
services.AddScoped<OidcProviderService>();
|
services.AddScoped<OidcProviderService>();
|
||||||
@@ -149,4 +150,4 @@ public static class ServiceCollectionExtensions
|
|||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
DysonNetwork.Shared/Models/SnLottery.cs
Normal file
49
DysonNetwork.Shared/Models/SnLottery.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Shared.Models;
|
||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
public enum LotteryDrawStatus
|
||||||
|
{
|
||||||
|
Pending = 0,
|
||||||
|
Drawn = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SnLotteryRecord : ModelBase
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public Instant DrawDate { get; set; } // Date of the draw
|
||||||
|
|
||||||
|
[Column(TypeName = "jsonb")]
|
||||||
|
public List<int> WinningRegionOneNumbers { get; set; } = new(); // 5 winning numbers
|
||||||
|
|
||||||
|
[Range(0, 99)]
|
||||||
|
public int WinningRegionTwoNumber { get; set; } // 1 winning number
|
||||||
|
|
||||||
|
public int TotalTickets { get; set; } // Total tickets processed for this draw
|
||||||
|
public int TotalPrizesAwarded { get; set; } // Total prizes awarded
|
||||||
|
public long TotalPrizeAmount { get; set; } // Total ISP prize amount awarded
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SnLottery : ModelBase
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public SnAccount Account { get; set; } = null!;
|
||||||
|
public Guid AccountId { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "jsonb")]
|
||||||
|
public List<int> RegionOneNumbers { get; set; } = new(); // 5 numbers, 0-99, unique
|
||||||
|
|
||||||
|
[Range(0, 99)]
|
||||||
|
public int RegionTwoNumber { get; set; } // 1 number, 0-99, can repeat
|
||||||
|
|
||||||
|
public int Multiplier { get; set; } = 1; // Default 1x
|
||||||
|
|
||||||
|
public LotteryDrawStatus DrawStatus { get; set; } = LotteryDrawStatus.Pending; // Status to track draw processing
|
||||||
|
|
||||||
|
public Instant? DrawDate { get; set; } // Date when this ticket was drawn
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user