Files
kolaytercih/src/lib/kredi-hatirlatma.ts
bilalgursen 820eea7eca refactor: ortak tipler @/types/yokatlas + features/rapor/types'a çıkarıldı, server-only korumaları eklendi (Faz 1)
- Program/PUAN_TURLERI/DilimKey/RankResults/UniturGrubu/ProgramNetSatiri/
  SihirbazFacetleri artık src/types/yokatlas.ts'te; lib/db.ts yalnızca sorgu
  fonksiyonları barındırıyor
- RaporSonuc/RaporKapsam/RaporParams/MaskeliRapor src/features/rapor/types/
  rapor.ts'e taşındı; ölü kalan TercihSchema/RaporSchema silindi
- 11 lib dosyasına import "server-only" eklendi; scripts'in düz node ile
  import ettiği dosyalara (db, appdb, rapor-havuzu, tadimlik-havuzu, ai/client,
  ai/cagri, credits) bilinçli eklenmedi
- Davranış değişikliği yok; pnpm build yeşil

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 01:06:06 +03:00

88 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<void> {
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);
}