✨ Notable days (holiday)
This commit is contained in:
		
							
								
								
									
										53
									
								
								DysonNetwork.Pass/Account/NotableDay.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								DysonNetwork.Pass/Account/NotableDay.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | using Nager.Holiday; | ||||||
|  | using NodaTime; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Pass.Account; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | /// Reference from Nager.Holiday | ||||||
|  | /// </summary> | ||||||
|  | public enum NotableHolidayType | ||||||
|  | { | ||||||
|  |     /// <summary>Public holiday</summary> | ||||||
|  |     Public, | ||||||
|  |     /// <summary>Bank holiday, banks and offices are closed</summary> | ||||||
|  |     Bank, | ||||||
|  |     /// <summary>School holiday, schools are closed</summary> | ||||||
|  |     School, | ||||||
|  |     /// <summary>Authorities are closed</summary> | ||||||
|  |     Authorities, | ||||||
|  |     /// <summary>Majority of people take a day off</summary> | ||||||
|  |     Optional, | ||||||
|  |     /// <summary>Optional festivity, no paid day off</summary> | ||||||
|  |     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() ?? [], | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										51
									
								
								DysonNetwork.Pass/Account/NotableDaysController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								DysonNetwork.Pass/Account/NotableDaysController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<ActionResult<List<NotableDay>>> GetRegionDays(string regionCode, int year) | ||||||
|  |     { | ||||||
|  |         var result = await days.GetNotableDays(year, regionCode); | ||||||
|  |         return Ok(result); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     [HttpGet("{regionCode}")] | ||||||
|  |     public async Task<ActionResult<List<NotableDay>>> 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<ActionResult<List<NotableDay>>> 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<ActionResult<List<NotableDay>>> 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); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										34
									
								
								DysonNetwork.Pass/Account/NotableDaysService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								DysonNetwork.Pass/Account/NotableDaysService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<List<NotableDay>> 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<List<NotableDay>>(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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -193,6 +193,7 @@ public static class ServiceCollectionExtensions | |||||||
|         services.AddScoped<ActionLogService>(); |         services.AddScoped<ActionLogService>(); | ||||||
|         services.AddScoped<AccountService>(); |         services.AddScoped<AccountService>(); | ||||||
|         services.AddScoped<AccountEventService>(); |         services.AddScoped<AccountEventService>(); | ||||||
|  |         services.AddScoped<NotableDaysService>(); | ||||||
|         services.AddScoped<ActionLogService>(); |         services.AddScoped<ActionLogService>(); | ||||||
|         services.AddScoped<RelationshipService>(); |         services.AddScoped<RelationshipService>(); | ||||||
|         services.AddScoped<MagicSpellService>(); |         services.AddScoped<MagicSpellService>(); | ||||||
|   | |||||||
| @@ -112,6 +112,8 @@ | |||||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe6898c1ddf974e16b95b114722270029e55000_003F5c_003F24270f3e_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe6898c1ddf974e16b95b114722270029e55000_003F5c_003F24270f3e_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APresignedGetObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F0df26a9d89e29319e9efcaea0a8489db9e97bc1aedcca3f7e360cc50f8f4ea_003FPresignedGetObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APresignedGetObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F0df26a9d89e29319e9efcaea0a8489db9e97bc1aedcca3f7e360cc50f8f4ea_003FPresignedGetObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APresignedPutObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F9202b7fc1762440f9948968ca0e7397b62200_003Fc1_003F4de0298e_003FPresignedPutObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APresignedPutObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F9202b7fc1762440f9948968ca0e7397b62200_003Fc1_003F4de0298e_003FPresignedPutObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||||
|  | 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APublicHolidayType_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F7256e0468d0b4a9ca42aecf1bfb8a7b12a00_003Fb9_003F806179e4_003FPublicHolidayType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||||
|  | 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APublicHoliday_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F7256e0468d0b4a9ca42aecf1bfb8a7b12a00_003Fd3_003Fa7bc1284_003FPublicHoliday_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APutObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F556d6adabb16af913b52d8825c320d848cd36ae3d8818e8d12db3a23680db73_003FPutObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APutObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F556d6adabb16af913b52d8825c320d848cd36ae3d8818e8d12db3a23680db73_003FPutObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APutObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F6efe388c7585d5dd5587416a55298550b030c2a107edf45f988791297c3ffa_003FPutObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APutObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F6efe388c7585d5dd5587416a55298550b030c2a107edf45f988791297c3ffa_003FPutObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AQueryable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F42d8f09d6a294d00a6f49efc989927492fe00_003F4e_003F26d1ee34_003FQueryable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AQueryable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F42d8f09d6a294d00a6f49efc989927492fe00_003F4e_003F26d1ee34_003FQueryable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user