chore(M001/S01): auto-commit after complete-slice
This commit is contained in:
@@ -28,6 +28,9 @@ public sealed class GmailController : ControllerBase
|
||||
public sealed record GmailImportMessageResultDto(int Imported, int Skipped, string MessageId, string? ThreadId, Correspondence? Message);
|
||||
public sealed record ImportGmailMessageRequest(int JobApplicationId, string MessageId);
|
||||
public sealed record ImportGmailThreadRequest(int JobApplicationId, string ThreadId, string[] MessageIds);
|
||||
public sealed record RefreshLinkedThreadsRequest(int JobApplicationId);
|
||||
public sealed record GmailThreadRefreshThreadDto(string ThreadId, int Imported, int Skipped, int TotalMessages, string Status, DateTimeOffset? LatestMessageDate);
|
||||
public sealed record GmailThreadRefreshResultDto(int JobApplicationId, int ThreadsChecked, int Imported, int Skipped, bool HasLinkedThreads, DateTimeOffset RefreshedAt, IReadOnlyList<GmailThreadRefreshThreadDto> Threads);
|
||||
public sealed record GmailJobMatchReasonDto(string Label, string Value, int Points);
|
||||
public sealed record GmailJobMatchedMessageDto(
|
||||
string Id,
|
||||
@@ -301,6 +304,88 @@ public sealed class GmailController : ControllerBase
|
||||
return Ok(new GmailImportResultDto(imported, skipped, request.ThreadId));
|
||||
}
|
||||
|
||||
[HttpPost("refresh-linked-threads")]
|
||||
public async Task<ActionResult<GmailThreadRefreshResultDto>> RefreshLinkedThreads([FromBody] RefreshLinkedThreadsRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (request.JobApplicationId <= 0) return BadRequest("Valid jobApplicationId is required.");
|
||||
|
||||
var ownerUserId = GetRequiredOwnerUserId();
|
||||
var job = await _db.JobApplications
|
||||
.Where(x => x.OwnerUserId == ownerUserId)
|
||||
.Include(x => x.Company)
|
||||
.Include(x => x.Messages)
|
||||
.FirstOrDefaultAsync(x => x.Id == request.JobApplicationId, cancellationToken);
|
||||
if (job is null) return NotFound("Job application not found.");
|
||||
|
||||
var connection = await _gmail.GetConnectionAsync(ownerUserId, cancellationToken);
|
||||
if (connection is null) return Conflict("Connect Gmail before refreshing linked threads.");
|
||||
|
||||
var linkedThreadIds = job.Messages
|
||||
.Where(message => !string.IsNullOrWhiteSpace(message.ExternalThreadId))
|
||||
.Select(message => message.ExternalThreadId!.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
if (linkedThreadIds.Count == 0)
|
||||
{
|
||||
return Ok(new GmailThreadRefreshResultDto(job.Id, 0, 0, 0, false, DateTimeOffset.UtcNow, Array.Empty<GmailThreadRefreshThreadDto>()));
|
||||
}
|
||||
|
||||
var existingMessageIds = job.Messages
|
||||
.Where(message => !string.IsNullOrWhiteSpace(message.ExternalMessageId))
|
||||
.Select(message => message.ExternalMessageId!)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
var refreshedThreads = new List<GmailThreadRefreshThreadDto>(linkedThreadIds.Count);
|
||||
var imported = 0;
|
||||
var skipped = 0;
|
||||
|
||||
foreach (var threadId in linkedThreadIds)
|
||||
{
|
||||
var threadMessages = await _gmail.ListThreadMessagesAsync(ownerUserId, threadId, cancellationToken);
|
||||
var distinctThreadMessages = threadMessages
|
||||
.Where(message => !string.IsNullOrWhiteSpace(message.Id))
|
||||
.GroupBy(message => message.Id, StringComparer.Ordinal)
|
||||
.Select(group => group.First())
|
||||
.OrderBy(message => message.Date ?? DateTimeOffset.MinValue)
|
||||
.ToList();
|
||||
|
||||
var threadImported = 0;
|
||||
var threadSkipped = 0;
|
||||
foreach (var message in distinctThreadMessages)
|
||||
{
|
||||
if (existingMessageIds.Contains(message.Id))
|
||||
{
|
||||
threadSkipped++;
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var created = await ImportSingleMessageAsync(ownerUserId, job, message.Id, cancellationToken);
|
||||
existingMessageIds.Add(created.ExternalMessageId ?? message.Id);
|
||||
threadImported++;
|
||||
imported++;
|
||||
}
|
||||
|
||||
var latestMessageDate = distinctThreadMessages
|
||||
.Select(message => message.Date)
|
||||
.OrderByDescending(message => message ?? DateTimeOffset.MinValue)
|
||||
.FirstOrDefault();
|
||||
var status = threadImported > 0 ? "imported-new-messages" : "already-current";
|
||||
refreshedThreads.Add(new GmailThreadRefreshThreadDto(threadId, threadImported, threadSkipped, distinctThreadMessages.Count, status, latestMessageDate));
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
return Ok(new GmailThreadRefreshResultDto(job.Id, refreshedThreads.Count, imported, skipped, true, DateTimeOffset.UtcNow, refreshedThreads));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Correspondence> ImportSingleMessageAsync(string ownerUserId, JobApplication job, string messageId, CancellationToken cancellationToken)
|
||||
{
|
||||
var detail = await _gmail.GetMessageAsync(ownerUserId, messageId, cancellationToken);
|
||||
|
||||
Reference in New Issue
Block a user