diff --git a/DysonNetwork.Pass/Account/AccountCurrentController.cs b/DysonNetwork.Pass/Account/AccountCurrentController.cs index ce1415e..2955c96 100644 --- a/DysonNetwork.Pass/Account/AccountCurrentController.cs +++ b/DysonNetwork.Pass/Account/AccountCurrentController.cs @@ -38,7 +38,7 @@ public class AccountCurrentController( .Include(e => e.Profile) .Where(e => e.Id == userId) .FirstOrDefaultAsync(); - + var perk = await subscriptions.GetPerkSubscriptionAsync(account!.Id); account.PerkSubscription = perk?.ToReference(); @@ -120,6 +120,7 @@ public class AccountCurrentController( ); profile.Picture = CloudFileReferenceObject.FromProtoValue(file); } + if (request.BackgroundId is not null) { var file = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId }); @@ -255,13 +256,27 @@ public class AccountCurrentController( } [HttpPost("check-in")] - public async Task> DoCheckIn([FromBody] string? captchaToken) + public async Task> DoCheckIn( + [FromBody] string? captchaToken, + [FromQuery] Instant? backdated = null + ) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - var isAvailable = await events.CheckInDailyIsAvailable(currentUser); - if (!isAvailable) - return BadRequest("Check-in is not available for today."); + if (backdated is null) + { + var isAvailable = await events.CheckInDailyIsAvailable(currentUser); + if (!isAvailable) + return BadRequest("Check-in is not available for today."); + } + else + { + if (currentUser.PerkSubscription is null) + return StatusCode(403, "You need to have a subscription to check-in backdated."); + var isAvailable = await events.CheckInBackdatedIsAvailable(currentUser, backdated.Value); + if (!isAvailable) + return BadRequest("Check-in is not available for this date."); + } try { @@ -269,9 +284,10 @@ public class AccountCurrentController( return needsCaptcha switch { true when string.IsNullOrWhiteSpace(captchaToken) => StatusCode(423, - "Captcha is required for this check-in."), + "Captcha is required for this check-in." + ), true when !await auth.ValidateCaptcha(captchaToken!) => BadRequest("Invalid captcha token."), - _ => await events.CheckInDaily(currentUser) + _ => await events.CheckInDaily(currentUser, backdated) }; } catch (InvalidOperationException ex) diff --git a/DysonNetwork.Pass/Account/AccountEventService.cs b/DysonNetwork.Pass/Account/AccountEventService.cs index 79a646d..751c5b8 100644 --- a/DysonNetwork.Pass/Account/AccountEventService.cs +++ b/DysonNetwork.Pass/Account/AccountEventService.cs @@ -5,6 +5,7 @@ using DysonNetwork.Shared.Proto; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Localization; using NodaTime; +using NodaTime.Extensions; namespace DysonNetwork.Pass.Account; @@ -166,7 +167,7 @@ public class AccountEventService( } private const int FortuneTipCount = 14; // This will be the max index for each type (positive/negative) - private const string CaptchaCacheKey = "CheckInCaptcha_"; + private const string CaptchaCacheKey = "checkin:captcha:"; private const int CaptchaProbabilityPercent = 20; public async Task CheckInDailyDoAskCaptcha(Account user) @@ -198,9 +199,55 @@ public class AccountEventService( return lastDate < currentDate; } - public const string CheckInLockKey = "CheckInLock_"; + public async Task CheckInBackdatedIsAvailable(Account user, Instant backdated) + { + var aDay = Duration.FromDays(1); + var backdatedStart = backdated.ToDateTimeUtc().Date.ToInstant(); + var backdatedEnd = backdated.Plus(aDay).ToDateTimeUtc().Date.ToInstant(); - public async Task CheckInDaily(Account user) + var backdatedDate = backdated.ToDateTimeUtc(); + var backdatedMonthStart = new DateTime( + backdatedDate.Year, + backdatedDate.Month, + 1, + 0, + 0, + 0 + ).ToInstant(); + var backdatedMonthEnd = + new DateTime( + backdatedDate.Year, + backdatedDate.Month, + DateTime.DaysInMonth( + backdatedDate.Year, + backdatedDate.Month + ), + 23, + 59, + 59 + ).ToInstant(); + + // The first check, if that day already has a check-in + var lastCheckIn = await db.AccountCheckInResults + .Where(x => x.AccountId == user.Id) + .Where(x => x.CreatedAt >= backdatedStart && x.CreatedAt < backdatedEnd) + .OrderByDescending(x => x.CreatedAt) + .FirstOrDefaultAsync(); + if (lastCheckIn is not null) return false; + + // The second check, is the user reached the max backdated check-ins limit, + // which is once a week, which is 4 times a month + var backdatedCheckInMonths = await db.AccountCheckInResults + .Where(x => x.AccountId == user.Id) + .Where(x => x.CreatedAt >= backdatedMonthStart && x.CreatedAt < backdatedMonthEnd) + .Where(x => x.BackdatedFrom != null) + .CountAsync(); + return backdatedCheckInMonths < 4; + } + + public const string CheckInLockKey = "checkin:lock:"; + + public async Task CheckInDaily(Account user, Instant? backdated = null) { var lockKey = $"{CheckInLockKey}{user.Id}"; @@ -254,7 +301,9 @@ public class AccountEventService( Level = (CheckInResultLevel)Random.Next(Enum.GetValues().Length), AccountId = user.Id, RewardExperience = 100, - RewardPoints = 10, + RewardPoints = backdated.HasValue ? null : 10, + BackdatedFrom = backdated.HasValue ? SystemClock.Instance.GetCurrentInstant() : null, + CreatedAt = backdated ?? SystemClock.Instance.GetCurrentInstant(), }; var now = SystemClock.Instance.GetCurrentInstant().InUtc().Date; @@ -298,7 +347,7 @@ public class AccountEventService( var statuses = await db.AccountStatuses .AsNoTracking() - .TagWith("GetEventCalendar_Statuses") + .TagWith("eventcal:statuses") .Where(x => x.AccountId == user.Id && x.CreatedAt >= startOfMonth && x.CreatedAt < endOfMonth) .Select(x => new Status { @@ -316,7 +365,7 @@ public class AccountEventService( var checkIn = await db.AccountCheckInResults .AsNoTracking() - .TagWith("GetEventCalendar_CheckIn") + .TagWith("eventcal:checkin") .Where(x => x.AccountId == user.Id && x.CreatedAt >= startOfMonth && x.CreatedAt < endOfMonth) .ToListAsync(); diff --git a/DysonNetwork.Pass/Account/Event.cs b/DysonNetwork.Pass/Account/Event.cs index ef83d14..9cbc49b 100644 --- a/DysonNetwork.Pass/Account/Event.cs +++ b/DysonNetwork.Pass/Account/Event.cs @@ -46,6 +46,8 @@ public class CheckInResult : ModelBase public Guid AccountId { get; set; } public Account Account { get; set; } = null!; + + public Instant? BackdatedFrom { get; set; } } public class FortuneTip diff --git a/DysonNetwork.sln.DotSettings.user b/DysonNetwork.sln.DotSettings.user index 4919c7c..7ac5870 100644 --- a/DysonNetwork.sln.DotSettings.user +++ b/DysonNetwork.sln.DotSettings.user @@ -27,6 +27,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -84,6 +85,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded