First Commit
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
|
||||
namespace JobTrackerApi.Services
|
||||
{
|
||||
public sealed record FollowUpDecision(bool NeedsFollowUp, string? Reason, bool ShouldGhost);
|
||||
|
||||
public static class RulesEngine
|
||||
{
|
||||
public static async Task<RuleSettings> GetSettings(JobTrackerContext db, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(db.CurrentUserId))
|
||||
{
|
||||
var u = await db.UserRuleSettings
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.OwnerUserId == db.CurrentUserId, cancellationToken);
|
||||
|
||||
if (u is not null)
|
||||
{
|
||||
return new RuleSettings
|
||||
{
|
||||
Id = 1,
|
||||
AppliedFollowUpDays = u.AppliedFollowUpDays,
|
||||
AppliedGhostDays = u.AppliedGhostDays,
|
||||
OfferFollowUpDays = u.OfferFollowUpDays,
|
||||
OfferGhostDays = u.OfferGhostDays,
|
||||
FeedbackFollowUpDays = u.FeedbackFollowUpDays,
|
||||
FeedbackGhostDays = u.FeedbackGhostDays,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var s = await db.RuleSettings.AsNoTracking().FirstOrDefaultAsync(x => x.Id == 1, cancellationToken);
|
||||
return s ?? new RuleSettings { Id = 1 };
|
||||
}
|
||||
|
||||
public static FollowUpDecision Evaluate(
|
||||
RuleSettings s,
|
||||
JobApplication job,
|
||||
DateTime now,
|
||||
DateTime? lastMessageAt
|
||||
)
|
||||
{
|
||||
if (job.IsDeleted) return new FollowUpDecision(false, null, false);
|
||||
|
||||
var status = job.Status ?? "Applied";
|
||||
if (status == "Interviewing") status = "Interview";
|
||||
|
||||
// Last activity: any explicit follow-up date, response date, feedback request, or correspondence message.
|
||||
var last = Max(
|
||||
job.DateApplied,
|
||||
job.ResponseDate,
|
||||
job.FollowUpAt,
|
||||
job.FeedbackRequestedAt,
|
||||
lastMessageAt
|
||||
);
|
||||
|
||||
var daysSinceLast = (now - last).TotalDays;
|
||||
|
||||
// Applied: if no response and enough time passed since applied.
|
||||
if (string.Equals(status, "Applied", StringComparison.OrdinalIgnoreCase) && !job.ResponseReceived)
|
||||
{
|
||||
var daysSinceApplied = (now - job.DateApplied).TotalDays;
|
||||
if (daysSinceApplied >= s.AppliedFollowUpDays)
|
||||
return new FollowUpDecision(true, $"No reply after {s.AppliedFollowUpDays}d", daysSinceApplied >= s.AppliedGhostDays);
|
||||
return new FollowUpDecision(false, null, daysSinceApplied >= s.AppliedGhostDays);
|
||||
}
|
||||
|
||||
// Offer/accepted waiting on next step
|
||||
if (string.Equals(status, "Offer", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (daysSinceLast >= s.OfferFollowUpDays)
|
||||
return new FollowUpDecision(true, $"Stalled after {s.OfferFollowUpDays}d", daysSinceLast >= s.OfferGhostDays);
|
||||
return new FollowUpDecision(false, null, daysSinceLast >= s.OfferGhostDays);
|
||||
}
|
||||
|
||||
// Rejected but feedback requested
|
||||
if (string.Equals(status, "Rejected", StringComparison.OrdinalIgnoreCase) && job.FeedbackRequestedAt is not null)
|
||||
{
|
||||
var daysSinceReq = (now - job.FeedbackRequestedAt.Value).TotalDays;
|
||||
if (daysSinceReq >= s.FeedbackFollowUpDays)
|
||||
return new FollowUpDecision(true, $"Feedback requested {s.FeedbackFollowUpDays}d ago", daysSinceReq >= s.FeedbackGhostDays);
|
||||
return new FollowUpDecision(false, null, daysSinceReq >= s.FeedbackGhostDays);
|
||||
}
|
||||
|
||||
// Waiting status: treat as follow-up based on applied follow-up days.
|
||||
if (string.Equals(status, "Waiting", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (daysSinceLast >= s.AppliedFollowUpDays)
|
||||
return new FollowUpDecision(true, $"Waiting {s.AppliedFollowUpDays}d", daysSinceLast >= s.AppliedGhostDays);
|
||||
return new FollowUpDecision(false, null, daysSinceLast >= s.AppliedGhostDays);
|
||||
}
|
||||
|
||||
// Default: no follow-up rule. Do not auto-ghost other statuses.
|
||||
return new FollowUpDecision(false, null, false);
|
||||
}
|
||||
|
||||
public static DateTime Max(DateTime a, params DateTime?[] rest)
|
||||
{
|
||||
var m = a;
|
||||
foreach (var r in rest)
|
||||
{
|
||||
if (r is not null && r.Value > m) m = r.Value;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user