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