diff --git a/src/RandomAPI/APIServices/ServiceInterfaces/ICronScheduledTask.cs b/src/RandomAPI/APIServices/ServiceInterfaces/ICronScheduledTask.cs
new file mode 100644
index 0000000..2aced34
--- /dev/null
+++ b/src/RandomAPI/APIServices/ServiceInterfaces/ICronScheduledTask.cs
@@ -0,0 +1,101 @@
+
+///
+/// Contract for a scheduled background task that includes its execution logic and its specific schedule.
+///
+public interface ICronScheduledTask
+{
+ ///
+ /// A unique name for logging and identification.
+ ///
+ string Name { get; }
+
+ ///
+ /// The schedule definition for the task.
+ /// Format: [DayOfWeek|Daily]@[HH:MM]
+ /// Examples: "Daily@08:30", "Monday@08:30", "Sunday@00:00"
+ ///
+ string Schedule { get; }
+
+ ///
+ /// Executes the scheduled job logic.
+ ///
+ Task ExecuteAsync();
+}
+
+
+///
+/// A daily scheduled task for routine maintenance (e.g., database cleanup, log rotation).
+/// Runs every day at 04:00 AM UTC.
+///
+public class DailyMaintenanceTask : ICronScheduledTask
+{
+ private readonly ILogger _logger;
+
+ public DailyMaintenanceTask(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ // --- ICronScheduledTask Implementation ---
+ public string Name => "Daily Maintenance & Cleanup";
+ public string Schedule => "Daily@04:00"; // Run every day at 4:00 AM UTC
+ // ----------------------------------------
+
+ public async Task ExecuteAsync()
+ {
+ _logger.LogInformation("Executing Daily Maintenance Task: Running routine cleanup.");
+
+ // --- Actual Scheduled Logic ---
+ await Task.Delay(100); // Simulate asynchronous database cleanup or file deletion
+ // Note: For a real cleanup task, you would inject and use a repository here.
+ // ------------------------------
+
+ _logger.LogInformation("Daily Maintenance Task completed successfully.");
+ }
+}
+
+///
+/// Scheduled task for generating a report every Monday morning.
+///
+public class BiWeeklyReportTaskMonday : ICronScheduledTask
+{
+ private readonly ILogger _logger;
+
+ public BiWeeklyReportTaskMonday(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public string Name => "Bi-Weekly Report (Monday)";
+ public string Schedule => "Monday@08:30"; // Run every Monday at 8:30 AM UTC
+
+ public async Task ExecuteAsync()
+ {
+ _logger.LogInformation("Executing Monday Report Task: Compiling weekly progress report.");
+ await Task.Delay(100); // Simulate report generation
+ _logger.LogInformation("Monday Report Task completed successfully.");
+ }
+}
+
+///
+/// Scheduled task for generating a report every Friday morning.
+///
+public class BiWeeklyReportTaskFriday : ICronScheduledTask
+{
+ private readonly ILogger _logger;
+
+ public BiWeeklyReportTaskFriday(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public string Name => "Bi-Weekly Report (Friday)";
+ public string Schedule => "Friday@08:30"; // Run every Friday at 8:30 AM UTC
+
+ public async Task ExecuteAsync()
+ {
+ _logger.LogInformation("Executing Friday Report Task: Compiling end-of-week summary report.");
+ await Task.Delay(100); // Simulate report generation
+ _logger.LogInformation("Friday Report Task completed successfully.");
+ }
+}
\ No newline at end of file
diff --git a/src/RandomAPI/APIServices/Services/CronTaskRunnerService.cs b/src/RandomAPI/APIServices/Services/CronTaskRunnerService.cs
new file mode 100644
index 0000000..1e80c70
--- /dev/null
+++ b/src/RandomAPI/APIServices/Services/CronTaskRunnerService.cs
@@ -0,0 +1,116 @@
+
+///
+/// The centralized background service that monitors the clock and executes
+/// all registered ICronScheduledTask instances based on their Schedule property.
+///
+public class CronTaskRunnerService : BackgroundService
+{
+ private readonly IServiceProvider _serviceProvider;
+ private readonly ILogger _logger;
+
+ // The runner checks the schedule every 60 seconds (or less if needed).
+ private static readonly TimeSpan CheckInterval = TimeSpan.FromSeconds(60);
+
+ public CronTaskRunnerService(
+ ILogger logger,
+ IServiceProvider serviceProvider)
+ {
+ _logger = logger;
+ _serviceProvider = serviceProvider;
+ }
+
+ ///
+ /// Logic to check if a task's schedule matches the current minute.
+ /// Uses a custom format: [DayOfWeek|Daily]@[HH:MM]
+ ///
+ private static bool IsScheduleDue(string schedule, DateTime now)
+ {
+ // Example: "Sunday@00:00"
+ if (string.IsNullOrWhiteSpace(schedule) || !schedule.Contains('@')) return false;
+
+ var parts = schedule.Split('@');
+ var dayPart = parts[0].Trim();
+ var timePart = parts[1].Trim();
+
+ // 1. Check Time (HH:MM)
+ // Check if the current hour and minute match the scheduled time
+ if (!TimeSpan.TryParseExact(timePart, "hh\\:mm", null, out TimeSpan scheduledTime))
+ {
+ return false;
+ }
+
+ // Only fire if the current UTC hour and minute match the scheduled time
+ if (now.Hour != scheduledTime.Hours || now.Minute != scheduledTime.Minutes)
+ {
+ return false;
+ }
+
+ // 2. Check Day (Daily or Specific DayOfWeek)
+ if (dayPart.Equals("Daily", StringComparison.OrdinalIgnoreCase))
+ {
+ return true; // Scheduled to run every day at this time
+ }
+
+ // Check if the current DayOfWeek matches the scheduled day
+ if (Enum.TryParse(dayPart, true, out DayOfWeek scheduledDay))
+ {
+ return now.DayOfWeek == scheduledDay;
+ }
+
+ return false;
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ _logger.LogInformation("Cron Task Runner Service started. Checking schedule every minute.");
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ // Get the current time in UTC, rounded down to the nearest minute.
+ var now = DateTime.UtcNow.AddSeconds(-DateTime.UtcNow.Second);
+ _logger.LogDebug($"Checking schedules for time: {now:yyyy-MM-dd HH:mm} UTC");
+
+ try
+ {
+ // Must create a scope for each execution cycle
+ using (var scope = _serviceProvider.CreateScope())
+ {
+ // Resolve ALL services registered under the ICronScheduledTask contract.
+ IEnumerable scheduledTasks =
+ scope.ServiceProvider.GetServices();
+
+ var tasksToRun = scheduledTasks
+ .Where(task => IsScheduleDue(task.Schedule, now))
+ .ToList();
+
+ if (tasksToRun.Any())
+ {
+ _logger.LogInformation($"Found {tasksToRun.Count} tasks due now. Executing concurrently.");
+
+ var executionTasks = tasksToRun
+ .Select(task => task.ExecuteAsync().ContinueWith(t =>
+ {
+ if (t.IsFaulted)
+ {
+ _logger.LogError(t.Exception, $"Task '{task.Name}' failed to execute.");
+ }
+ }, TaskContinuationOptions.ExecuteSynchronously)) // Ensure logging is safe
+ .ToList();
+
+ await Task.WhenAll(executionTasks);
+ _logger.LogInformation("Concurrent execution completed for this cycle.");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "An unhandled error occurred during the Cron execution cycle.");
+ }
+
+ // Wait for the next interval check.
+ await Task.Delay(CheckInterval, stoppingToken);
+ }
+
+ _logger.LogInformation("Cron Task Runner Service stopping.");
+ }
+}
\ No newline at end of file
diff --git a/src/RandomAPI/APIServices/Services/WebhookActionService.cs b/src/RandomAPI/APIServices/Services/WebhookActionService.cs
index b96268b..a394797 100644
--- a/src/RandomAPI/APIServices/Services/WebhookActionService.cs
+++ b/src/RandomAPI/APIServices/Services/WebhookActionService.cs
@@ -37,14 +37,13 @@ public async Task HandleRegisterActionAsync([FromBody] string url
public async Task HandleUnregisterActionAsync([FromBody] string url)
{
- string safeUrlForLog = url;
if (string.IsNullOrWhiteSpace(url))
{
return new BadRequestObjectResult("URL cannot be empty.");
}
- safeUrlForLog = SanitizeURL(ref url);
+ string safeUrlForLog = SanitizeURL(ref url);
var removed = await base.RemoveListenerAsync(url);
if (!removed)
{
diff --git a/src/RandomAPI/DTOs/CalendarEventDto.cs b/src/RandomAPI/DTOs/CalendarEventDto.cs
new file mode 100644
index 0000000..2f1ddb5
--- /dev/null
+++ b/src/RandomAPI/DTOs/CalendarEventDto.cs
@@ -0,0 +1,210 @@
+using RandomAPI.Repository;
+using RandomAPI.Models;
+using System.Text.Json;
+
+// PRODUCTION READY:
+// To make this file compile with the real logic uncommented, you must install:
+//Google.Apis.Calendar.v3(NuGet)
+using Google.Apis.Calendar.v3;
+using Google.Apis.Calendar.v3.Data;
+
+namespace RandomAPI.DTOs
+{
+ ///
+ /// Data Transfer Object representing a single scheduled calendar event.
+ ///
+ public class CalendarEventDTO
+ {
+ public required string Summary { get; set; }
+ public required DateTime StartTime { get; set; }
+ public required DateTime EndTime { get; set; }
+ public string? Location { get; set; }
+ public string? Description { get; set; }
+ }
+ ///
+ /// Contract for interacting with the Google Calendar API.
+ /// (Uses a mock implementation until full API integration is configured.)
+ ///
+ public interface IGoogleCalendarService
+ {
+ ///
+ /// Fetches all events scheduled for a specific day.
+ ///
+ /// The day for which to fetch events (e.g., Monday or Friday).
+ /// A list of scheduled events.
+ Task> GetEventsForDayAsync(DateTime date);
+ }
+
+ ///
+ /// MOCK implementation of the Google Calendar Service.
+ /// NOTE: In a production environment, this is where you would integrate
+ /// the Google.Apis.Calendar.v3 package and use OAuth credentials.
+ ///
+ public class GoogleCalendarService : IGoogleCalendarService
+ {
+ private readonly ILogger _logger;
+ private readonly IEventRepository _eventRepository;
+
+ public GoogleCalendarService(ILogger logger, IEventRepository eventRepository)
+ {
+ _logger = logger;
+ _eventRepository = eventRepository;
+ }
+
+ public async Task> GetEventsForDayAsync(DateTime date)
+ {
+ _logger.LogWarning("Using MOCK Google Calendar Service. No real API call is being made.");
+
+ // --- Placeholder Data Generation ---
+
+ var mockEvents = new List();
+
+ if (date.DayOfWeek == DayOfWeek.Monday)
+ {
+ mockEvents.Add(new CalendarEventDTO
+ {
+ Summary = "Weekly Planning Meeting",
+ StartTime = date.Date.AddHours(9).AddMinutes(30),
+ EndTime = date.Date.AddHours(10).AddMinutes(30),
+ Location = "Zoom Link",
+ Description = "Review last week's tickets and plan sprints."
+ });
+ mockEvents.Add(new CalendarEventDTO
+ {
+ Summary = "Client Check-in Call",
+ StartTime = date.Date.AddHours(14).AddMinutes(0),
+ EndTime = date.Date.AddHours(14).AddMinutes(45),
+ Location = "Google Meet",
+ });
+ }
+ else if (date.DayOfWeek == DayOfWeek.Friday)
+ {
+ mockEvents.Add(new CalendarEventDTO
+ {
+ Summary = "Project Retrospective",
+ StartTime = date.Date.AddHours(11).AddMinutes(0),
+ EndTime = date.Date.AddHours(12).AddMinutes(0),
+ Location = "Office Lounge",
+ Description = "Discuss what went well and areas for improvement."
+ });
+ }
+
+ _logger.LogInformation($"Mock events generated for {date.ToShortDateString()}: {mockEvents.Count} events.");
+
+ try
+ {
+ var jsonData = JsonSerializer.Serialize(mockEvents);
+
+ var logEntry = new Models.Event(
+ service: "GoogleCalendarService",
+ type: "EventFetch",
+ jsonData: jsonData
+ );
+
+ _logger.LogDebug($"AUDIT LOG: Calendar events pulled. {mockEvents.Count} events logged.");
+ await _eventRepository.AddEventAsync(logEntry);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to log calendar fetch activity to the Event Repository.");
+ }
+
+ return mockEvents;
+ }
+ }
+
+
+ ///
+ /// Contract for a service dedicated to external interactions with the Google Calendar API.
+ /// This layer is responsible for network communication and parsing raw data.
+ ///
+ public interface IGoogleCalendarExternalService
+ {
+ ///
+ /// Fetches calendar events directly from the Google API for a specific day.
+ ///
+ /// The day for which to fetch events.
+ /// A list of scheduled events (DTOs).
+ Task> GetRawEventsForDayAsync(DateTime date);
+ }
+
+
+
+ ///
+ /// PRODUCTION implementation of the external Google Calendar client.
+ /// This service is solely responsible for fetching data from the real API
+ /// using an injected, authenticated client.
+ ///
+ public class GoogleCalendarExternalService : IGoogleCalendarExternalService
+ {
+ private readonly ILogger _logger;
+
+ // PRODUCTION: This field holds the actual Google API client.
+ private readonly CalendarService _calendarApi;
+
+ public GoogleCalendarExternalService(
+ ILogger logger, CalendarService calendarApi ) // PRODUCTION: Inject authenticated client
+ {
+ _logger = logger;
+ _calendarApi = calendarApi;
+ }
+
+ public async Task> GetRawEventsForDayAsync(DateTime date)
+ {
+ _logger.LogInformation($"Attempting to fetch events from Google API for: {date.ToShortDateString()}.");
+ var fetchedEvents = new List();
+
+ try
+ {
+ // 1. Define the service request parameters
+ var request = _calendarApi.Events.List("primary"); // "primary" is the user's main calendar ID
+
+ // TimeMin and TimeMax must be defined in RFC3339 format, often handled by DateTimeOffset conversion
+ request.TimeMin = date.Date;
+ request.TimeMax = date.Date.AddDays(1); // Fetch for the whole day
+ request.SingleEvents = true; // Required to expand recurring events into individual occurrences
+ request.OrderBy = EventsResource.ListRequest.OrderByEnum.StartTime;
+ request.ShowDeleted = false; // Only get active events
+
+ // 2. Execute the request
+ // Events is the Google API's data structure
+ Events apiEvents = await request.ExecuteAsync();
+
+ // 3. Map Google API Events (Event) to your DTO (CalendarEventDTO)
+ if (apiEvents.Items != null)
+ {
+ fetchedEvents = apiEvents.Items
+ .Where(item => item.Status != "cancelled")
+ .Select(item => new CalendarEventDTO
+ {
+ Summary = item.Summary,
+ Location = item.Location,
+ Description = item.Description,
+ // Note: We check for the preferred DateTimeOffset values first
+ StartTime = item.Start.DateTimeOffset.HasValue
+ ? item.Start.DateTimeOffset.Value.DateTime
+ : item.Start.Date is string startDateString
+ ? DateOnly.Parse(startDateString).ToDateTime(TimeOnly.MinValue)
+ : DateTime.MinValue,
+ EndTime = item.End.DateTimeOffset.HasValue
+ ? item.End.DateTimeOffset.Value.DateTime
+ : item.End.Date is string endDateString
+ ? DateOnly.Parse(endDateString).ToDateTime(TimeOnly.MinValue)
+ : DateTime.MinValue
+ }).ToList();
+ }
+
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to execute Google Calendar API request.");
+ // Important: Throw the exception so the upstream GoogleCalendarService can handle
+ // the failure and prevent bad logging data.
+ throw;
+ }
+
+ _logger.LogDebug($"External fetch completed: {fetchedEvents.Count} events returned.");
+ return fetchedEvents;
+ }
+ }
+}
diff --git a/src/RandomAPI/Program.cs b/src/RandomAPI/Program.cs
index c7e3100..32753f4 100644
--- a/src/RandomAPI/Program.cs
+++ b/src/RandomAPI/Program.cs
@@ -34,6 +34,16 @@
builder.Services.AddScoped();
builder.Services.AddScoped();
+// CRON schjeduled services
+builder.Services.AddHostedService();
+// Register all tasks as Scoped
+// Schedule 1: Once a day at 4:00 AM (Schedule: "Daily@04:00")
+builder.Services.AddScoped();
+// Schedule 2: Monday and Friday mornings @ 8:30 AM (Schedule: "Monday@08:30" and "Friday@08:30")
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
+
#endregion
#region Initialization
diff --git a/src/RandomAPI/RandomAPI.csproj b/src/RandomAPI/RandomAPI.csproj
index f587fa9..1aac139 100644
--- a/src/RandomAPI/RandomAPI.csproj
+++ b/src/RandomAPI/RandomAPI.csproj
@@ -10,6 +10,8 @@
+
+