feat(job-search): read providers from DB and suppress link when none enabled
JobTokenService.CreateTokenAsync queries cvSearch.JobProviders for any enabled row; returns null (no token created) when the table is empty or all providers are disabled. TriggerStartAsync snapshots enabled providers from DB at session-start time, preserving the existing snapshot contract. CvMatcherController guards link-building on a non-null TokenId so the "Start a job search" CTA is omitted from match emails when no providers are configured. JobSearchSettings.Providers list removed — provider config now lives exclusively in the DB. CvSearchJobTask.GetProviders falls back to an empty list with a warning (snapshot should always be populated from DB). Closes #35 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,9 @@ namespace Api.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Creates and validates one-time job search tokens, and creates the corresponding search sessions.
|
||||
/// Provider configuration is read from <c>cvSearch.JobProviders</c> at session-creation time and
|
||||
/// snapshotted into <c>JobSearchSessionEntity.ProviderConfigJson</c> so subsequent config changes
|
||||
/// do not affect already-queued sessions.
|
||||
/// </summary>
|
||||
public sealed class JobTokenService : IJobTokenService
|
||||
{
|
||||
@@ -34,8 +37,15 @@ public sealed class JobTokenService : IJobTokenService
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> CreateTokenAsync(string cvDocumentId, string email, string language, CancellationToken ct)
|
||||
public async Task<string?> CreateTokenAsync(string cvDocumentId, string email, string language, CancellationToken ct)
|
||||
{
|
||||
var hasEnabledProviders = await _db.JobProviders.AnyAsync(p => p.Enabled, ct);
|
||||
if (!hasEnabledProviders)
|
||||
{
|
||||
_logger.LogDebug("Job search token skipped — no enabled providers in cvSearch.JobProviders");
|
||||
return null;
|
||||
}
|
||||
|
||||
var token = new JobSearchTokenEntity
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
@@ -67,8 +77,13 @@ public sealed class JobTokenService : IJobTokenService
|
||||
var cv = await _rag.GetDocumentAsync(token.CvDocumentId, ct);
|
||||
var keywords = cv is not null ? ExtractKeywords(cv.Text) : string.Empty;
|
||||
|
||||
var enabledProviders = await _db.JobProviders
|
||||
.Where(p => p.Enabled)
|
||||
.OrderBy(p => p.DisplayOrder)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var providerConfigJson = JsonSerializer.Serialize(
|
||||
_settings.Providers.Where(p => p.Enabled).ToList(),
|
||||
enabledProviders.Select(ToConfig).ToList(),
|
||||
new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
var session = new JobSearchSessionEntity
|
||||
@@ -86,11 +101,41 @@ public sealed class JobTokenService : IJobTokenService
|
||||
|
||||
_db.JobSearchSessions.Add(session);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
_logger.LogInformation("Job search session created. SessionId={SessionId}, Keywords={Keywords}", session.Id, keywords);
|
||||
_logger.LogInformation(
|
||||
"Job search session created. SessionId={SessionId}, Keywords={Keywords}, Providers={Providers}",
|
||||
session.Id, keywords, string.Join(", ", enabledProviders.Select(p => p.Name)));
|
||||
|
||||
return StartJobSearchStatus.Started;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a <see cref="JobProviderEntity"/> to the <see cref="JobProviderConfig"/> DTO used by
|
||||
/// <c>cv-search-job</c>. The <c>InitialKeywords</c> list is stored as a JSON array in the entity.
|
||||
/// </summary>
|
||||
private static JobProviderConfig ToConfig(JobProviderEntity entity)
|
||||
{
|
||||
List<string> keywords;
|
||||
try
|
||||
{
|
||||
keywords = JsonSerializer.Deserialize<List<string>>(entity.InitialKeywordsJson,
|
||||
new JsonSerializerOptions(JsonSerializerDefaults.Web)) ?? [];
|
||||
}
|
||||
catch
|
||||
{
|
||||
keywords = [];
|
||||
}
|
||||
|
||||
return new JobProviderConfig
|
||||
{
|
||||
Name = entity.Name,
|
||||
Enabled = entity.Enabled,
|
||||
SearchUrlTemplate = entity.SearchUrlTemplate,
|
||||
JobLinkContains = entity.JobLinkContains,
|
||||
InitialKeywords = keywords,
|
||||
MaxResults = entity.MaxResults
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts up to 10 meaningful keywords from the CV text using simple heuristics (no LLM).
|
||||
/// Takes the first 5 usable lines, splits them into words, strips punctuation, and deduplicates.
|
||||
|
||||
Reference in New Issue
Block a user