diff --git a/DysonNetwork.Pass/Account/NotableDay.cs b/DysonNetwork.Pass/Account/NotableDay.cs new file mode 100644 index 0000000..cbbd258 --- /dev/null +++ b/DysonNetwork.Pass/Account/NotableDay.cs @@ -0,0 +1,53 @@ +using Nager.Holiday; +using NodaTime; + +namespace DysonNetwork.Pass.Account; + +/// +/// Reference from Nager.Holiday +/// +public enum NotableHolidayType +{ + /// Public holiday + Public, + /// Bank holiday, banks and offices are closed + Bank, + /// School holiday, schools are closed + School, + /// Authorities are closed + Authorities, + /// Majority of people take a day off + Optional, + /// Optional festivity, no paid day off + Observance, +} + + +public class NotableDay +{ + public Instant Date { get; set; } + public string? LocalName { get; set; } + public string? GlobalName { get; set; } + public string? CountryCode { get; set; } + public NotableHolidayType[] Holidays { get; set; } = []; + + public static NotableDay FromNagerHoliday(PublicHoliday holiday) + { + return new NotableDay() + { + Date = Instant.FromDateTimeUtc(holiday.Date.ToUniversalTime()), + LocalName = holiday.LocalName, + GlobalName = holiday.Name, + CountryCode = holiday.CountryCode, + Holidays = holiday.Types?.Select(x => x switch + { + PublicHolidayType.Public => NotableHolidayType.Public, + PublicHolidayType.Bank => NotableHolidayType.Bank, + PublicHolidayType.School => NotableHolidayType.School, + PublicHolidayType.Authorities => NotableHolidayType.Authorities, + PublicHolidayType.Optional => NotableHolidayType.Optional, + _ => NotableHolidayType.Observance + }).ToArray() ?? [], + }; + } +} \ No newline at end of file diff --git a/DysonNetwork.Pass/Account/NotableDaysController.cs b/DysonNetwork.Pass/Account/NotableDaysController.cs new file mode 100644 index 0000000..49e33f2 --- /dev/null +++ b/DysonNetwork.Pass/Account/NotableDaysController.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DysonNetwork.Pass.Account; + +[ApiController] +[Route("/api/notable")] +public class NotableDaysController(NotableDaysService days) : ControllerBase +{ + [HttpGet("{regionCode}/{year:int}")] + public async Task>> GetRegionDays(string regionCode, int year) + { + var result = await days.GetNotableDays(year, regionCode); + return Ok(result); + } + + [HttpGet("{regionCode}")] + public async Task>> GetRegionDaysCurrentYear(string regionCode) + { + var currentYear = DateTime.Now.Year; + var result = await days.GetNotableDays(currentYear, regionCode); + return Ok(result); + } + + [HttpGet("me/{year:int}")] + [Authorize] + public async Task>> GetAccountNotableDays(int year) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var region = currentUser.Region; + if (string.IsNullOrWhiteSpace(region)) region = "us"; + + var result = await days.GetNotableDays(year, region); + return Ok(result); + } + + [HttpGet("me")] + [Authorize] + public async Task>> GetAccountNotableDaysCurrentYear() + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var currentYear = DateTime.Now.Year; + var region = currentUser.Region; + if (string.IsNullOrWhiteSpace(region)) region = "us"; + + var result = await days.GetNotableDays(currentYear, region); + return Ok(result); + } +} diff --git a/DysonNetwork.Pass/Account/NotableDaysService.cs b/DysonNetwork.Pass/Account/NotableDaysService.cs new file mode 100644 index 0000000..ea0c505 --- /dev/null +++ b/DysonNetwork.Pass/Account/NotableDaysService.cs @@ -0,0 +1,34 @@ +using DysonNetwork.Shared.Cache; +using Nager.Holiday; + +namespace DysonNetwork.Pass.Account; + +public class NotableDaysService(ICacheService cache) +{ + private const string NotableDaysCacheKeyPrefix = "notable:"; + + public async Task> GetNotableDays(int? year, string regionCode) + { + year ??= DateTime.UtcNow.Year; + + // Generate cache key using year and region code + var cacheKey = $"{NotableDaysCacheKeyPrefix}:{year}:{regionCode}"; + + // Try to get from cache first + var (found, cachedDays) = await cache.GetAsyncWithStatus>(cacheKey); + if (found && cachedDays != null) + { + return cachedDays; + } + + // If not in cache, fetch from API + using var holidayClient = new HolidayClient(); + var holidays = await holidayClient.GetHolidaysAsync(year.Value, regionCode); + var days = holidays?.Select(NotableDay.FromNagerHoliday).ToList() ?? []; + + // Cache the result for 1 day (holiday data doesn't change frequently) + await cache.SetAsync(cacheKey, days, TimeSpan.FromDays(1)); + + return days; + } +} diff --git a/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs index efd4ca3..712c03a 100644 --- a/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs @@ -193,6 +193,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/DysonNetwork.sln.DotSettings.user b/DysonNetwork.sln.DotSettings.user index 7042f50..6e97067 100644 --- a/DysonNetwork.sln.DotSettings.user +++ b/DysonNetwork.sln.DotSettings.user @@ -112,6 +112,8 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded