, recent_requests:int, avg_reserved:?float} $s * daily_units: units used on each of the last seven days (newest first, zeros included) * @return list */ public static function evaluate(array $s): array { $limit = (int) $s['limit']; if ($limit <= 0) { return []; // no limit is set: reported elsewhere, there is no percentage to speak of } $used = (int) $s['spent'] + (int) $s['reserved']; $available = $limit - $used; $percent = (int) min(100, intdiv($used * 100, $limit)); $base = ['scope' => 'site', 'percent' => $percent, 'available' => max(0, $available)]; $out = []; if ($available <= 0) { return [['code' => 'exhausted'] + $base]; } if ($used * 100 >= self::WARNING_PERCENT * $limit) { $out[] = ['code' => 'warning_95'] + $base; } elseif ($used * 100 >= self::NOTICE_PERCENT * $limit) { $out[] = ['code' => 'notice_80'] + $base; } if ((int) $s['recent_requests'] >= self::MIN_REQUESTS_FOR_HEADROOM && $s['avg_reserved'] !== null && $available < $s['avg_reserved']) { $out[] = ['code' => 'headroom'] + $base; } $daily = array_map('intval', array_slice((array) $s['daily_units'], 0, 7)); $active = count(array_filter($daily, static fn(int $u): bool => $u > 0)); if ($active >= self::MIN_ACTIVE_DAYS && (int) $s['uncertain_requests'] === 0 && (int) $s['estimated_requests'] === 0) { $perDay = array_sum($daily) / 7; if ($perDay > 0) { $days = (int) floor($available / $perDay); if ($days <= self::MAX_PROJECTION_DAYS) { $out[] = ['code' => 'pace', 'days' => $days] + $base; } } } return $out; } /** * Gathers the numbers for one site and evaluates them. Reads local accounting rows only: the site balance, the last seven days of * usage, unresolved requests and the size of recent reservations. Never contacts a provider. * * @return list */ public static function forSite(Ledger $ledger, int $siteId, int $now, \DateTimeZone $tz): array { $balance = $ledger->balance($siteId, 'site'); if ($balance === null || (int) $balance['limit'] <= 0) { return []; } $week = $ledger->periodTotals($siteId, UsageSeries::days($now, $tz, 7)); $profile = $ledger->recentProfile($siteId, $now - 7 * 86400); return self::evaluate([ 'limit' => (int) $balance['limit'], 'spent' => (int) $balance['spent'], 'reserved' => (int) $balance['reserved'], // Unresolved or unconfirmed requests of ANY age make a pace misleading, so only whether one exists matters. 'uncertain_requests' => count($ledger->uncertain($siteId, 1)), 'estimated_requests' => count($ledger->estimated($siteId, 1)), 'daily_units' => array_column($week, 'units'), 'recent_requests' => $profile['requests'], 'avg_reserved' => $profile['avg_reserved'], ]); } }