import { Timestamp, type DocumentReference } from 'firebase-admin/firestore'; import { dayKey } from '../core/time.js'; /** * 2026-09-16 EC02·EC05: 누락된 집계를 0으로 채우지 않는다. * 원장 C-26 `target = max(1, ceil(avgDau × rate))` · C-27 생성 후 완료 집계일 ≤ [windowDays] 일 평균 · * C-21 집계 기간은 운영값(기본 7일). */ export async function eclipseTarget( fandom: DocumentReference, createdAt: unknown, day: string, rate = 0.05, windowDays = 7, ): Promise< { target: number; averageDau: number | null; dauDays: string[] } | { result: 'cancelled_error'; cancelReason: string } > { const invalid = (cancelReason: string) => ({ result: 'cancelled_error' as const, cancelReason }); if (!Number.isFinite(rate) || rate <= 0 || rate > 1) return invalid('INVALID_TARGET_RATE'); if (!Number.isSafeInteger(windowDays) || windowDays < 1) return invalid('INVALID_DAU_WINDOW'); if (!(createdAt instanceof Timestamp)) return invalid('MISSING_FANDOM_CREATED_AT'); // 기존 scheduler/dayKey와 같은 날짜 키로 완료된 일별 집계를 조회한다. const createdDay = dayKey(createdAt.toDate()); if (createdDay > day) return invalid('FUTURE_FANDOM_CREATED_AT'); if (createdDay === day) return { target: 1, averageDau: null, dauDays: [] }; const days: string[] = []; for (let i = 1; i <= windowDays; i++) { const previous = new Date(`${day}T00:00:00Z`); previous.setUTCDate(previous.getUTCDate() - i); const key = previous.toISOString().slice(0, 10); if (key < createdDay) break; days.push(key); } const docs = await fandom.firestore.getAll( ...days.map((key) => fandom.collection('fandomDailyStats').doc(key)), ); if (docs.some((doc) => !doc.exists || !Number.isSafeInteger(doc.get('dau')) || doc.get('dau') < 0)) { return invalid('MISSING_OR_INVALID_DAU'); } const sum = docs.reduce((total, doc) => total + (doc.get('dau') as number), 0); if (!Number.isSafeInteger(sum)) return invalid('INVALID_DAU_TOTAL'); const averageDau = sum / days.length; return { target: Math.max(1, Math.ceil(averageDau * rate)), averageDau, dauDays: days }; }