trim((string)$key), 'secret' => trim((string)$secret)]; } function ds_require_credentials(): array { $c = ds_credentials(); if ($c['key'] === '' || $c['secret'] === '') { throw new RuntimeException('Missing GoDaddy credentials. Create config/credentials.local.json from credentials.example.json or set GODADDY_API_KEY and GODADDY_API_SECRET.'); } return $c; } function ds_http_get_json(string $url, array $headers): array { if (!function_exists('curl_init')) { throw new RuntimeException('PHP cURL extension is required.'); } $ch = curl_init($url); if ($ch === false) throw new RuntimeException('Could not initialize cURL.'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_TIMEOUT => 45, CURLOPT_HTTPHEADER => $headers, CURLOPT_HEADER => true ]); $raw = curl_exec($ch); if ($raw === false) { $err = curl_error($ch); curl_close($ch); throw new RuntimeException('cURL error: ' . $err); } $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $headerSize = (int)curl_getinfo($ch, CURLINFO_HEADER_SIZE); $body = substr($raw, $headerSize); curl_close($ch); if ($status < 200 || $status >= 300) { throw new RuntimeException('GoDaddy API HTTP ' . $status . ': ' . substr($body, 0, 500)); } $data = json_decode($body, true); if (!is_array($data)) throw new RuntimeException('GoDaddy API returned non-JSON or unexpected JSON.'); return $data; } function ds_domain_tld(string $domain): string { $parts = explode('.', strtolower($domain)); return count($parts) > 1 ? end($parts) : ''; } function ds_days_until(?string $date): ?int { if (!$date) return null; $ts = strtotime($date); if ($ts === false) return null; return (int)ceil(($ts - time()) / 86400); } function ds_estimated_renewal(string $domain, array $settings): float { $tld = ds_domain_tld($domain); $table = $settings['tld_annual_renewal_estimate_usd'] ?? []; return isset($table[$tld]) ? (float)$table[$tld] : (float)$settings['default_annual_renewal_estimate_usd']; } function ds_action_hint(array $row): string { $days = $row['days_until_expiration']; $auto = strtolower((string)$row['renew_auto']); if ($days !== null && $days <= 45 && ($auto === 'true' || $auto === '1' || $auto === 'yes')) return 'URGENT REVIEW AUTO-RENEW'; if ($days !== null && $days <= 90) return 'REVIEW BEFORE RENEWAL'; return 'CLASSIFY: keep / sell / donate / return / drop'; } function ds_fetch_domains(): array { $settings = ds_settings(); $creds = ds_require_credentials(); $base = rtrim((string)$settings['api_base_url'], '/'); $headers = [ 'Authorization: sso-key ' . $creds['key'] . ':' . $creds['secret'], 'Accept: application/json', 'X-Market-Id: ' . (string)$settings['market_id'] ]; if (!empty($settings['shopper_id'])) $headers[] = 'X-Shopper-Id: ' . (string)$settings['shopper_id']; $all = []; $marker = null; $limit = 1000; for ($i = 0; $i < 50; $i++) { $query = ['limit' => $limit]; if ($marker) $query['marker'] = $marker; $url = $base . '/v1/domains?' . http_build_query($query); $batch = ds_http_get_json($url, $headers); if (!$batch) break; foreach ($batch as $d) if (is_array($d)) $all[] = $d; if (count($batch) < $limit) break; $last = end($batch); if (!is_array($last) || empty($last['domain'])) break; $marker = (string)$last['domain']; } return $all; } function ds_normalize_domains(array $domains): array { $settings = ds_settings(); $window = (int)$settings['estimate_window_days']; $rows = []; foreach ($domains as $d) { $domain = (string)($d['domain'] ?? ''); if ($domain === '') continue; $expires = (string)($d['expires'] ?? ($d['expirationDate'] ?? '')); $days = ds_days_until($expires); $est = ds_estimated_renewal($domain, $settings); $due = ($days !== null && $days >= 0 && $days <= $window) ? $est : 0.0; $row = [ 'domain' => $domain, 'tld' => ds_domain_tld($domain), 'status' => (string)($d['status'] ?? ''), 'expires' => $expires, 'days_until_expiration' => $days, 'renew_auto' => isset($d['renewAuto']) ? ($d['renewAuto'] ? 'true' : 'false') : '', 'renewable' => isset($d['renewable']) ? ($d['renewable'] ? 'true' : 'false') : '', 'estimated_annual_renewal_usd' => number_format($est, 2, '.', ''), 'estimated_due_next_' . $window . '_days_usd' => number_format($due, 2, '.', ''), 'project_bucket' => '', 'recommended_action' => '', 'notes' => '' ]; $row['recommended_action'] = ds_action_hint($row); $rows[] = $row; } usort($rows, function($a, $b) { $ad = $a['expires'] ?: '9999-12-31'; $bd = $b['expires'] ?: '9999-12-31'; return strcmp($ad, $bd); }); return $rows; } function ds_csv(array $rows): string { $settings = ds_settings(); $window = (int)$settings['estimate_window_days']; $headers = [ 'domain', 'tld', 'status', 'expires', 'days_until_expiration', 'renew_auto', 'renewable', 'estimated_annual_renewal_usd', 'estimated_due_next_' . $window . '_days_usd', 'project_bucket', 'recommended_action', 'notes' ]; $fh = fopen('php://temp', 'r+'); fputcsv($fh, $headers); foreach ($rows as $row) { $line = []; foreach ($headers as $h) $line[] = $row[$h] ?? ''; fputcsv($fh, $line); } rewind($fh); $csv = stream_get_contents($fh); fclose($fh); return $csv === false ? '' : $csv; } function ds_save_exports(array $rows): array { $dir = ds_root() . '/exports'; if (!is_dir($dir)) mkdir($dir, 0700, true); $stamp = date('Y-m-d-His'); $csvPath = $dir . '/godaddy-domain-inventory-' . $stamp . '.csv'; $jsonPath = $dir . '/godaddy-domain-inventory-' . $stamp . '.json'; file_put_contents($csvPath, ds_csv($rows)); file_put_contents($jsonPath, json_encode($rows, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); return ['csv' => basename($csvPath), 'json' => basename($jsonPath)]; }