Files
fanuzu/app/firebase/functions/src/fandom/eclipse_target.ts
T

46 lines
2.1 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 { 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 };
}