fix(email): retire legacy SMTP follow-up
Keep follow-up draft generation but remove the direct application SMTP delivery boundary. Route users to the provider-aware Job email flow and return 410 for legacy API callers.
This commit is contained in:
@@ -178,7 +178,6 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
|
||||
|
||||
const [followUpDraft, setFollowUpDraft] = useState<FollowUpDraft | null>(null);
|
||||
const [loadingDraft, setLoadingDraft] = useState(false);
|
||||
const [sendingDraft, setSendingDraft] = useState(false);
|
||||
const [refreshingAi, setRefreshingAi] = useState(false);
|
||||
const [candidateFit, setCandidateFit] = useState<CandidateFit | null>(null);
|
||||
const [matchScore, setMatchScore] = useState<MatchScore | null>(null);
|
||||
@@ -1265,29 +1264,15 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
|
||||
<Box sx={{ p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "warning.main", backgroundColor: "background.default" }}>
|
||||
<Typography variant="overline">Manual send boundary</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>
|
||||
Generating or regenerating this grounded draft never sends recruiter email. The only outbound step is the explicit “Send and log email” action below.
|
||||
Generating or regenerating this grounded draft never sends recruiter email. Copy it or open Job email, where the connected provider and final confirmation are shown before delivery.
|
||||
</Typography>
|
||||
</Box>
|
||||
<TextField label={t("jobDetailsRecipient")} value={draftRecipient} onChange={(e) => setDraftRecipient(e.target.value)} helperText={`${t("jobDetailsRecipientHelp")} Manual send only — nothing is dispatched until you press send.`} />
|
||||
<TextField label={t("jobDetailsRecipient")} value={draftRecipient} onChange={(e) => setDraftRecipient(e.target.value)} helperText={`${t("jobDetailsRecipientHelp")} This draft is not sent from the Follow up tab.`} />
|
||||
<TextField label={t("jobDetailsSubject")} value={draftSubject} onChange={(e) => setDraftSubject(e.target.value)} />
|
||||
<TextField label={t("jobDetailsDraft")} multiline minRows={8} value={draftBody} onChange={(e) => setDraftBody(e.target.value)} helperText="You can edit this before sending. Sending stays manual and logs the sent note back to correspondence." />
|
||||
<TextField label={t("jobDetailsDraft")} multiline minRows={8} value={draftBody} onChange={(e) => setDraftBody(e.target.value)} helperText="You can edit and copy this draft. Provider delivery is available only from Job email." />
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, flexWrap: "wrap" }}>
|
||||
<Button variant="outlined" onClick={() => navigator.clipboard.writeText(`${draftSubject}\n\n${draftBody}`)}>{t("jobDetailsCopyDraft")}</Button>
|
||||
<Button variant="contained" disabled={sendingDraft || !draftSubject.trim() || !draftBody.trim()} onClick={async () => {
|
||||
if (!jobId) return;
|
||||
setSendingDraft(true);
|
||||
try {
|
||||
await api.post(`/jobapplications/${jobId}/send-followup`, { toEmail: draftRecipient || null, subject: draftSubject, body: draftBody, nextFollowUpAt: followUpDraft.suggestedSendOn || null });
|
||||
setJob((prev) => prev ? { ...prev, followUpAt: followUpDraft.suggestedSendOn } : prev);
|
||||
readinessCache.clearCached();
|
||||
setReadiness(null);
|
||||
toast(t("jobDetailsFollowUpSent"), "success");
|
||||
} catch (error: any) {
|
||||
toast(getApiErrorMessage(error, t("jobDetailsFollowUpSendFailed")), "error");
|
||||
} finally {
|
||||
setSendingDraft(false);
|
||||
}
|
||||
}}>{sendingDraft ? t("jobDetailsSending") : t("jobDetailsSendAndLogEmail")}</Button>
|
||||
<Button variant="contained" href="/correspondence">{t("jobDetailsOpenJobEmail")}</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
) : <Typography sx={{ color: "text.secondary" }}>{t("jobDetailsNoDraftAvailable")}</Typography>}
|
||||
|
||||
@@ -217,7 +217,7 @@ describe('end-to-end trust loop', () => {
|
||||
expect(await screen.findByText(/follow-up context/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/saved application package material is available for reuse/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/manual send boundary/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/the only outbound step is the explicit “send and log email” action below/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/open job email.*connected provider.*final confirmation/i)).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: /tailored cv/i }));
|
||||
|
||||
@@ -243,6 +243,7 @@ describe('end-to-end trust loop', () => {
|
||||
fireEvent.click(screen.getByRole('tab', { name: /follow up/i }));
|
||||
|
||||
expect(await screen.findByDisplayValue(/i wanted to follow up on the backend developer thread/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: /open job email/i })).toHaveAttribute('href', '/correspondence');
|
||||
expect(mockedApi.post.mock.calls.some(([url]) => url === '/jobapplications/42/send-followup')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1053,6 +1053,7 @@ export const translations = {
|
||||
jobDetailsSubject: "Subject",
|
||||
jobDetailsDraft: "Draft",
|
||||
jobDetailsCopyDraft: "Copy draft",
|
||||
jobDetailsOpenJobEmail: "Open Job email",
|
||||
jobDetailsSendAndLogEmail: "Send and log email",
|
||||
jobDetailsSending: "Sending...",
|
||||
jobDetailsFollowUpSent: "Follow-up sent and logged.",
|
||||
@@ -2183,6 +2184,7 @@ export const translations = {
|
||||
jobDetailsSubject: "Emne",
|
||||
jobDetailsDraft: "Utkast",
|
||||
jobDetailsCopyDraft: "Kopier utkast",
|
||||
jobDetailsOpenJobEmail: "Åpne Job email",
|
||||
jobDetailsSendAndLogEmail: "Send og loggfør e-post",
|
||||
jobDetailsSending: "Sender...",
|
||||
jobDetailsFollowUpSent: "Oppfølging sendt og loggført.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { ConfirmProvider } from './confirm';
|
||||
import { PromptProvider } from './prompt';
|
||||
import { ToastProvider } from './toast';
|
||||
@@ -92,7 +92,7 @@ afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('follow-up workspace shows thread grounding and keeps sending manual', async () => {
|
||||
test('follow-up workspace keeps the draft editable and routes delivery to Job email', async () => {
|
||||
renderDialog();
|
||||
|
||||
fireEvent.click(await screen.findByRole('tab', { name: /follow up/i }));
|
||||
@@ -100,20 +100,14 @@ test('follow-up workspace shows thread grounding and keeps sending manual', asyn
|
||||
expect(await screen.findByText(/follow-up context/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/saved application package material is available for reuse/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/saved cover letter available/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/manual send only/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/manual send boundary/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/open job email.*connected provider.*final confirmation/i)).toBeInTheDocument();
|
||||
expect(await screen.findByDisplayValue(/i wanted to follow up on the backend developer thread/i)).toBeInTheDocument();
|
||||
|
||||
const draft = screen.getByDisplayValue(/i wanted to follow up on the backend developer thread/i);
|
||||
fireEvent.change(draft, { target: { value: 'Hi Maria,\n\nEdited follow-up.\n\nThanks,\nCasey' } });
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /send and log email/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApi.post).toHaveBeenCalledWith('/jobapplications/42/send-followup', {
|
||||
toEmail: 'recruiter@acme.test',
|
||||
subject: 'Re: Backend Developer application update',
|
||||
body: 'Hi Maria,\n\nEdited follow-up.\n\nThanks,\nCasey',
|
||||
nextFollowUpAt: expect.any(String),
|
||||
});
|
||||
});
|
||||
expect(draft).toHaveValue('Hi Maria,\n\nEdited follow-up.\n\nThanks,\nCasey');
|
||||
expect(screen.getByRole('link', { name: /open job email/i })).toHaveAttribute('href', '/correspondence');
|
||||
expect(mockedApi.post).not.toHaveBeenCalledWith('/jobapplications/42/send-followup', expect.anything());
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user