import "server-only"; import { and, eq, isNull, lt, lte } from "drizzle-orm"; import { appDb, schema } from "./appdb"; import { RAPOR_KREDI } from "./credits"; import { krediHatirlatmaEpostasi } from "./eposta"; import { epostaGonder } from "./eposta-gonder"; const { user } = schema; // 48 saat sonra tek seferlik hatırlatma (PRD İP-A / A2). Env override'ları // lokal testte akışı dakikalara sıkıştırmak için var. const GECIKME_MS = Number( process.env.KREDI_HATIRLATMA_GECIKME_MS ?? 48 * 60 * 60 * 1000, ); const ARALIK_MS = Number( process.env.KREDI_HATIRLATMA_ARALIK_MS ?? 60 * 60 * 1000, ); /** * Kredisi bitmiş ve 48 saattir dönmemiş kullanıcılara hatırlatma e-postası * gönderir. Kredi yüklemek `krediBittiAt`'ı sıfırladığı (grantCredits) ve * bakiye filtresi de burada olduğu için yalnızca hâlâ takılı olanlar seçilir. * Gönderildi işareti e-postadan ÖNCE koşullu UPDATE ile atılır: yarışta * yalnızca bir taraf `rowsAffected === 1` görür — çift gönderim imkânsız, * Resend hatasında o kullanıcının hatırlatması yanar (bilinçli at-most-once). */ export async function krediHatirlatmaTaramasi(): Promise { const esik = new Date(Date.now() - GECIKME_MS); const adaylar = await appDb .select({ id: user.id, email: user.email }) .from(user) .where( and( lte(user.krediBittiAt, esik), isNull(user.krediHatirlatmaGonderildiAt), lt(user.creditBalance, RAPOR_KREDI), ), ) .limit(100); for (const aday of adaylar) { const res = await appDb .update(user) .set({ krediHatirlatmaGonderildiAt: new Date() }) .where( and(eq(user.id, aday.id), isNull(user.krediHatirlatmaGonderildiAt)), ); if (res.rowsAffected !== 1) continue; // başka instance kazandı try { await epostaGonder(aday.email, krediHatirlatmaEpostasi()); } catch (err) { console.error( `[hatirlatma] Kredi hatırlatması gönderilemedi (${aday.email}):`, err, ); } } } /** * Saatlik tarama zamanlayıcısı — instrumentation.ts'ten, sunucu başına bir * kez çağrılır. globalThis guard'ı dev hot-reload'da çifte interval'i önler * (appdb/index.ts deseni). */ export function krediHatirlatmaBaslat(): void { const g = globalThis as unknown as { __krediHatirlatmaKuruldu?: boolean }; if (g.__krediHatirlatmaKuruldu) return; g.__krediHatirlatmaKuruldu = true; let calisiyor = false; const tik = async () => { if (calisiyor) return; // önceki tarama uzadıysa üst üste binme calisiyor = true; try { await krediHatirlatmaTaramasi(); } catch (err) { console.error("[hatirlatma] Tarama hatası:", err); } finally { calisiyor = false; } }; // Container yeniden başlarsa saatlik açıklık büyümesin diye kısa bir // gecikmeyle açılış taraması da yapılır. setTimeout(tik, 60_000); setInterval(tik, ARALIK_MS); }