feat(email): add Correspondence.Provider discriminator #13
@@ -0,0 +1,35 @@
|
|||||||
|
using JobTrackerApi.Controllers;
|
||||||
|
using JobTrackerApi.Data;
|
||||||
|
using JobTrackerApi.Models;
|
||||||
|
using JobTrackerApi.Tests.TestSupport;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace JobTrackerApi.Tests;
|
||||||
|
|
||||||
|
public sealed class CorrespondenceControllerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Create_tags_manually_entered_correspondence_with_manual_provider()
|
||||||
|
{
|
||||||
|
await using var db = TestHostFactory.CreateInMemoryDb();
|
||||||
|
var company = new Company { Name = "Acme", OwnerUserId = "user-1" };
|
||||||
|
db.Companies.Add(company);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var job = new JobApplication { JobTitle = "Backend Developer", CompanyId = company.Id, OwnerUserId = "user-1" };
|
||||||
|
db.JobApplications.Add(job);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var controller = new CorrespondenceController(db);
|
||||||
|
var request = new CorrespondenceController.CreateCorrespondenceRequestV2(
|
||||||
|
job.Id, "Me", "Called to follow up.", "Follow-up call", "Call", null, "outbound", null, null, null, null, null, null);
|
||||||
|
|
||||||
|
var result = await controller.Create(request, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.IsType<Correspondence>(((CreatedAtActionResult)result.Result!).Value);
|
||||||
|
var stored = await db.Correspondences.SingleAsync();
|
||||||
|
Assert.Equal("manual", stored.Provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -288,6 +288,7 @@ public sealed class GmailControllerTests
|
|||||||
|
|
||||||
var storedMessages = await db.Correspondences.Where(message => message.JobApplicationId == job.Id).ToListAsync();
|
var storedMessages = await db.Correspondences.Where(message => message.JobApplicationId == job.Id).ToListAsync();
|
||||||
Assert.Single(storedMessages);
|
Assert.Single(storedMessages);
|
||||||
|
Assert.Equal("gmail", storedMessages[0].Provider);
|
||||||
gmail.Verify(service => service.GetMessageAsync("user-1", "msg-1", It.IsAny<CancellationToken>()), Times.Once);
|
gmail.Verify(service => service.GetMessageAsync("user-1", "msg-1", It.IsAny<CancellationToken>()), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ namespace JobTrackerApi.Controllers
|
|||||||
ExternalTo = string.IsNullOrWhiteSpace(request.ExternalTo) ? null : request.ExternalTo.Trim(),
|
ExternalTo = string.IsNullOrWhiteSpace(request.ExternalTo) ? null : request.ExternalTo.Trim(),
|
||||||
ExternalLabelsJson = string.IsNullOrWhiteSpace(request.ExternalLabelsJson) ? null : request.ExternalLabelsJson.Trim(),
|
ExternalLabelsJson = string.IsNullOrWhiteSpace(request.ExternalLabelsJson) ? null : request.ExternalLabelsJson.Trim(),
|
||||||
AttachmentMetadataJson = string.IsNullOrWhiteSpace(request.AttachmentMetadataJson) ? null : request.AttachmentMetadataJson.Trim(),
|
AttachmentMetadataJson = string.IsNullOrWhiteSpace(request.AttachmentMetadataJson) ? null : request.AttachmentMetadataJson.Trim(),
|
||||||
|
Provider = "manual",
|
||||||
Content = request.Content,
|
Content = request.Content,
|
||||||
Date = request.Date ?? DateTime.Now,
|
Date = request.Date ?? DateTime.Now,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -977,6 +977,7 @@ public sealed class GmailController : ControllerBase
|
|||||||
GmailAttachmentId = attachment.ExternalAttachmentId,
|
GmailAttachmentId = attachment.ExternalAttachmentId,
|
||||||
Inline = attachment.Inline,
|
Inline = attachment.Inline,
|
||||||
})),
|
})),
|
||||||
|
Provider = "gmail",
|
||||||
Content = string.IsNullOrWhiteSpace(detail.BodyText) ? detail.Snippet : detail.BodyText,
|
Content = string.IsNullOrWhiteSpace(detail.BodyText) ? detail.Snippet : detail.BodyText,
|
||||||
Date = messageDate,
|
Date = messageDate,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -555,6 +555,12 @@ public static class StartupInitializationExtensions
|
|||||||
EnsureColumn(conn, "Correspondences", "Direction", "ALTER TABLE Correspondences ADD COLUMN Direction TEXT NULL;");
|
EnsureColumn(conn, "Correspondences", "Direction", "ALTER TABLE Correspondences ADD COLUMN Direction TEXT NULL;");
|
||||||
EnsureColumn(conn, "Correspondences", "ExternalLabelsJson", "ALTER TABLE Correspondences ADD COLUMN ExternalLabelsJson TEXT NULL;");
|
EnsureColumn(conn, "Correspondences", "ExternalLabelsJson", "ALTER TABLE Correspondences ADD COLUMN ExternalLabelsJson TEXT NULL;");
|
||||||
EnsureColumn(conn, "Correspondences", "AttachmentMetadataJson", "ALTER TABLE Correspondences ADD COLUMN AttachmentMetadataJson TEXT NULL;");
|
EnsureColumn(conn, "Correspondences", "AttachmentMetadataJson", "ALTER TABLE Correspondences ADD COLUMN AttachmentMetadataJson TEXT NULL;");
|
||||||
|
EnsureColumn(conn, "Correspondences", "Provider", "ALTER TABLE Correspondences ADD COLUMN Provider TEXT NULL;");
|
||||||
|
// Backfill: historically the only import source was Gmail (rows with an
|
||||||
|
// ExternalThreadId); everything else was hand-entered. Idempotent — only touches
|
||||||
|
// rows the app hasn't tagged yet.
|
||||||
|
Exec(conn, "UPDATE Correspondences SET Provider = 'gmail' WHERE Provider IS NULL AND ExternalThreadId IS NOT NULL;");
|
||||||
|
Exec(conn, "UPDATE Correspondences SET Provider = 'manual' WHERE Provider IS NULL;");
|
||||||
EnsureColumn(conn, "Attachments", "Purpose", "ALTER TABLE Attachments ADD COLUMN Purpose TEXT NULL;");
|
EnsureColumn(conn, "Attachments", "Purpose", "ALTER TABLE Attachments ADD COLUMN Purpose TEXT NULL;");
|
||||||
EnsureColumn(conn, "Attachments", "UseForAi", "ALTER TABLE Attachments ADD COLUMN UseForAi INTEGER NOT NULL DEFAULT 1;");
|
EnsureColumn(conn, "Attachments", "UseForAi", "ALTER TABLE Attachments ADD COLUMN UseForAi INTEGER NOT NULL DEFAULT 1;");
|
||||||
|
|
||||||
@@ -709,6 +715,17 @@ public static class StartupInitializationExtensions
|
|||||||
EnsureMySqlColumn(conn, "Correspondences", "Direction", "ALTER TABLE `Correspondences` ADD COLUMN `Direction` varchar(100) NULL;");
|
EnsureMySqlColumn(conn, "Correspondences", "Direction", "ALTER TABLE `Correspondences` ADD COLUMN `Direction` varchar(100) NULL;");
|
||||||
EnsureMySqlColumn(conn, "Correspondences", "ExternalLabelsJson", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalLabelsJson` longtext NULL;");
|
EnsureMySqlColumn(conn, "Correspondences", "ExternalLabelsJson", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalLabelsJson` longtext NULL;");
|
||||||
EnsureMySqlColumn(conn, "Correspondences", "AttachmentMetadataJson", "ALTER TABLE `Correspondences` ADD COLUMN `AttachmentMetadataJson` longtext NULL;");
|
EnsureMySqlColumn(conn, "Correspondences", "AttachmentMetadataJson", "ALTER TABLE `Correspondences` ADD COLUMN `AttachmentMetadataJson` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "Correspondences", "Provider", "ALTER TABLE `Correspondences` ADD COLUMN `Provider` varchar(50) NULL;");
|
||||||
|
using (var backfillGmail = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
backfillGmail.CommandText = "UPDATE `Correspondences` SET `Provider` = 'gmail' WHERE `Provider` IS NULL AND `ExternalThreadId` IS NOT NULL;";
|
||||||
|
backfillGmail.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
using (var backfillManual = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
backfillManual.CommandText = "UPDATE `Correspondences` SET `Provider` = 'manual' WHERE `Provider` IS NULL;";
|
||||||
|
backfillManual.ExecuteNonQuery();
|
||||||
|
}
|
||||||
EnsureMySqlColumn(conn, "Attachments", "Purpose", "ALTER TABLE `Attachments` ADD COLUMN `Purpose` varchar(100) NULL;");
|
EnsureMySqlColumn(conn, "Attachments", "Purpose", "ALTER TABLE `Attachments` ADD COLUMN `Purpose` varchar(100) NULL;");
|
||||||
EnsureMySqlColumn(conn, "Attachments", "UseForAi", "ALTER TABLE `Attachments` ADD COLUMN `UseForAi` tinyint(1) NOT NULL DEFAULT 1;");
|
EnsureMySqlColumn(conn, "Attachments", "UseForAi", "ALTER TABLE `Attachments` ADD COLUMN `UseForAi` tinyint(1) NOT NULL DEFAULT 1;");
|
||||||
EnsureMySqlColumn(conn, "AspNetUsers", "ProfileCvText", "ALTER TABLE `AspNetUsers` ADD COLUMN `ProfileCvText` longtext NULL;");
|
EnsureMySqlColumn(conn, "AspNetUsers", "ProfileCvText", "ALTER TABLE `AspNetUsers` ADD COLUMN `ProfileCvText` longtext NULL;");
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ namespace JobTrackerApi.Models
|
|||||||
public string? ExternalTo { get; set; }
|
public string? ExternalTo { get; set; }
|
||||||
public string? ExternalLabelsJson { get; set; }
|
public string? ExternalLabelsJson { get; set; }
|
||||||
public string? AttachmentMetadataJson { get; set; }
|
public string? AttachmentMetadataJson { get; set; }
|
||||||
|
// Provider discriminator: "gmail" | "microsoft" | "imap" | "manual". Set at the write
|
||||||
|
// site (import controller or the manual-entry endpoint), not inferred from other fields,
|
||||||
|
// so it stays correct even for hand-entered rows that happen to carry external-looking data.
|
||||||
|
public string? Provider { get; set; }
|
||||||
public string Content { get; set; } = "";
|
public string Content { get; set; } = "";
|
||||||
public DateTime Date { get; set; } = DateTime.Now;
|
public DateTime Date { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user