Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
import { disableUnusedAutoDrafts } from "./disable-unused-auto-drafts";
import prisma from "@/utils/prisma";
import { ActionType, SystemType } from "@prisma/client";
import subDays from "date-fns/subDays";

vi.mock("@/utils/prisma", () => ({
default: {
emailAccount: {
findMany: vi.fn(),
},
executedAction: {
findMany: vi.fn(),
},
action: {
deleteMany: vi.fn(),
},
},
}));

vi.mock("@/utils/error", () => ({
captureException: vi.fn(),
}));

describe("disableUnusedAutoDrafts", () => {
const mockFindManyEmailAccount = prisma.emailAccount.findMany as Mock;
const mockFindManyExecutedAction = prisma.executedAction.findMany as Mock;
const mockDeleteManyAction = prisma.action.deleteMany as Mock;

beforeEach(() => {
vi.clearAllMocks();
});

it("should disable auto-draft for users who haven't used any of their last 10 drafts", async () => {
const twoDaysAgo = subDays(new Date(), 2);

// Mock user with auto-draft enabled
mockFindManyEmailAccount.mockResolvedValue([
{
id: "account-123",
rules: [{ id: "rule-123" }],
},
]);

// Mock 10 unused drafts (all older than 1 day)
const mockUnusedDrafts = Array.from({ length: 10 }, (_, i) => ({
id: `action-${i}`,
wasDraftSent: false,
draftSendLog: null,
createdAt: twoDaysAgo,
}));

mockFindManyExecutedAction.mockResolvedValue(mockUnusedDrafts);

const result = await disableUnusedAutoDrafts();

// Verify auto-draft was disabled
expect(mockDeleteManyAction).toHaveBeenCalledWith({
where: {
rule: {
emailAccountId: "account-123",
systemType: SystemType.TO_REPLY,
},
type: ActionType.DRAFT_EMAIL,
content: null,
},
});

expect(result).toEqual({
usersChecked: 1,
usersDisabled: 1,
errors: 0,
});
});

it("should not disable auto-draft if user has sent at least one draft", async () => {
const twoDaysAgo = subDays(new Date(), 2);

mockFindManyEmailAccount.mockResolvedValue([
{
id: "account-456",
rules: [{ id: "rule-456" }],
},
]);

// Mock 10 drafts where one was sent
const mockDraftsWithOneSent = Array.from({ length: 10 }, (_, i) => ({
id: `action-${i}`,
wasDraftSent: i === 5, // One draft was sent
draftSendLog: null,
createdAt: twoDaysAgo,
}));

mockFindManyExecutedAction.mockResolvedValue(mockDraftsWithOneSent);

const result = await disableUnusedAutoDrafts();

// Verify auto-draft was NOT disabled
expect(mockDeleteManyAction).not.toHaveBeenCalled();

expect(result).toEqual({
usersChecked: 1,
usersDisabled: 0,
errors: 0,
});
});

it("should not disable auto-draft if user has draft with send log", async () => {
const twoDaysAgo = subDays(new Date(), 2);

mockFindManyEmailAccount.mockResolvedValue([
{
id: "account-789",
rules: [{ id: "rule-789" }],
},
]);

// Mock 10 drafts where one has a send log
const mockDraftsWithSendLog = Array.from({ length: 10 }, (_, i) => ({
id: `action-${i}`,
wasDraftSent: false,
draftSendLog: i === 3 ? { id: "log-123" } : null, // One has send log
createdAt: twoDaysAgo,
}));

mockFindManyExecutedAction.mockResolvedValue(mockDraftsWithSendLog);

const result = await disableUnusedAutoDrafts();

// Verify auto-draft was NOT disabled
expect(mockDeleteManyAction).not.toHaveBeenCalled();

expect(result).toEqual({
usersChecked: 1,
usersDisabled: 0,
errors: 0,
});
});

it("should skip users with fewer than 10 drafts", async () => {
const twoDaysAgo = subDays(new Date(), 2);

mockFindManyEmailAccount.mockResolvedValue([
{
id: "account-999",
rules: [{ id: "rule-999" }],
},
]);

// Mock only 5 drafts (less than 10)
const mockFewDrafts = Array.from({ length: 5 }, (_, i) => ({
id: `action-${i}`,
wasDraftSent: false,
draftSendLog: null,
createdAt: twoDaysAgo,
}));

mockFindManyExecutedAction.mockResolvedValue(mockFewDrafts);

const result = await disableUnusedAutoDrafts();

// Verify auto-draft was NOT disabled
expect(mockDeleteManyAction).not.toHaveBeenCalled();

expect(result).toEqual({
usersChecked: 1,
usersDisabled: 0,
errors: 0,
});
});

it("should handle multiple users correctly", async () => {
const twoDaysAgo = subDays(new Date(), 2);

mockFindManyEmailAccount.mockResolvedValue([
{ id: "account-1", rules: [{ id: "rule-1" }] },
{ id: "account-2", rules: [{ id: "rule-2" }] },
{ id: "account-3", rules: [{ id: "rule-3" }] },
]);

// Mock different scenarios for each user
mockFindManyExecutedAction
.mockResolvedValueOnce(
// User 1: 10 unused drafts - should be disabled
Array.from({ length: 10 }, (_, i) => ({
id: `action-1-${i}`,
wasDraftSent: false,
draftSendLog: null,
createdAt: twoDaysAgo,
})),
)
.mockResolvedValueOnce(
// User 2: 10 drafts with one sent - should NOT be disabled
Array.from({ length: 10 }, (_, i) => ({
id: `action-2-${i}`,
wasDraftSent: i === 0,
draftSendLog: null,
createdAt: twoDaysAgo,
})),
)
.mockResolvedValueOnce(
// User 3: Only 5 drafts - should NOT be disabled
Array.from({ length: 5 }, (_, i) => ({
id: `action-3-${i}`,
wasDraftSent: false,
draftSendLog: null,
createdAt: twoDaysAgo,
})),
);

const result = await disableUnusedAutoDrafts();

// Only user 1 should have auto-draft disabled
expect(mockDeleteManyAction).toHaveBeenCalledTimes(1);
expect(mockDeleteManyAction).toHaveBeenCalledWith({
where: {
rule: {
emailAccountId: "account-1",
systemType: SystemType.TO_REPLY,
},
type: ActionType.DRAFT_EMAIL,
content: null,
},
});

expect(result).toEqual({
usersChecked: 3,
usersDisabled: 1,
errors: 0,
});
});

it("should handle errors gracefully", async () => {
mockFindManyEmailAccount.mockResolvedValue([
{ id: "account-error", rules: [{ id: "rule-error" }] },
]);

// Mock an error when finding drafts
mockFindManyExecutedAction.mockRejectedValue(new Error("Database error"));

const result = await disableUnusedAutoDrafts();

expect(result).toEqual({
usersChecked: 1,
usersDisabled: 0,
errors: 1,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import subDays from "date-fns/subDays";
import prisma from "@/utils/prisma";
import { ActionType, SystemType } from "@prisma/client";
import { captureException } from "@/utils/error";
import { createScopedLogger } from "@/utils/logger";

const MAX_DRAFTS_TO_CHECK = 10;

const logger = createScopedLogger("auto-draft/disable-unused");

/**
* Disables auto-draft feature for users who haven't used their last 10 drafts
* Only checks drafts that are more than a day old to give users time to use them
*/
export async function disableUnusedAutoDrafts() {
logger.info("Starting to check for unused auto-drafts");

const oneDayAgo = subDays(new Date(), 1);

// TODO: may need to make this more efficient
// Find all users who have the auto-draft feature enabled (have an Action of type DRAFT_EMAIL)
const emailAccountsWithAutoDraft = await prisma.emailAccount.findMany({
where: {
rules: {
some: {
systemType: SystemType.TO_REPLY,
actions: {
some: {
type: ActionType.DRAFT_EMAIL,
},
},
},
},
},
select: {
id: true,
rules: {
where: {
systemType: SystemType.TO_REPLY,
},
select: {
id: true,
},
},
},
});

logger.info(
`Found ${emailAccountsWithAutoDraft.length} users with auto-draft enabled`,
);

const results = {
usersChecked: emailAccountsWithAutoDraft.length,
usersDisabled: 0,
errors: 0,
};

// Process each user
for (const emailAccount of emailAccountsWithAutoDraft) {
const emailAccountId = emailAccount.id;

try {
// Find the last 10 drafts created for the user
const lastTenDrafts = await prisma.executedAction.findMany({
where: {
executedRule: {
emailAccountId,
rule: {
systemType: SystemType.TO_REPLY,
},
},
type: ActionType.DRAFT_EMAIL,
draftId: { not: null },
createdAt: { lt: oneDayAgo }, // Only check drafts older than a day
},
orderBy: {
createdAt: "desc",
},
take: MAX_DRAFTS_TO_CHECK,
select: {
id: true,
wasDraftSent: true,
draftSendLog: {
select: {
id: true,
},
},
},
});

// Skip if user has fewer than 10 drafts (not enough data to make a decision)
if (lastTenDrafts.length < MAX_DRAFTS_TO_CHECK) {
logger.info("Skipping user - only has few drafts", {
emailAccountId,
numDrafts: lastTenDrafts.length,
});
continue;
}

// Check if any of the drafts were sent
const anyDraftsSent = lastTenDrafts.some(
(draft) => draft.wasDraftSent === true || draft.draftSendLog,
);

// If none of the drafts were sent, disable auto-draft
if (!anyDraftsSent) {
logger.info("Disabling auto-draft for user - last 10 drafts not used", {
emailAccountId,
});

// Delete the DRAFT_EMAIL actions from all TO_REPLY rules
await prisma.action.deleteMany({
where: {
rule: {
emailAccountId,
systemType: SystemType.TO_REPLY,
},
type: ActionType.DRAFT_EMAIL,
content: null,
},
});

results.usersDisabled++;
}
} catch (error) {
logger.error("Error processing user", { emailAccountId, error });
captureException(error);
results.errors++;
}
}

logger.info("Completed auto-draft usage check", results);
return results;
}
Loading