diff --git a/apps/web/prisma/migrations/20251110013724_add_outlook_subscription_history/migration.sql b/apps/web/prisma/migrations/20251110013724_add_outlook_subscription_history/migration.sql new file mode 100644 index 0000000000..8210af0e0d --- /dev/null +++ b/apps/web/prisma/migrations/20251110013724_add_outlook_subscription_history/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "EmailAccount" ADD COLUMN IF NOT EXISTS "watchEmailsSubscriptionHistory" JSONB; diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index 6fc0a5abeb..08747dd174 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -113,16 +113,17 @@ model EmailAccount { name String? // Name associated with the Google account image String? // Profile image URL from the Google account - about String? - writingStyle String? - signature String? // User's email signature from provider or manually set - includeReferralSignature Boolean @default(true) - watchEmailsExpirationDate DateTime? - watchEmailsSubscriptionId String? // For Outlook subscription ID - lastSyncedHistoryId String? - behaviorProfile Json? - personaAnalysis Json? // ai analysis of the user's persona - role String? // the role confirmed by the user - previously `User.surveyRole` + about String? + writingStyle String? + signature String? // User's email signature from provider or manually set + includeReferralSignature Boolean @default(true) + watchEmailsExpirationDate DateTime? + watchEmailsSubscriptionId String? // For Outlook subscription ID + watchEmailsSubscriptionHistory Json? // Historical Outlook subscription IDs: [{ subscriptionId, createdAt, replacedAt }] + lastSyncedHistoryId String? + behaviorProfile Json? + personaAnalysis Json? // ai analysis of the user's persona + role String? // the role confirmed by the user - previously `User.surveyRole` statsEmailFrequency Frequency @default(WEEKLY) summaryEmailFrequency Frequency @default(WEEKLY) @@ -802,8 +803,6 @@ model Payment { premiumId String? premium Premium? @relation(fields: [premiumId], references: [id], onDelete: SetNull) - @@index([premiumId]) - // Payment processor information processorType ProcessorType @default(LEMON_SQUEEZY) processorId String? @unique // External payment ID from Stripe/Lemon Squeezy @@ -824,6 +823,8 @@ model Payment { // Metadata billingReason String? // initial, renewal, update, etc. + + @@index([premiumId]) } model DraftSendLog { diff --git a/apps/web/utils/outlook/subscription-history.test.ts b/apps/web/utils/outlook/subscription-history.test.ts new file mode 100644 index 0000000000..1fdd36967c --- /dev/null +++ b/apps/web/utils/outlook/subscription-history.test.ts @@ -0,0 +1,267 @@ +import { describe, it, expect, vi } from "vitest"; +import { + parseSubscriptionHistory, + createHistoryEntry, + cleanupOldHistoryEntries, + isSubscriptionInHistory, + addCurrentSubscriptionToHistory, +} from "./subscription-history"; + +describe("subscription-history", () => { + describe("parseSubscriptionHistory", () => { + it("should parse valid subscription history", () => { + const history = [ + { + subscriptionId: "sub-1", + createdAt: "2024-01-01T00:00:00Z", + replacedAt: "2024-01-05T00:00:00Z", + }, + { + subscriptionId: "sub-2", + createdAt: "2024-01-05T00:00:00Z", + replacedAt: "2024-01-10T00:00:00Z", + }, + ]; + + const result = parseSubscriptionHistory(history); + + expect(result).toEqual(history); + }); + + it("should return empty array for null/undefined", () => { + expect(parseSubscriptionHistory(null)).toEqual([]); + expect(parseSubscriptionHistory(undefined)).toEqual([]); + }); + + it("should filter out invalid entries", () => { + const logger = { warn: vi.fn() } as any; + const history = [ + { + subscriptionId: "sub-1", + createdAt: "2024-01-01T00:00:00Z", + replacedAt: "2024-01-05T00:00:00Z", + }, + { subscriptionId: "sub-2" }, // missing fields + "invalid", // not an object + ]; + + const result = parseSubscriptionHistory(history, logger); + + expect(result).toHaveLength(1); + expect(result[0].subscriptionId).toBe("sub-1"); + expect(logger.warn).toHaveBeenCalledTimes(2); + }); + + it("should handle non-array input", () => { + const result = parseSubscriptionHistory({ not: "an array" }); + expect(result).toEqual([]); + }); + }); + + describe("createHistoryEntry", () => { + it("should create a valid history entry", () => { + const entry = createHistoryEntry( + "sub-123", + "2024-01-01T00:00:00Z", + "2024-01-05T00:00:00Z", + ); + + expect(entry).toEqual({ + subscriptionId: "sub-123", + createdAt: "2024-01-01T00:00:00Z", + replacedAt: "2024-01-05T00:00:00Z", + }); + }); + }); + + describe("cleanupOldHistoryEntries", () => { + it("should remove entries older than specified days", () => { + const now = new Date(); + const tenDaysAgo = new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000); + const twentyDaysAgo = new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000); + const fortyDaysAgo = new Date(now.getTime() - 40 * 24 * 60 * 60 * 1000); + + const history = [ + { + subscriptionId: "very-old", + createdAt: "2024-01-01T00:00:00Z", + replacedAt: fortyDaysAgo.toISOString(), + }, + { + subscriptionId: "old", + createdAt: "2024-01-10T00:00:00Z", + replacedAt: twentyDaysAgo.toISOString(), + }, + { + subscriptionId: "recent", + createdAt: "2024-01-20T00:00:00Z", + replacedAt: tenDaysAgo.toISOString(), + }, + ]; + + const result = cleanupOldHistoryEntries(history, 30); + + expect(result).toHaveLength(2); + expect(result[0].subscriptionId).toBe("old"); + expect(result[1].subscriptionId).toBe("recent"); + }); + + it("should use default 30 days when not specified", () => { + const now = new Date(); + const fortyDaysAgo = new Date(now.getTime() - 40 * 24 * 60 * 60 * 1000); + const twentyDaysAgo = new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000); + + const history = [ + { + subscriptionId: "old", + createdAt: "2024-01-01T00:00:00Z", + replacedAt: fortyDaysAgo.toISOString(), + }, + { + subscriptionId: "recent", + createdAt: "2024-01-10T00:00:00Z", + replacedAt: twentyDaysAgo.toISOString(), + }, + ]; + + const result = cleanupOldHistoryEntries(history); + + expect(result).toHaveLength(1); + expect(result[0].subscriptionId).toBe("recent"); + }); + + it("should keep all entries if all are recent", () => { + const now = new Date(); + const fiveDaysAgo = new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000); + + const history = [ + { + subscriptionId: "sub-1", + createdAt: "2024-01-01T00:00:00Z", + replacedAt: fiveDaysAgo.toISOString(), + }, + ]; + + const result = cleanupOldHistoryEntries(history, 30); + + expect(result).toHaveLength(1); + }); + }); + + describe("isSubscriptionInHistory", () => { + it("should return true if subscription ID exists in history", () => { + const history = [ + { + subscriptionId: "sub-1", + createdAt: "2024-01-01T00:00:00Z", + replacedAt: "2024-01-05T00:00:00Z", + }, + { + subscriptionId: "sub-2", + createdAt: "2024-01-05T00:00:00Z", + replacedAt: "2024-01-10T00:00:00Z", + }, + ]; + + expect(isSubscriptionInHistory("sub-1", history)).toBe(true); + expect(isSubscriptionInHistory("sub-2", history)).toBe(true); + }); + + it("should return false if subscription ID does not exist", () => { + const history = [ + { + subscriptionId: "sub-1", + createdAt: "2024-01-01T00:00:00Z", + replacedAt: "2024-01-05T00:00:00Z", + }, + ]; + + expect(isSubscriptionInHistory("sub-999", history)).toBe(false); + }); + + it("should return false for empty history", () => { + expect(isSubscriptionInHistory("sub-1", [])).toBe(false); + expect(isSubscriptionInHistory("sub-1", null)).toBe(false); + }); + + it("should handle invalid history data", () => { + expect(isSubscriptionInHistory("sub-1", "not an array")).toBe(false); + expect(isSubscriptionInHistory("sub-1", { not: "valid" })).toBe(false); + }); + }); + + describe("addCurrentSubscriptionToHistory", () => { + it("should add subscription to empty history", () => { + const replacedAt = new Date("2024-01-10T00:00:00Z"); + const fallbackCreatedAt = new Date("2024-01-01T00:00:00Z"); + + const result = addCurrentSubscriptionToHistory( + null, + "sub-123", + replacedAt, + fallbackCreatedAt, + ); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + subscriptionId: "sub-123", + createdAt: fallbackCreatedAt.toISOString(), + replacedAt: replacedAt.toISOString(), + }); + }); + + it("should use last entry's replacedAt as createdAt for new entry", () => { + const existingHistory = [ + { + subscriptionId: "sub-1", + createdAt: "2024-01-01T00:00:00Z", + replacedAt: "2024-01-05T00:00:00Z", + }, + ]; + + const replacedAt = new Date("2024-01-10T00:00:00Z"); + const fallbackCreatedAt = new Date("2024-01-01T00:00:00Z"); + + const result = addCurrentSubscriptionToHistory( + existingHistory, + "sub-2", + replacedAt, + fallbackCreatedAt, + ); + + expect(result).toHaveLength(2); + expect(result[1]).toEqual({ + subscriptionId: "sub-2", + createdAt: "2024-01-05T00:00:00Z", // from last entry's replacedAt + replacedAt: replacedAt.toISOString(), + }); + }); + + it("should preserve existing history entries", () => { + const existingHistory = [ + { + subscriptionId: "sub-1", + createdAt: "2024-01-01T00:00:00Z", + replacedAt: "2024-01-05T00:00:00Z", + }, + { + subscriptionId: "sub-2", + createdAt: "2024-01-05T00:00:00Z", + replacedAt: "2024-01-08T00:00:00Z", + }, + ]; + + const result = addCurrentSubscriptionToHistory( + existingHistory, + "sub-3", + new Date("2024-01-10T00:00:00Z"), + new Date("2024-01-01T00:00:00Z"), + ); + + expect(result).toHaveLength(3); + expect(result[0]).toEqual(existingHistory[0]); + expect(result[1]).toEqual(existingHistory[1]); + expect(result[2].subscriptionId).toBe("sub-3"); + }); + }); +}); diff --git a/apps/web/utils/outlook/subscription-history.ts b/apps/web/utils/outlook/subscription-history.ts new file mode 100644 index 0000000000..2ce0bf577e --- /dev/null +++ b/apps/web/utils/outlook/subscription-history.ts @@ -0,0 +1,126 @@ +import type { Logger } from "@/utils/logger"; + +export type SubscriptionHistoryEntry = { + subscriptionId: string; + createdAt: string; + replacedAt: string; +}; + +export type SubscriptionHistory = SubscriptionHistoryEntry[]; + +/** + * Parse subscription history from unknown JSONB data + */ +export function parseSubscriptionHistory( + rawHistory: unknown, + logger?: Logger, +): SubscriptionHistory { + if (!rawHistory) { + return []; + } + + try { + if (Array.isArray(rawHistory)) { + // Validate that each entry has the required fields + return rawHistory.filter((entry): entry is SubscriptionHistoryEntry => { + const hasRequiredFields = + typeof entry === "object" && + entry !== null && + "subscriptionId" in entry && + "createdAt" in entry && + "replacedAt" in entry && + typeof entry.subscriptionId === "string" && + typeof entry.createdAt === "string" && + typeof entry.replacedAt === "string"; + + if (!hasRequiredFields) { + logger?.warn("Invalid subscription history entry", { entry }); + } + + return hasRequiredFields; + }); + } + } catch (error) { + logger?.warn("Failed to parse subscription history", { error }); + } + + return []; +} + +/** + * Create a new history entry + */ +export function createHistoryEntry( + subscriptionId: string, + createdAt: string, + replacedAt: string, +): SubscriptionHistoryEntry { + return { + subscriptionId, + createdAt, + replacedAt, + }; +} + +/** + * Remove history entries older than the specified number of days + */ +export function cleanupOldHistoryEntries( + history: SubscriptionHistory, + daysToKeep = 30, +): SubscriptionHistory { + const cutoffDate = new Date(Date.now() - daysToKeep * 24 * 60 * 60 * 1000); + return history.filter((entry) => new Date(entry.replacedAt) > cutoffDate); +} + +/** + * Check if a subscription ID exists in the history + */ +export function isSubscriptionInHistory( + subscriptionId: string, + rawHistory: unknown, +): boolean { + const history = parseSubscriptionHistory(rawHistory); + return history.some((entry) => entry.subscriptionId === subscriptionId); +} + +/** + * Add a subscription to history with timestamps + */ +export function addToHistory( + currentHistory: unknown, + subscriptionId: string, + createdAt: string, + replacedAt: string, + logger?: Logger, +): SubscriptionHistory { + const parsed = parseSubscriptionHistory(currentHistory, logger); + const newEntry = createHistoryEntry(subscriptionId, createdAt, replacedAt); + return [...parsed, newEntry]; +} + +/** + * Add current subscription to history, estimating createdAt from history or fallback date + */ +export function addCurrentSubscriptionToHistory( + currentHistory: unknown, + subscriptionId: string, + replacedAt: Date, + fallbackCreatedAt: Date, + logger?: Logger, +): SubscriptionHistory { + const parsed = parseSubscriptionHistory(currentHistory, logger); + + const estimatedCreatedAt = + parsed.length > 0 + ? parsed[parsed.length - 1].replacedAt + : fallbackCreatedAt.toISOString(); + + const newEntry = createHistoryEntry( + subscriptionId, + estimatedCreatedAt, + replacedAt.toISOString(), + ); + + return [...parsed, newEntry]; +} diff --git a/apps/web/utils/outlook/subscription-manager.test.ts b/apps/web/utils/outlook/subscription-manager.test.ts index e2a394d193..cfc21b6670 100644 --- a/apps/web/utils/outlook/subscription-manager.test.ts +++ b/apps/web/utils/outlook/subscription-manager.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { OutlookSubscriptionManager } from "@/utils/outlook/subscription-manager"; import prisma from "@/utils/prisma"; import type { EmailProvider } from "@/utils/email/types"; +import type { SubscriptionHistoryEntry } from "@/utils/outlook/subscription-history"; // Mock dependencies vi.mock("server-only", () => ({})); @@ -132,22 +133,164 @@ describe("OutlookSubscriptionManager", () => { describe("updateSubscriptionInDatabase", () => { it("should update database with new subscription details", async () => { + vi.mocked(prisma.emailAccount.findUnique).mockResolvedValue({ + id: emailAccountId, + watchEmailsSubscriptionId: null, + watchEmailsSubscriptionHistory: null, + createdAt: new Date("2024-01-01T00:00:00Z"), + } as any); + const subscription = { subscriptionId: "test-subscription-id", expirationDate: new Date("2024-01-01T00:00:00Z"), }; - // Act await manager.updateSubscriptionInDatabase(subscription); - // Assert expect(prisma.emailAccount.update).toHaveBeenCalledWith({ where: { id: emailAccountId }, data: { watchEmailsExpirationDate: new Date("2024-01-01T00:00:00Z"), watchEmailsSubscriptionId: "test-subscription-id", + watchEmailsSubscriptionHistory: [], }, }); }); + + it("should move old subscription to history when updating to new subscription", async () => { + const oldSubscriptionId = "old-subscription-id"; + const accountCreatedAt = new Date("2024-01-01T00:00:00Z"); + + vi.mocked(prisma.emailAccount.findUnique).mockResolvedValue({ + id: emailAccountId, + watchEmailsSubscriptionId: oldSubscriptionId, + watchEmailsSubscriptionHistory: null, + createdAt: accountCreatedAt, + } as any); + + const subscription = { + subscriptionId: "new-subscription-id", + expirationDate: new Date("2024-01-15T00:00:00Z"), + }; + + await manager.updateSubscriptionInDatabase(subscription); + + const updateCall = vi.mocked(prisma.emailAccount.update).mock.calls[0][0]; + const history = updateCall.data + .watchEmailsSubscriptionHistory as SubscriptionHistoryEntry[]; + + expect(updateCall.data.watchEmailsSubscriptionId).toBe( + "new-subscription-id", + ); + expect(history).toHaveLength(1); + expect(history[0]).toMatchObject({ + subscriptionId: oldSubscriptionId, + createdAt: accountCreatedAt.toISOString(), + }); + expect(history[0].replacedAt).toBeDefined(); + }); + + it("should preserve existing history when adding new entry", async () => { + const now = new Date(); + const fiveDaysAgo = new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000); + const tenDaysAgo = new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000); + + const existingHistory = [ + { + subscriptionId: "previous-subscription", + createdAt: tenDaysAgo.toISOString(), + replacedAt: fiveDaysAgo.toISOString(), + }, + ]; + + vi.mocked(prisma.emailAccount.findUnique).mockResolvedValue({ + id: emailAccountId, + watchEmailsSubscriptionId: "old-subscription-id", + watchEmailsSubscriptionHistory: existingHistory, + createdAt: new Date("2024-01-01T00:00:00Z"), + } as any); + + const subscription = { + subscriptionId: "new-subscription-id", + expirationDate: new Date("2024-01-15T00:00:00Z"), + }; + + await manager.updateSubscriptionInDatabase(subscription); + + const updateCall = vi.mocked(prisma.emailAccount.update).mock.calls[0][0]; + const history = updateCall.data + .watchEmailsSubscriptionHistory as SubscriptionHistoryEntry[]; + + expect(history).toHaveLength(2); + expect(history[0]).toEqual(existingHistory[0]); + expect(history[1].subscriptionId).toBe("old-subscription-id"); + }); + + it("should clean up history entries older than 30 days", async () => { + const now = new Date(); + const thirtyOneDaysAgo = new Date( + now.getTime() - 31 * 24 * 60 * 60 * 1000, + ); + const twentyNineDaysAgo = new Date( + now.getTime() - 29 * 24 * 60 * 60 * 1000, + ); + + const existingHistory = [ + { + subscriptionId: "very-old-subscription", + createdAt: "2024-01-01T00:00:00Z", + replacedAt: thirtyOneDaysAgo.toISOString(), + }, + { + subscriptionId: "recent-subscription", + createdAt: "2024-01-10T00:00:00Z", + replacedAt: twentyNineDaysAgo.toISOString(), + }, + ]; + + vi.mocked(prisma.emailAccount.findUnique).mockResolvedValue({ + id: emailAccountId, + watchEmailsSubscriptionId: "current-subscription-id", + watchEmailsSubscriptionHistory: existingHistory, + createdAt: new Date("2024-01-01T00:00:00Z"), + } as any); + + const subscription = { + subscriptionId: "new-subscription-id", + expirationDate: new Date("2024-01-15T00:00:00Z"), + }; + + await manager.updateSubscriptionInDatabase(subscription); + + const updateCall = vi.mocked(prisma.emailAccount.update).mock.calls[0][0]; + const history = updateCall.data + .watchEmailsSubscriptionHistory as SubscriptionHistoryEntry[]; + + // Should only have the recent entry + the new one being added + expect(history).toHaveLength(2); + expect(history[0].subscriptionId).toBe("recent-subscription"); + expect(history[1].subscriptionId).toBe("current-subscription-id"); + }); + + it("should not add to history when subscription ID has not changed", async () => { + const currentSubscriptionId = "same-subscription-id"; + + vi.mocked(prisma.emailAccount.findUnique).mockResolvedValue({ + id: emailAccountId, + watchEmailsSubscriptionId: currentSubscriptionId, + watchEmailsSubscriptionHistory: null, + createdAt: new Date("2024-01-01T00:00:00Z"), + } as any); + + const subscription = { + subscriptionId: currentSubscriptionId, + expirationDate: new Date("2024-01-15T00:00:00Z"), + }; + + await manager.updateSubscriptionInDatabase(subscription); + + const updateCall = vi.mocked(prisma.emailAccount.update).mock.calls[0][0]; + expect(updateCall.data.watchEmailsSubscriptionHistory).toHaveLength(0); + }); }); }); diff --git a/apps/web/utils/outlook/subscription-manager.ts b/apps/web/utils/outlook/subscription-manager.ts index 7ac91e49c8..45ed39bbc6 100644 --- a/apps/web/utils/outlook/subscription-manager.ts +++ b/apps/web/utils/outlook/subscription-manager.ts @@ -4,6 +4,11 @@ import { captureException } from "@/utils/error"; import type { EmailProvider } from "@/utils/email/types"; import { createEmailProvider } from "@/utils/email/provider"; import type { Logger } from "@/utils/logger"; +import { + parseSubscriptionHistory, + cleanupOldHistoryEntries, + addCurrentSubscriptionToHistory, +} from "@/utils/outlook/subscription-history"; /** * Manages Outlook subscriptions, ensuring only one active subscription per email account @@ -138,6 +143,8 @@ export class OutlookSubscriptionManager { select: { watchEmailsSubscriptionId: true, watchEmailsExpirationDate: true, + watchEmailsSubscriptionHistory: true, + createdAt: true, }, }); @@ -146,9 +153,8 @@ export class OutlookSubscriptionManager { return { subscriptionId: emailAccount.watchEmailsSubscriptionId || null, expirationDate: emailAccount.watchEmailsExpirationDate || null, - } as { - subscriptionId: string | null; - expirationDate: Date | null; + subscriptionHistory: emailAccount.watchEmailsSubscriptionHistory, + accountCreatedAt: emailAccount.createdAt, }; } @@ -161,18 +167,48 @@ export class OutlookSubscriptionManager { } const expirationDate = subscription.expirationDate; + const now = new Date(); + + const existing = await this.getExistingSubscription(); + + let updatedHistory = parseSubscriptionHistory( + existing?.subscriptionHistory, + this.logger, + ); + updatedHistory = cleanupOldHistoryEntries(updatedHistory); + + if ( + existing?.subscriptionId && + existing.subscriptionId !== subscription.subscriptionId + ) { + updatedHistory = addCurrentSubscriptionToHistory( + updatedHistory, + existing.subscriptionId, + now, + existing.accountCreatedAt, + this.logger, + ); + + this.logger.info("Moving old subscription to history", { + oldSubscriptionId: existing.subscriptionId, + newSubscriptionId: subscription.subscriptionId, + historyLength: updatedHistory.length, + }); + } await prisma.emailAccount.update({ where: { id: this.emailAccountId }, data: { watchEmailsExpirationDate: expirationDate, watchEmailsSubscriptionId: subscription.subscriptionId, + watchEmailsSubscriptionHistory: updatedHistory, }, }); this.logger.info("Updated subscription in database", { subscriptionId: subscription.subscriptionId, expirationDate, + historyEntries: updatedHistory.length, }); } } diff --git a/apps/web/utils/webhook/validate-webhook-account.ts b/apps/web/utils/webhook/validate-webhook-account.ts index d1baec9d73..ac490afce5 100644 --- a/apps/web/utils/webhook/validate-webhook-account.ts +++ b/apps/web/utils/webhook/validate-webhook-account.ts @@ -19,6 +19,7 @@ export async function getWebhookEmailAccount( lastSyncedHistoryId: true, autoCategorizeSenders: true, watchEmailsSubscriptionId: true, + watchEmailsSubscriptionHistory: true, account: { select: { provider: true, @@ -55,11 +56,40 @@ export async function getWebhookEmailAccount( }); } - const emailAccount = await prisma.emailAccount.findFirst({ + let emailAccount = await prisma.emailAccount.findFirst({ where: { watchEmailsSubscriptionId: where.watchEmailsSubscriptionId }, ...query, }); + if (!emailAccount) { + logger.info("Subscription not found in current field, checking history", { + subscriptionId: where.watchEmailsSubscriptionId, + }); + + const [foundAccount] = await prisma.$queryRaw>` + SELECT id FROM "EmailAccount" + WHERE "watchEmailsSubscriptionHistory" @> ${JSON.stringify([ + { subscriptionId: where.watchEmailsSubscriptionId }, + ])}::jsonb + LIMIT 1 + `; + + if (foundAccount) { + emailAccount = await prisma.emailAccount.findUnique({ + where: { id: foundAccount.id }, + ...query, + }); + + if (emailAccount) { + logger.info("Found account by historical subscription ID", { + subscriptionId: where.watchEmailsSubscriptionId, + email: emailAccount.email, + currentSubscriptionId: emailAccount.watchEmailsSubscriptionId, + }); + } + } + } + if (!emailAccount) { logger.error("Account not found", where); } diff --git a/version.txt b/version.txt index 542963996e..b8f4127b23 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v2.18.13 +v2.18.14