- 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>
88 lines
2.9 KiB
TypeScript
88 lines
2.9 KiB
TypeScript
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);
|
||
} |