Added scheduler jobs
This commit is contained in:
@@ -28,50 +28,149 @@ public sealed class LateProjectEventHandler(IDbContextFactory<ApplicationDbConte
|
|||||||
{
|
{
|
||||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
var lateMilestones = await context.Milestones
|
var now = DateTime.UtcNow;
|
||||||
|
var overdueCutoff = lateDays > 0 ? now.AddDays(-lateDays) : now;
|
||||||
|
|
||||||
|
// Query overdue milestones that are not yet approved
|
||||||
|
// Execute ToListAsync before GroupBy so EF Core does not fail SQL translation
|
||||||
|
var overdueMilestones = await context.Milestones
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(i => i.Project!.Programme!.Portfolio!.Organisation!.User)
|
.Include(i => i.Project)
|
||||||
.Where(m => (DateTime.UtcNow - m.Project!.StartedAt!).Value.TotalDays < lateDays)
|
.ThenInclude(p => p!.Programme)
|
||||||
.OrderBy(o => o.CreatedAt)
|
.ThenInclude(pr => pr!.Owner)
|
||||||
.GroupBy(g => g.ProjectId)
|
.Include(i => i.Project)
|
||||||
.AsSplitQuery()
|
.ThenInclude(p => p!.Programme)
|
||||||
|
.ThenInclude(pr => pr!.Portfolio)
|
||||||
|
.ThenInclude(po => po!.Organisation)
|
||||||
|
.ThenInclude(o => o!.User)
|
||||||
|
.Where(m => m.Status != ApprovalStatus.Approved && m.DueAt <= overdueCutoff)
|
||||||
|
.OrderBy(o => o.DueAt)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
if (lateMilestones is null) return;
|
if (overdueMilestones.Count == 0) return;
|
||||||
|
|
||||||
using var htmlTemplateReader = new StreamReader(Assembly.GetAssembly(typeof(IDomain))?
|
var lateMilestonesByProject = overdueMilestones.GroupBy(g => g.ProjectId);
|
||||||
.GetManifestResourceStream("PostFundManagement.Domain.Templates.lateprojectsmilestones.html")!);
|
|
||||||
|
|
||||||
var template = await htmlTemplateReader.ReadToEndAsync(cancellationToken);
|
var partnerResourceStream = typeof(IDomain).Assembly
|
||||||
|
.GetManifestResourceStream("PostFundManagement.Domain.Templates.lateprojectsmilestones.html");
|
||||||
|
|
||||||
foreach (var group in lateMilestones)
|
if (partnerResourceStream is null) return;
|
||||||
|
|
||||||
|
using var partnerHtmlTemplateReader = new StreamReader(partnerResourceStream);
|
||||||
|
var partnerTemplate = await partnerHtmlTemplateReader.ReadToEndAsync(cancellationToken);
|
||||||
|
|
||||||
|
var programmeResourceStream = typeof(IDomain).Assembly
|
||||||
|
.GetManifestResourceStream("PostFundManagement.Domain.Templates.noncompliantprojects-prog.html");
|
||||||
|
|
||||||
|
string? programmeTemplate = null;
|
||||||
|
if (programmeResourceStream is not null)
|
||||||
{
|
{
|
||||||
if (!group.Any()) continue;
|
using var programmeHtmlTemplateReader = new StreamReader(programmeResourceStream);
|
||||||
|
programmeTemplate = await programmeHtmlTemplateReader.ReadToEndAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
var project = group.FirstOrDefault()?.Project;
|
foreach (var group in lateMilestonesByProject)
|
||||||
|
{
|
||||||
|
var milestones = group.ToList();
|
||||||
|
|
||||||
|
if (milestones.Count == 0) continue;
|
||||||
|
|
||||||
|
var project = milestones[0].Project;
|
||||||
|
|
||||||
if (project is null) continue;
|
if (project is null) continue;
|
||||||
|
|
||||||
var mostLateMilestone = group.OrderBy(m => m.DueAt).FirstOrDefault();
|
var mostLateMilestone = milestones.OrderBy(m => m.DueAt).FirstOrDefault();
|
||||||
var missedMilestone = group.Where(m => DateTime.UtcNow > m.DueAt).FirstOrDefault();
|
|
||||||
|
|
||||||
var messageBody = template;
|
var daysOverdue = mostLateMilestone != null
|
||||||
messageBody = messageBody.Replace("[ProjectName]", project.Name);
|
? (int)Math.Max(0, (now - mostLateMilestone.DueAt).TotalDays)
|
||||||
messageBody = messageBody.Replace("[ProjectStartDate]", $"{project.StartedAt:yyyy-MM-dd}");
|
: 0;
|
||||||
messageBody = messageBody.Replace("[ProjectEndDate]", $"{project.EndedAt:yyyy-MM-dd}");
|
|
||||||
messageBody = messageBody.Replace("[TotalLateMiletones]", $"{group.Count()}");
|
|
||||||
|
|
||||||
// TODO: output information of most late milestone
|
|
||||||
// TODO: output information of milestones already missed
|
|
||||||
|
|
||||||
var organisation = mostLateMilestone?.Project?.Programme?.Portfolio?.Organisation;
|
var organisation = mostLateMilestone?.Project?.Programme?.Portfolio?.Organisation;
|
||||||
|
|
||||||
if (organisation is null) continue;
|
if (organisation is null) continue;
|
||||||
|
|
||||||
var notificationRequest = new CreateNotificationRequest(organisation.Id, organisation.User!.Id, NotificationPlatform.Email,
|
var recipientId = organisation.UserId ?? organisation.CreatedBy;
|
||||||
"Project Status Update", organisation.User!.Email, messageBody, NotificationStatus.Queued);
|
var recipientEmail = organisation.User?.Email ?? organisation.Email;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(recipientEmail) || recipientId == Guid.Empty) continue;
|
||||||
|
|
||||||
|
var tableRowsBuilder = new StringBuilder();
|
||||||
|
foreach (var milestone in milestones.OrderBy(m => m.DueAt))
|
||||||
|
{
|
||||||
|
var delay = (int)Math.Max(0, (now - milestone.DueAt).TotalDays);
|
||||||
|
var statusText = milestone.Status.ToString();
|
||||||
|
var milestoneName = WebUtility.HtmlEncode(milestone.Name ?? "Milestone Deliverable");
|
||||||
|
|
||||||
|
tableRowsBuilder.AppendLine($@"
|
||||||
|
<tr style=""border-bottom: 1px solid #e2e8f0;"">
|
||||||
|
<td style=""padding: 10px 14px; font-weight: 500; color: #1e293b;"">{milestoneName}</td>
|
||||||
|
<td style=""padding: 10px 14px; color: #64748b; font-size: 13px;"">{milestone.DueAt:yyyy-MM-dd}</td>
|
||||||
|
<td style=""padding: 10px 14px; color: #dc2626; font-weight: 600; font-size: 13px;"">+{delay} d</td>
|
||||||
|
<td style=""padding: 10px 14px; font-size: 12px;""><span style=""background-color: #fee2e2; color: #991b1b; padding: 2px 8px; border-radius: 9999px; font-weight: 600;"">{statusText}</span></td>
|
||||||
|
</tr>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Send notice to Partner / Recipient Organisation
|
||||||
|
var messageBody = partnerTemplate;
|
||||||
|
messageBody = messageBody.Replace("[OrganisationName]", WebUtility.HtmlEncode(organisation.Name ?? "Partner"));
|
||||||
|
messageBody = messageBody.Replace("[RecipientName]", WebUtility.HtmlEncode(organisation.Name ?? "Partner"));
|
||||||
|
messageBody = messageBody.Replace("[ProjectName]", WebUtility.HtmlEncode(project.Name ?? "Project"));
|
||||||
|
messageBody = messageBody.Replace("[ProjectStartDate]", project.StartedAt.HasValue ? $"{project.StartedAt:yyyy-MM-dd}" : "Not Specified");
|
||||||
|
messageBody = messageBody.Replace("[ProjectEndDate]", project.EndedAt.HasValue ? $"{project.EndedAt:yyyy-MM-dd}" : "Not Specified");
|
||||||
|
messageBody = messageBody.Replace("[TotalLateMilestones]", milestones.Count.ToString());
|
||||||
|
messageBody = messageBody.Replace("[TotalLateMiletones]", milestones.Count.ToString());
|
||||||
|
messageBody = messageBody.Replace("[MostLateMilestoneName]", WebUtility.HtmlEncode(mostLateMilestone?.Name ?? "N/A"));
|
||||||
|
messageBody = messageBody.Replace("[MostLateMilestoneDueDate]", mostLateMilestone != null ? $"{mostLateMilestone.DueAt:yyyy-MM-dd}" : "N/A");
|
||||||
|
messageBody = messageBody.Replace("[MostLateMilestoneDaysOverdue]", daysOverdue.ToString());
|
||||||
|
messageBody = messageBody.Replace("[LateMilestonesTable]", tableRowsBuilder.ToString());
|
||||||
|
messageBody = messageBody.Replace("[PortalUrl]", "https://partner.ifri.khongisa.co.za");
|
||||||
|
messageBody = messageBody.Replace("[Year]", now.Year.ToString());
|
||||||
|
|
||||||
|
var notificationRequest = new CreateNotificationRequest(organisation.Id, recipientId, NotificationPlatform.Email,
|
||||||
|
$"Overdue Milestone Alert: {project.Name}", recipientEmail, messageBody, NotificationStatus.Queued);
|
||||||
|
|
||||||
_ = await service.CreateNotificationAsync(notificationRequest, SystemUser, cancellationToken);
|
_ = await service.CreateNotificationAsync(notificationRequest, SystemUser, cancellationToken);
|
||||||
|
|
||||||
|
// 2. Construct and send escalation notification for the Programme Manager (FR-M07-009)
|
||||||
|
if (!string.IsNullOrWhiteSpace(programmeTemplate))
|
||||||
|
{
|
||||||
|
var programme = project.Programme;
|
||||||
|
var programmeManager = programme?.Owner;
|
||||||
|
var programmeManagerId = programme?.OwnedBy ?? programmeManager?.Id;
|
||||||
|
var programmeManagerEmail = programmeManager?.Email;
|
||||||
|
|
||||||
|
if (programmeManagerId.HasValue && programmeManagerId.Value != Guid.Empty && !string.IsNullOrWhiteSpace(programmeManagerEmail))
|
||||||
|
{
|
||||||
|
var progMessageBody = programmeTemplate;
|
||||||
|
progMessageBody = progMessageBody.Replace("[ProgrammeManagerName]", WebUtility.HtmlEncode(programmeManagerEmail ?? "Programme Manager"));
|
||||||
|
progMessageBody = progMessageBody.Replace("[ProgrammeName]", WebUtility.HtmlEncode(programme?.Name ?? "Programme"));
|
||||||
|
progMessageBody = progMessageBody.Replace("[PortfolioName]", WebUtility.HtmlEncode(programme?.Portfolio?.Name ?? "Portfolio"));
|
||||||
|
progMessageBody = progMessageBody.Replace("[OrganisationName]", WebUtility.HtmlEncode(organisation.Name ?? "Partner"));
|
||||||
|
progMessageBody = progMessageBody.Replace("[ProjectName]", WebUtility.HtmlEncode(project.Name ?? "Project"));
|
||||||
|
progMessageBody = progMessageBody.Replace("[ProjectStartDate]", project.StartedAt.HasValue ? $"{project.StartedAt:yyyy-MM-dd}" : "Not Specified");
|
||||||
|
progMessageBody = progMessageBody.Replace("[ProjectEndDate]", project.EndedAt.HasValue ? $"{project.EndedAt:yyyy-MM-dd}" : "Not Specified");
|
||||||
|
progMessageBody = progMessageBody.Replace("[TotalLateMilestones]", milestones.Count.ToString());
|
||||||
|
progMessageBody = progMessageBody.Replace("[TotalLateMiletones]", milestones.Count.ToString());
|
||||||
|
progMessageBody = progMessageBody.Replace("[MostLateMilestoneName]", WebUtility.HtmlEncode(mostLateMilestone?.Name ?? "N/A"));
|
||||||
|
progMessageBody = progMessageBody.Replace("[MostLateMilestoneDueDate]", mostLateMilestone != null ? $"{mostLateMilestone.DueAt:yyyy-MM-dd}" : "N/A");
|
||||||
|
progMessageBody = progMessageBody.Replace("[MostLateMilestoneDaysOverdue]", daysOverdue.ToString());
|
||||||
|
progMessageBody = progMessageBody.Replace("[LateMilestonesTable]", tableRowsBuilder.ToString());
|
||||||
|
progMessageBody = progMessageBody.Replace("[ConsoleUrl]", "https://console.ifri.khongisa.co.za");
|
||||||
|
progMessageBody = progMessageBody.Replace("[PortalUrl]", "https://partner.ifri.khongisa.co.za");
|
||||||
|
progMessageBody = progMessageBody.Replace("[Year]", now.Year.ToString());
|
||||||
|
|
||||||
|
var progNotificationRequest = new CreateNotificationRequest(
|
||||||
|
organisation.Id,
|
||||||
|
programmeManagerId.Value,
|
||||||
|
NotificationPlatform.Email,
|
||||||
|
$"[Escalation] Overdue Milestones: {project.Name} ({programme?.Name})",
|
||||||
|
programmeManagerEmail,
|
||||||
|
progMessageBody,
|
||||||
|
NotificationStatus.Queued);
|
||||||
|
|
||||||
|
_ = await service.CreateNotificationAsync(progNotificationRequest, SystemUser, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using PostFundManagement.Application.Abstractions.Services;
|
||||||
|
using PostFundManagement.Application.Services.Communications;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Events;
|
||||||
|
|
||||||
|
public sealed class NonCompliantProjectEvent : IEvent
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.CreateVersion7();
|
||||||
|
|
||||||
|
public string Name { get; set; } = nameof(NonCompliantProjectEvent);
|
||||||
|
|
||||||
|
public DateTimeOffset EnqueueAt { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
public string CorrelationId { get; set; } = "noncompliant-project-check";
|
||||||
|
|
||||||
|
public static NonCompliantProjectEvent Create() => new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class NonCompliantProjectEventHandler(IDbContextFactory<ApplicationDbContext> contextFactory,
|
||||||
|
ICommunicationsService service) : INotificationHandler<NonCompliantProjectEvent>
|
||||||
|
{
|
||||||
|
public async ValueTask Handle(NonCompliantProjectEvent notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Fetch active/monitored projects with programme, portfolio, and award organisations
|
||||||
|
var projects = await context.Projects
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(p => p.Programme)
|
||||||
|
.ThenInclude(pr => pr!.Owner)
|
||||||
|
.Include(p => p.Programme)
|
||||||
|
.ThenInclude(pr => pr!.Portfolio)
|
||||||
|
.ThenInclude(po => po!.Organisation)
|
||||||
|
.ThenInclude(o => o!.User)
|
||||||
|
.Include(p => p.Awards)
|
||||||
|
.ThenInclude(a => a.Organisation)
|
||||||
|
.ThenInclude(o => o!.User)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (projects.Count == 0) return;
|
||||||
|
|
||||||
|
// Fetch compliance items and milestones to evaluate compliance according to ConsoleUI rules
|
||||||
|
var complianceItems = await context.ComplianceItems
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var milestones = await context.Milestones
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var complianceByProject = complianceItems.GroupBy(c => c.ProjectId)
|
||||||
|
.ToDictionary(g => g.Key, g => g.ToList());
|
||||||
|
|
||||||
|
var milestonesByProject = milestones.GroupBy(m => m.ProjectId)
|
||||||
|
.ToDictionary(g => g.Key, g => g.ToList());
|
||||||
|
|
||||||
|
// Load noncompliantprojects.html template for partner organisations
|
||||||
|
var partnerResourceStream = typeof(IDomain).Assembly
|
||||||
|
.GetManifestResourceStream("PostFundManagement.Domain.Templates.noncompliantprojects.html");
|
||||||
|
|
||||||
|
if (partnerResourceStream is null) return;
|
||||||
|
|
||||||
|
using var partnerHtmlTemplateReader = new StreamReader(partnerResourceStream);
|
||||||
|
var partnerTemplate = await partnerHtmlTemplateReader.ReadToEndAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Load noncompliantprojects-prog.html template for programme managers
|
||||||
|
var progResourceStream = typeof(IDomain).Assembly
|
||||||
|
.GetManifestResourceStream("PostFundManagement.Domain.Templates.noncompliantprojects-prog.html");
|
||||||
|
|
||||||
|
string? programmeTemplate = null;
|
||||||
|
if (progResourceStream is not null)
|
||||||
|
{
|
||||||
|
using var progHtmlTemplateReader = new StreamReader(progResourceStream);
|
||||||
|
programmeTemplate = await progHtmlTemplateReader.ReadToEndAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var project in projects)
|
||||||
|
{
|
||||||
|
complianceByProject.TryGetValue(project.Id, out var pComplianceItems);
|
||||||
|
pComplianceItems ??= [];
|
||||||
|
|
||||||
|
milestonesByProject.TryGetValue(project.Id, out var pMilestones);
|
||||||
|
pMilestones ??= [];
|
||||||
|
|
||||||
|
// ConsoleUI Invariant: A project is 100% compliant if unmetCompCount == 0 && overdueMilestones.Count == 0
|
||||||
|
var unmetCompItems = pComplianceItems.Where(c => c.Status == ComplianceStatus.NotMet).ToList();
|
||||||
|
var overdueMilestones = pMilestones
|
||||||
|
.Where(m => m.Status != ApprovalStatus.Approved && m.DueAt < now)
|
||||||
|
.OrderBy(m => m.DueAt)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var isCompliant = unmetCompItems.Count == 0 && overdueMilestones.Count == 0;
|
||||||
|
if (isCompliant) continue;
|
||||||
|
|
||||||
|
// Compliance rate calculation
|
||||||
|
var totalEvaluated = pComplianceItems.Count + pMilestones.Count;
|
||||||
|
var totalMet = pComplianceItems.Count(c => c.Status == ComplianceStatus.Met || c.Status == ComplianceStatus.Exceeded)
|
||||||
|
+ pMilestones.Count(m => m.Status == ApprovalStatus.Approved);
|
||||||
|
var compliancePercentage = totalEvaluated > 0
|
||||||
|
? (int)Math.Round((double)totalMet / totalEvaluated * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Resolve target partner organisation
|
||||||
|
var organisation = project.Awards.FirstOrDefault()?.Organisation
|
||||||
|
?? project.Programme?.Portfolio?.Organisation;
|
||||||
|
|
||||||
|
if (organisation is null) continue;
|
||||||
|
|
||||||
|
var recipientId = organisation.UserId ?? organisation.CreatedBy;
|
||||||
|
var recipientEmail = organisation.User?.Email ?? organisation.Email;
|
||||||
|
if (string.IsNullOrWhiteSpace(recipientEmail) || recipientId == Guid.Empty) continue;
|
||||||
|
|
||||||
|
// Construct non-compliance table rows
|
||||||
|
var tableRowsBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (var item in unmetCompItems)
|
||||||
|
{
|
||||||
|
var title = WebUtility.HtmlEncode(item.Title ?? "Statutory Compliance Obligation");
|
||||||
|
var requirements = WebUtility.HtmlEncode(item.Requirements ?? "Verification evidence required");
|
||||||
|
|
||||||
|
tableRowsBuilder.AppendLine($@"
|
||||||
|
<tr style=""border-bottom: 1px solid #e2e8f0;"">
|
||||||
|
<td style=""padding: 10px 14px; font-weight: 600; color: #475569; font-size: 12px;"">Statutory / Legal</td>
|
||||||
|
<td style=""padding: 10px 14px; font-weight: 500; color: #1e293b;"">{title}</td>
|
||||||
|
<td style=""padding: 10px 14px; color: #64748b; font-size: 13px;"">{requirements}</td>
|
||||||
|
<td style=""padding: 10px 14px; font-size: 12px;""><span style=""background-color: #fee2e2; color: #991b1b; padding: 2px 8px; border-radius: 9999px; font-weight: 600;"">Not Met</span></td>
|
||||||
|
</tr>");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var m in overdueMilestones)
|
||||||
|
{
|
||||||
|
var delay = (int)Math.Max(0, (now - m.DueAt).TotalDays);
|
||||||
|
var mName = WebUtility.HtmlEncode(m.Name ?? "Milestone Deliverable");
|
||||||
|
|
||||||
|
tableRowsBuilder.AppendLine($@"
|
||||||
|
<tr style=""border-bottom: 1px solid #e2e8f0;"">
|
||||||
|
<td style=""padding: 10px 14px; font-weight: 600; color: #d97706; font-size: 12px;"">Milestone SLA</td>
|
||||||
|
<td style=""padding: 10px 14px; font-weight: 500; color: #1e293b;"">{mName}</td>
|
||||||
|
<td style=""padding: 10px 14px; color: #dc2626; font-size: 13px;"">Due: {m.DueAt:yyyy-MM-dd} (+{delay} d)</td>
|
||||||
|
<td style=""padding: 10px 14px; font-size: 12px;""><span style=""background-color: #fee2e2; color: #991b1b; padding: 2px 8px; border-radius: 9999px; font-weight: 600;"">Overdue</span></td>
|
||||||
|
</tr>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Dispatch partner audit notice
|
||||||
|
var messageBody = partnerTemplate;
|
||||||
|
messageBody = messageBody.Replace("[OrganisationName]", WebUtility.HtmlEncode(organisation.Name ?? "Partner"));
|
||||||
|
messageBody = messageBody.Replace("[ProjectName]", WebUtility.HtmlEncode(project.Name ?? "Project"));
|
||||||
|
messageBody = messageBody.Replace("[ProgrammeName]", WebUtility.HtmlEncode(project.Programme?.Name ?? "Programme"));
|
||||||
|
messageBody = messageBody.Replace("[PortfolioName]", WebUtility.HtmlEncode(project.Programme?.Portfolio?.Name ?? "Portfolio"));
|
||||||
|
messageBody = messageBody.Replace("[ProjectStartDate]", project.StartedAt.HasValue ? $"{project.StartedAt:yyyy-MM-dd}" : "Not Specified");
|
||||||
|
messageBody = messageBody.Replace("[ProjectEndDate]", project.EndedAt.HasValue ? $"{project.EndedAt:yyyy-MM-dd}" : "Not Specified");
|
||||||
|
messageBody = messageBody.Replace("[ComplianceScore]", $"{compliancePercentage}%");
|
||||||
|
messageBody = messageBody.Replace("[UnmetComplianceCount]", unmetCompItems.Count.ToString());
|
||||||
|
messageBody = messageBody.Replace("[UnmetMilestonesCount]", overdueMilestones.Count.ToString());
|
||||||
|
messageBody = messageBody.Replace("[NonComplianceTable]", tableRowsBuilder.ToString());
|
||||||
|
messageBody = messageBody.Replace("[PortalUrl]", "https://partner.ifri.khongisa.co.za");
|
||||||
|
messageBody = messageBody.Replace("[ConsoleUrl]", "https://console.ifri.khongisa.co.za");
|
||||||
|
messageBody = messageBody.Replace("[Year]", now.Year.ToString());
|
||||||
|
|
||||||
|
var notificationRequest = new CreateNotificationRequest(
|
||||||
|
organisation.Id,
|
||||||
|
recipientId,
|
||||||
|
NotificationPlatform.Email,
|
||||||
|
$"[Audit Notice] Project Non-Compliance: {project.Name}",
|
||||||
|
recipientEmail,
|
||||||
|
messageBody,
|
||||||
|
NotificationStatus.Queued);
|
||||||
|
|
||||||
|
_ = await service.CreateNotificationAsync(notificationRequest, SystemUser, cancellationToken);
|
||||||
|
|
||||||
|
// 2. Dispatch internal escalation to Programme Manager (FR-M07-009 / M10)
|
||||||
|
if (!string.IsNullOrWhiteSpace(programmeTemplate))
|
||||||
|
{
|
||||||
|
var programme = project.Programme;
|
||||||
|
var programmeManager = programme?.Owner;
|
||||||
|
var programmeManagerId = programme?.OwnedBy ?? programmeManager?.Id;
|
||||||
|
var programmeManagerEmail = programmeManager?.Email;
|
||||||
|
|
||||||
|
if (programmeManagerId.HasValue && programmeManagerId.Value != Guid.Empty && !string.IsNullOrWhiteSpace(programmeManagerEmail))
|
||||||
|
{
|
||||||
|
var progMessageBody = programmeTemplate;
|
||||||
|
progMessageBody = progMessageBody.Replace("[ProgrammeManagerName]", WebUtility.HtmlEncode(programmeManagerEmail ?? "Programme Manager"));
|
||||||
|
progMessageBody = progMessageBody.Replace("[ProgrammeName]", WebUtility.HtmlEncode(programme?.Name ?? "Programme"));
|
||||||
|
progMessageBody = progMessageBody.Replace("[PortfolioName]", WebUtility.HtmlEncode(programme?.Portfolio?.Name ?? "Portfolio"));
|
||||||
|
progMessageBody = progMessageBody.Replace("[OrganisationName]", WebUtility.HtmlEncode(organisation.Name ?? "Partner"));
|
||||||
|
progMessageBody = progMessageBody.Replace("[ProjectName]", WebUtility.HtmlEncode(project.Name ?? "Project"));
|
||||||
|
progMessageBody = progMessageBody.Replace("[ProjectStartDate]", project.StartedAt.HasValue ? $"{project.StartedAt:yyyy-MM-dd}" : "Not Specified");
|
||||||
|
progMessageBody = progMessageBody.Replace("[ProjectEndDate]", project.EndedAt.HasValue ? $"{project.EndedAt:yyyy-MM-dd}" : "Not Specified");
|
||||||
|
progMessageBody = progMessageBody.Replace("[TotalLateMilestones]", overdueMilestones.Count.ToString());
|
||||||
|
progMessageBody = progMessageBody.Replace("[TotalLateMiletones]", overdueMilestones.Count.ToString());
|
||||||
|
progMessageBody = progMessageBody.Replace("[MostLateMilestoneName]", WebUtility.HtmlEncode(overdueMilestones.FirstOrDefault()?.Name ?? "N/A"));
|
||||||
|
progMessageBody = progMessageBody.Replace("[MostLateMilestoneDueDate]", overdueMilestones.FirstOrDefault() != null ? $"{overdueMilestones.First().DueAt:yyyy-MM-dd}" : "N/A");
|
||||||
|
progMessageBody = progMessageBody.Replace("[MostLateMilestoneDaysOverdue]", overdueMilestones.FirstOrDefault() != null ? $"{(int)Math.Max(0, (now - overdueMilestones.First().DueAt).TotalDays)}" : "0");
|
||||||
|
progMessageBody = progMessageBody.Replace("[LateMilestonesTable]", tableRowsBuilder.ToString());
|
||||||
|
progMessageBody = progMessageBody.Replace("[ConsoleUrl]", "https://console.ifri.khongisa.co.za");
|
||||||
|
progMessageBody = progMessageBody.Replace("[PortalUrl]", "https://partner.ifri.khongisa.co.za");
|
||||||
|
progMessageBody = progMessageBody.Replace("[Year]", now.Year.ToString());
|
||||||
|
|
||||||
|
var progNotificationRequest = new CreateNotificationRequest(
|
||||||
|
organisation.Id,
|
||||||
|
programmeManagerId.Value,
|
||||||
|
NotificationPlatform.Email,
|
||||||
|
$"[Weekly Audit Escalation] Non-Compliant Project: {project.Name} ({programme?.Name})",
|
||||||
|
programmeManagerEmail,
|
||||||
|
progMessageBody,
|
||||||
|
NotificationStatus.Queued);
|
||||||
|
|
||||||
|
_ = await service.CreateNotificationAsync(progNotificationRequest, SystemUser, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ public static class Constants
|
|||||||
|
|
||||||
public const string FeatureEmailProcessor = "EmailProcessorJobEnabled";
|
public const string FeatureEmailProcessor = "EmailProcessorJobEnabled";
|
||||||
public const string FeatureLateProjectReminder = "LateProjectReminderEnabled";
|
public const string FeatureLateProjectReminder = "LateProjectReminderEnabled";
|
||||||
|
public const string FeatureNonCompliantProjectCheck = "NonCompliantProjectCheckEnabled";
|
||||||
public const string FeatureServiceCaching = "ServiceCaching";
|
public const string FeatureServiceCaching = "ServiceCaching";
|
||||||
|
|
||||||
public static Guid SystemUser { get; } = new(0x6059f1e4, 0x6a41, 0x4447, 0xa5, 0x46, 0x86, 0xec, 0xe5, 0x10, 0x20, 0x4) /* 6059f1e4-6a41-4447-a546-86ece5102004 */;
|
public static Guid SystemUser { get; } = new(0x6059f1e4, 0x6a41, 0x4447, 0xa5, 0x46, 0x86, 0xec, 0xe5, 0x10, 0x20, 0x4) /* 6059f1e4-6a41-4447-a546-86ece5102004 */;
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Templates\lateprojectsmilestones.html" />
|
<None Remove="Templates\lateprojectsmilestones.html" />
|
||||||
<None Remove="Templates\noncompliantprojects.html" />
|
<None Remove="Templates\noncompliantprojects.html" />
|
||||||
|
<None Remove="Templates\noncompliantprojects-prog.html" />
|
||||||
<EmbeddedResource Include="Templates\lateprojectsmilestones.html" />
|
<EmbeddedResource Include="Templates\lateprojectsmilestones.html" />
|
||||||
<EmbeddedResource Include="Templates\noncompliantprojects.html" />
|
<EmbeddedResource Include="Templates\noncompliantprojects.html" />
|
||||||
|
<EmbeddedResource Include="Templates\noncompliantprojects-prog.html" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Transformation -->
|
<!-- Transformation -->
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>Project Milestone Overdue Notification</title>
|
||||||
|
<!--[if mso]>
|
||||||
|
<style type="text/css">
|
||||||
|
body, table, td { font-family: Arial, Helvetica, sans-serif !important; }
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; background-color: #f1f5f9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; color: #1e293b; line-height: 1.6;">
|
||||||
|
|
||||||
|
<!-- Wrapper Table -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f1f5f9; padding: 32px 16px;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<!-- Main Container Card (600px) -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px; background-color: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0;">
|
||||||
|
|
||||||
|
<!-- Header Bar -->
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #0f172a; padding: 24px 32px; border-bottom: 3px solid #e11d48;">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div style="font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 1.2px; margin-bottom: 4px;">
|
||||||
|
Post-Fund Management Platform (PFM)
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 18px; font-weight: 700; color: #ffffff; letter-spacing: -0.3px;">
|
||||||
|
Portfolio Performance & Monitoring
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td align="right" style="vertical-align: middle;">
|
||||||
|
<span style="display: inline-block; background-color: rgba(225, 29, 72, 0.2); border: 1px solid rgba(225, 29, 72, 0.5); color: #fda4af; padding: 4px 10px; border-radius: 9999px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Action Required
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Alert Banner -->
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #fff1f2; border-bottom: 1px solid #ffe4e6; padding: 16px 32px;">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 28px; vertical-align: top; padding-right: 12px;">
|
||||||
|
<div style="width: 24px; height: 24px; background-color: #e11d48; border-radius: 50%; color: #ffffff; text-align: center; line-height: 24px; font-size: 13px; font-weight: bold;">
|
||||||
|
!
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style="font-size: 14px; font-weight: 700; color: #9f1239; margin-bottom: 2px;">
|
||||||
|
Overdue Milestone Notice: [TotalLateMilestones] Milestone(s) Exceeded Due Date
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px; color: #be123c;">
|
||||||
|
Automated daily sweep flagged scheduled deliverables requiring evidence submission or status updates.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Content Body -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 32px;">
|
||||||
|
|
||||||
|
<!-- Salutation -->
|
||||||
|
<p style="margin: 0 0 16px 0; font-size: 15px; color: #334155;">
|
||||||
|
Dear <strong>[OrganisationName]</strong> Team,
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Message -->
|
||||||
|
<p style="margin: 0 0 24px 0; font-size: 14px; color: #475569; line-height: 1.6;">
|
||||||
|
During our automated lifecycle monitoring sweep, our platform identified one or more contractual milestones for
|
||||||
|
<strong style="color: #0f172a;">[ProjectName]</strong> that have passed their agreed target completion deadlines without formal evidence approval.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Project Overview Box -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0; margin-bottom: 24px; overflow: hidden;">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" style="background-color: #f1f5f9; padding: 10px 16px; border-bottom: 1px solid #e2e8f0; font-size: 12px; font-weight: 700; color: #475569; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Project Summary
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b; width: 40%; border-bottom: 1px solid #f1f5f9;">Project Title</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; font-weight: 600; color: #0f172a; border-bottom: 1px solid #f1f5f9;">[ProjectName]</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b; border-bottom: 1px solid #f1f5f9;">Implementation Window</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #334155; border-bottom: 1px solid #f1f5f9;">[ProjectStartDate] — [ProjectEndDate]</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b; border-bottom: 1px solid #f1f5f9;">Overdue Deliverables</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; font-weight: 700; color: #e11d48; border-bottom: 1px solid #f1f5f9;">
|
||||||
|
<span style="background-color: #fee2e2; color: #991b1b; padding: 2px 8px; border-radius: 4px;">[TotalLateMilestones] Milestone(s)</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b;">Most Overdue Milestone</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #334155;">
|
||||||
|
<strong>[MostLateMilestoneName]</strong> <span style="color: #64748b; font-size: 12px;">(Due: [MostLateMilestoneDueDate])</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Detailed Milestones Table Header -->
|
||||||
|
<div style="font-size: 13px; font-weight: 700; color: #1e293b; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Milestone Breakdown
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Milestones Table -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse; margin-bottom: 24px; font-size: 13px; border: 1px solid #e2e8f0; border-radius: 6px; overflow: hidden;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color: #f8fafc; text-align: left; border-bottom: 2px solid #e2e8f0;">
|
||||||
|
<th style="padding: 10px 14px; font-size: 11px; font-weight: 700; color: #475569; text-transform: uppercase;">Milestone</th>
|
||||||
|
<th style="padding: 10px 14px; font-size: 11px; font-weight: 700; color: #475569; text-transform: uppercase;">Due Date</th>
|
||||||
|
<th style="padding: 10px 14px; font-size: 11px; font-weight: 700; color: #475569; text-transform: uppercase;">Delay</th>
|
||||||
|
<th style="padding: 10px 14px; font-size: 11px; font-weight: 700; color: #475569; text-transform: uppercase;">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
[LateMilestonesTable]
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Governance & SLA Callout -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f8fafc; border-left: 4px solid #0284c7; padding: 14px 18px; border-radius: 4px; margin-bottom: 28px;">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div style="font-size: 12px; font-weight: 700; color: #0369a1; text-transform: uppercase; margin-bottom: 4px;">
|
||||||
|
Disbursement & Governance Policy (BR-003 / BR-004)
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px; color: #475569; line-height: 1.5;">
|
||||||
|
Under PFM governance regulations, tranches and disbursements are strictly gated by verified milestone deliverables. Overdue milestones may trigger project risk escalations and freeze scheduled payment releases.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- CTA Button -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-bottom: 28px;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="[PortalUrl]" target="_blank" style="display: inline-block; background-color: #0f172a; color: #ffffff; text-decoration: none; font-size: 14px; font-weight: 600; padding: 12px 32px; border-radius: 6px; box-shadow: 0 2px 4px rgba(15, 23, 42, 0.15);">
|
||||||
|
Open PFM Portal & Submit Evidence →
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin: 0; font-size: 13px; color: #64748b; line-height: 1.5;">
|
||||||
|
If you have already submitted the evidence for these deliverables or have an active change request under review with your Programme Manager, you may disregard this notice.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #f8fafc; padding: 24px 32px; border-top: 1px solid #e2e8f0; font-size: 12px; color: #94a3b8; text-align: center;">
|
||||||
|
<p style="margin: 0 0 8px 0; color: #64748b; font-weight: 600;">
|
||||||
|
Post-Fund Management (PFM) Platform • Automated Lifecycle & SLA Daemon
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0 0 12px 0; line-height: 1.4;">
|
||||||
|
This is an automated contractual notification sent in accordance with your funding agreement SLA.<br>
|
||||||
|
Please do not reply directly to this email. For support, please contact your designated Programme Officer.
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0; font-size: 11px; color: #cbd5e1;">
|
||||||
|
© [Year] PostFundManagement Platform. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>Programme Oversight: Project Milestone Escalation Notice</title>
|
||||||
|
<!--[if mso]>
|
||||||
|
<style type="text/css">
|
||||||
|
body, table, td { font-family: Arial, Helvetica, sans-serif !important; }
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; background-color: #f8fafc; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; color: #1e293b; line-height: 1.6;">
|
||||||
|
|
||||||
|
<!-- Wrapper Table -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f8fafc; padding: 32px 16px;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<!-- Main Container Card (600px) -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px; background-color: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0;">
|
||||||
|
|
||||||
|
<!-- Header Bar -->
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #0f172a; padding: 24px 32px; border-bottom: 3px solid #d97706;">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div style="font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 1.2px; margin-bottom: 4px;">
|
||||||
|
PFM Internal Governance • FR-M07-009 Escalation
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 18px; font-weight: 700; color: #ffffff; letter-spacing: -0.3px;">
|
||||||
|
Programme Milestone Escalation
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td align="right" style="vertical-align: middle;">
|
||||||
|
<span style="display: inline-block; background-color: rgba(217, 119, 6, 0.2); border: 1px solid rgba(217, 119, 6, 0.6); color: #fde68a; padding: 4px 10px; border-radius: 9999px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Internal Notice
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Alert Banner -->
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #fffbeb; border-bottom: 1px solid #fef3c7; padding: 16px 32px;">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 28px; vertical-align: top; padding-right: 12px;">
|
||||||
|
<div style="width: 24px; height: 24px; background-color: #d97706; border-radius: 50%; color: #ffffff; text-align: center; line-height: 24px; font-size: 13px; font-weight: bold;">
|
||||||
|
!
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style="font-size: 14px; font-weight: 700; color: #92400e; margin-bottom: 2px;">
|
||||||
|
Milestone SLA Breach: [TotalLateMilestones] Deliverable(s) Past Due Date
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px; color: #b45309;">
|
||||||
|
Project under your programme governance has breached milestone delivery deadlines. Automated partner reminder was dispatched.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Content Body -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 32px;">
|
||||||
|
|
||||||
|
<!-- Salutation -->
|
||||||
|
<p style="margin: 0 0 16px 0; font-size: 15px; color: #334155;">
|
||||||
|
Dear <strong>[ProgrammeManagerName]</strong>,
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Context Narrative -->
|
||||||
|
<p style="margin: 0 0 24px 0; font-size: 14px; color: #475569; line-height: 1.6;">
|
||||||
|
In accordance with portfolio monitoring requirement <strong>FR-M07-009</strong>, this automated escalation notice alerts you that
|
||||||
|
<strong style="color: #0f172a;">[ProjectName]</strong> has overdue milestones requiring your oversight and possible intervention.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Governance Context Box -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0; margin-bottom: 24px; overflow: hidden;">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" style="background-color: #f1f5f9; padding: 10px 16px; border-bottom: 1px solid #e2e8f0; font-size: 12px; font-weight: 700; color: #475569; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Governance & Project Hierarchy
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b; width: 40%; border-bottom: 1px solid #f1f5f9;">Programme</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; font-weight: 600; color: #0f172a; border-bottom: 1px solid #f1f5f9;">[ProgrammeName]</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b; border-bottom: 1px solid #f1f5f9;">Portfolio</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #334155; border-bottom: 1px solid #f1f5f9;">[PortfolioName]</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b; border-bottom: 1px solid #f1f5f9;">Recipient Organisation</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; font-weight: 600; color: #0f172a; border-bottom: 1px solid #f1f5f9;">[OrganisationName]</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b; border-bottom: 1px solid #f1f5f9;">Project Title</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; font-weight: 600; color: #0f172a; border-bottom: 1px solid #f1f5f9;">[ProjectName]</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b; border-bottom: 1px solid #f1f5f9;">Project Duration</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #334155; border-bottom: 1px solid #f1f5f9;">[ProjectStartDate] — [ProjectEndDate]</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b;">Overdue Status</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px;">
|
||||||
|
<span style="background-color: #fee2e2; color: #991b1b; padding: 2px 8px; border-radius: 4px; font-weight: 700;">
|
||||||
|
[TotalLateMilestones] Milestone(s) Overdue
|
||||||
|
</span>
|
||||||
|
<span style="color: #64748b; font-size: 12px; margin-left: 6px;">(Primary: [MostLateMilestoneName], [MostLateMilestoneDaysOverdue] days late)</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Detailed Milestones Table Header -->
|
||||||
|
<div style="font-size: 13px; font-weight: 700; color: #1e293b; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Delinquent Deliverables Breakdown
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Milestones Table -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse; margin-bottom: 24px; font-size: 13px; border: 1px solid #e2e8f0; border-radius: 6px; overflow: hidden;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color: #f8fafc; text-align: left; border-bottom: 2px solid #e2e8f0;">
|
||||||
|
<th style="padding: 10px 14px; font-size: 11px; font-weight: 700; color: #475569; text-transform: uppercase;">Milestone</th>
|
||||||
|
<th style="padding: 10px 14px; font-size: 11px; font-weight: 700; color: #475569; text-transform: uppercase;">Due Date</th>
|
||||||
|
<th style="padding: 10px 14px; font-size: 11px; font-weight: 700; color: #475569; text-transform: uppercase;">Delay</th>
|
||||||
|
<th style="padding: 10px 14px; font-size: 11px; font-weight: 700; color: #475569; text-transform: uppercase;">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
[LateMilestonesTable]
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Recommended Action Guidance -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f8fafc; border-left: 4px solid #d97706; padding: 14px 18px; border-radius: 4px; margin-bottom: 28px;">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div style="font-size: 12px; font-weight: 700; color: #92400e; text-transform: uppercase; margin-bottom: 4px;">
|
||||||
|
Recommended Manager Actions
|
||||||
|
</div>
|
||||||
|
<ul style="margin: 0; padding-left: 18px; font-size: 12px; color: #475569; line-height: 1.6;">
|
||||||
|
<li>Verify whether unreviewed evidence submissions are awaiting your sign-off in the Admin Console.</li>
|
||||||
|
<li>Engage recipient organisation leadership regarding timeline remediation or change requests (M15).</li>
|
||||||
|
<li>Ensure disbursements linked to gated milestones remain withheld per rule BR-003.</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- CTA Button -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-bottom: 24px;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="[ConsoleUrl]" target="_blank" style="display: inline-block; background-color: #0f172a; color: #ffffff; text-decoration: none; font-size: 14px; font-weight: 600; padding: 12px 32px; border-radius: 6px; box-shadow: 0 2px 4px rgba(15, 23, 42, 0.15);">
|
||||||
|
Open Programme in Console →
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #f8fafc; padding: 24px 32px; border-top: 1px solid #e2e8f0; font-size: 12px; color: #94a3b8; text-align: center;">
|
||||||
|
<p style="margin: 0 0 8px 0; color: #64748b; font-weight: 600;">
|
||||||
|
Post-Fund Management (PFM) Platform • Automated Governance & Monitoring Daemon
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0 0 12px 0; line-height: 1.4;">
|
||||||
|
You received this notification because you are designated as the Programme Owner/Manager for [ProgrammeName].<br>
|
||||||
|
Automated escalation according to SRS v2.0 requirement FR-M07-009.
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0; font-size: 11px; color: #cbd5e1;">
|
||||||
|
© [Year] PostFundManagement Platform. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>Weekly Governance Audit: Project Non-Compliance Notice</title>
|
||||||
|
<!--[if mso]>
|
||||||
|
<style type="text/css">
|
||||||
|
body, table, td { font-family: Arial, Helvetica, sans-serif !important; }
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; background-color: #f1f5f9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; color: #1e293b; line-height: 1.6;">
|
||||||
|
|
||||||
|
<!-- Wrapper Table -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f1f5f9; padding: 32px 16px;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<!-- Main Container Card (600px) -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px; background-color: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0;">
|
||||||
|
|
||||||
|
<!-- Header Bar -->
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #0f172a; padding: 24px 32px; border-bottom: 3px solid #be123c;">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div style="font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 1.2px; margin-bottom: 4px;">
|
||||||
|
Post-Fund Management Platform (PFM) • M10 Compliance
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 18px; font-weight: 700; color: #ffffff; letter-spacing: -0.3px;">
|
||||||
|
Weekly Project Compliance Audit
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td align="right" style="vertical-align: middle;">
|
||||||
|
<span style="display: inline-block; background-color: rgba(190, 18, 60, 0.2); border: 1px solid rgba(190, 18, 60, 0.6); color: #fda4af; padding: 4px 10px; border-radius: 9999px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Non-Compliant
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Alert Banner -->
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #fff1f2; border-bottom: 1px solid #ffe4e6; padding: 16px 32px;">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 28px; vertical-align: top; padding-right: 12px;">
|
||||||
|
<div style="width: 24px; height: 24px; background-color: #be123c; border-radius: 50%; color: #ffffff; text-align: center; line-height: 24px; font-size: 13px; font-weight: bold;">
|
||||||
|
!
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style="font-size: 14px; font-weight: 700; color: #9f1239; margin-bottom: 2px;">
|
||||||
|
Action Required: Project Flagged as Non-Compliant ([ComplianceScore] Met)
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px; color: #be123c;">
|
||||||
|
Weekly automated governance audit identified unmet statutory obligations and/or overdue milestone deliverables.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Content Body -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 32px;">
|
||||||
|
|
||||||
|
<!-- Salutation -->
|
||||||
|
<p style="margin: 0 0 16px 0; font-size: 15px; color: #334155;">
|
||||||
|
Dear <strong>[OrganisationName]</strong> Team,
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Narrative -->
|
||||||
|
<p style="margin: 0 0 24px 0; font-size: 14px; color: #475569; line-height: 1.6;">
|
||||||
|
During our weekly automated compliance audit, <strong style="color: #0f172a;">[ProjectName]</strong> (under programme <strong>[ProgrammeName]</strong>) did not satisfy the mandatory statutory and contractual compliance threshold. In accordance with system governance policies, immediate remediation is required.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Project Summary Box -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0; margin-bottom: 24px; overflow: hidden;">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" style="background-color: #f1f5f9; padding: 10px 16px; border-bottom: 1px solid #e2e8f0; font-size: 12px; font-weight: 700; color: #475569; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Audit Findings Overview
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b; width: 40%; border-bottom: 1px solid #f1f5f9;">Project Title</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; font-weight: 600; color: #0f172a; border-bottom: 1px solid #f1f5f9;">[ProjectName]</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b; border-bottom: 1px solid #f1f5f9;">Programme / Portfolio</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #334155; border-bottom: 1px solid #f1f5f9;">[ProgrammeName] • [PortfolioName]</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b; border-bottom: 1px solid #f1f5f9;">Implementation Window</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #334155; border-bottom: 1px solid #f1f5f9;">[ProjectStartDate] — [ProjectEndDate]</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b; border-bottom: 1px solid #f1f5f9;">Compliance Verdict</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; border-bottom: 1px solid #f1f5f9;">
|
||||||
|
<span style="background-color: #fee2e2; color: #991b1b; padding: 2px 8px; border-radius: 4px; font-weight: 700;">
|
||||||
|
Non-Compliant — Score: [ComplianceScore]
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b; border-bottom: 1px solid #f1f5f9;">Unmet Compliance Items</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; font-weight: 600; color: #be123c; border-bottom: 1px solid #f1f5f9;">
|
||||||
|
[UnmetComplianceCount] Item(s) Pending Resolution
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; color: #64748b;">Overdue Milestones</td>
|
||||||
|
<td style="padding: 12px 16px; font-size: 13px; font-weight: 600; color: #be123c;">
|
||||||
|
[UnmetMilestonesCount] Milestone(s) Overdue
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Items Table Header -->
|
||||||
|
<div style="font-size: 13px; font-weight: 700; color: #1e293b; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
Outstanding Compliance & Deliverable Items
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Non-Compliance Items Table -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse; margin-bottom: 24px; font-size: 13px; border: 1px solid #e2e8f0; border-radius: 6px; overflow: hidden;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color: #f8fafc; text-align: left; border-bottom: 2px solid #e2e8f0;">
|
||||||
|
<th style="padding: 10px 14px; font-size: 11px; font-weight: 700; color: #475569; text-transform: uppercase;">Category</th>
|
||||||
|
<th style="padding: 10px 14px; font-size: 11px; font-weight: 700; color: #475569; text-transform: uppercase;">Item / Obligation</th>
|
||||||
|
<th style="padding: 10px 14px; font-size: 11px; font-weight: 700; color: #475569; text-transform: uppercase;">Details / Due Date</th>
|
||||||
|
<th style="padding: 10px 14px; font-size: 11px; font-weight: 700; color: #475569; text-transform: uppercase;">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
[NonComplianceTable]
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Governance & Disbursement Hold Callout -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #fff1f2; border-left: 4px solid #be123c; padding: 14px 18px; border-radius: 4px; margin-bottom: 28px;">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div style="font-size: 12px; font-weight: 700; color: #9f1239; text-transform: uppercase; margin-bottom: 4px;">
|
||||||
|
Disbursement Hold Notice (BR-003 / AT-08)
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px; color: #475569; line-height: 1.5;">
|
||||||
|
Under cardinal rule BR-003, all pending tranche disbursements and drawdown requests are frozen while a project carries non-compliant obligations. Please upload the required compliance verification evidence through the Partner Portal to restore good standing.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- CTA Button -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-bottom: 28px;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="[PortalUrl]" target="_blank" style="display: inline-block; background-color: #0f172a; color: #ffffff; text-decoration: none; font-size: 14px; font-weight: 600; padding: 12px 32px; border-radius: 6px; box-shadow: 0 2px 4px rgba(15, 23, 42, 0.15);">
|
||||||
|
Open Partner Portal & Submit Compliance Evidence →
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin: 0; font-size: 13px; color: #64748b; line-height: 1.5;">
|
||||||
|
Once evidence is submitted, your designated Programme Officer will review and verify the documentation to restore your project's 100% compliant status.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #f8fafc; padding: 24px 32px; border-top: 1px solid #e2e8f0; font-size: 12px; color: #94a3b8; text-align: center;">
|
||||||
|
<p style="margin: 0 0 8px 0; color: #64748b; font-weight: 600;">
|
||||||
|
Post-Fund Management (PFM) Platform • Weekly Compliance Audit Daemon
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0 0 12px 0; line-height: 1.4;">
|
||||||
|
Automated statutory oversight notification generated in accordance with SRS v2.0 Module M10.<br>
|
||||||
|
Please do not reply directly to this email. For assistance, contact your Programme Officer.
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0; font-size: 11px; color: #cbd5e1;">
|
||||||
|
© [Year] PostFundManagement Platform. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ public class JobsWorker(IJobOrchestrator jobOrchestrator, IFeatureManager featur
|
|||||||
await jobOrchestrator.ScheduleAsync(ProcessEmailEvent.Create(), "0 */5 * * * ?", stoppingToken);
|
await jobOrchestrator.ScheduleAsync(ProcessEmailEvent.Create(), "0 */5 * * * ?", stoppingToken);
|
||||||
|
|
||||||
if (await featureManager.IsEnabledAsync(FeatureLateProjectReminder))
|
if (await featureManager.IsEnabledAsync(FeatureLateProjectReminder))
|
||||||
await jobOrchestrator.ScheduleAsync(LateProjectEvent.Create(), "", stoppingToken);
|
await jobOrchestrator.ScheduleAsync(LateProjectEvent.Create(), "0 30 6 * * ?", stoppingToken);
|
||||||
|
|
||||||
// TODO: Add job to check for non-compliant projects once a week
|
if (await featureManager.IsEnabledAsync(FeatureNonCompliantProjectCheck))
|
||||||
|
await jobOrchestrator.ScheduleAsync(NonCompliantProjectEvent.Create(), "0 0 7 ? * MON", stoppingToken);
|
||||||
|
|
||||||
logger.LogInformation("Recurring jobs have been registered or updated.");
|
logger.LogInformation("Recurring jobs have been registered or updated.");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user