// 公司研究同步節奏:首次進入必抓;之後等到「下次財報/公開」再更新 import { fetchEarningsEvents } from './calendar.js'; function addDaysISO(base, days) { const d = new Date(base + 'T12:00:00Z'); d.setUTCDate(d.getUTCDate() + days); return d.toISOString().slice(0, 10); } function todayISO() { return new Date().toISOString().slice(0, 10); } /** 下次允許重新抓取的日期 = 下一個財報日(尚無則約一季後) */ export async function computeNextPublicRefresh(symbol) { symbol = String(symbol || '').trim().toUpperCase(); const today = todayISO(); const end = addDaysISO(today, 200); try { const events = await fetchEarningsEvents(today, end, [symbol]); const upcoming = (events || []) .filter(e => e.date && e.date > today) .sort((a, b) => a.date.localeCompare(b.date)); if (upcoming.length) { const next = upcoming[0]; return { nextRefreshAfter: next.date, nextPublicLabel: next.title || `${symbol} 財報`, nextPublicDate: next.date, }; } } catch { /* fallback */ } const fallback = addDaysISO(today, 92); return { nextRefreshAfter: fallback, nextPublicLabel: '約一季後(暫無財報日曆)', nextPublicDate: fallback, }; } export function intelRefreshPolicy(enrichedRow) { const data = enrichedRow?.data || {}; const lastSyncAt = data.lastSyncAt || enrichedRow?.updatedAt || null; const nextRefreshAfter = data.nextRefreshAfter || null; const today = todayISO(); const neverSynced = !lastSyncAt; const due = nextRefreshAfter ? today >= nextRefreshAfter : false; const needsSync = neverSynced || due; let skipReason = null; if (!needsSync && nextRefreshAfter) { skipReason = `已同步;下次更新:${nextRefreshAfter}(${data.nextPublicLabel || '下次財報/公開'})`; } else if (!needsSync) { skipReason = '已同步'; } return { needsSync, lastSyncAt: lastSyncAt ? new Date(lastSyncAt).toISOString() : null, nextRefreshAfter, nextPublicLabel: data.nextPublicLabel || null, skipReason, }; } export function shouldRunIntelSync(enrichedRow, { force = false } = {}) { if (force) return { run: true, reason: 'force' }; const p = intelRefreshPolicy(enrichedRow); return { run: p.needsSync, reason: p.needsSync ? 'first_or_due' : 'wait_public', ...p }; }