import type { Domain, DomainWithMetrics } from '@/lib/types' import { domains as domainData } from '@/lib/domains/data' import type { SortOrder } from '@/lib/types/service' /** * Service for managing domain portfolio data and computing domain metrics. * * @remarks * This service provides comprehensive domain portfolio management: * - Automatic metric calculation (expiration, ownership duration) * - Flexible filtering and sorting * - Statistical analysis * - Renewal tracking * * All date calculations use UTC to ensure consistency across timezones. * * @example * ```ts * import { DomainService } from '@/lib/services' * * // Get all domains with computed metrics * const domains = DomainService.getAllDomainsEnriched() * * // Find expiring domains * const expiring = DomainService.filterDomains({ expiringSoon: true }) * * // Get portfolio statistics * const stats = DomainService.getDomainStats() * console.log(`Total domains: ${stats.total}`) * ``` * * @category Services * @public */ export class DomainService { private static computeExpirationDate(domain: Domain): Date { let expirationDate = new Date(domain.renewals[0].date) for (const renewal of domain.renewals) { const renewalDate = new Date(renewal.date) expirationDate = new Date(renewalDate) expirationDate.setFullYear(expirationDate.getFullYear() + renewal.years) } return expirationDate } private static computeRegistrationDate(domain: Domain): Date { return new Date(domain.renewals[0].date) } private static computeOwnershipDays(domain: Domain): number { const registrationDate = this.computeRegistrationDate(domain) const now = new Date() const diffTime = Math.abs(now.getTime() - registrationDate.getTime()) return Math.ceil(diffTime / (1000 * 60 * 60 * 24)) } private static computeOwnershipYears(domain: Domain): number { const days = this.computeOwnershipDays(domain) return Math.floor(days / 365.25) } private static computeOwnershipMonths(domain: Domain): number { const registrationDate = this.computeRegistrationDate(domain) const now = new Date() let months = (now.getFullYear() - registrationDate.getFullYear()) * 12 months += now.getMonth() - registrationDate.getMonth() if (now.getDate() < registrationDate.getDate()) { months-- } return Math.max(0, months) } private static computeDaysUntilExpiration(domain: Domain): number { const expirationDate = this.computeExpirationDate(domain) const now = new Date() const diffTime = expirationDate.getTime() - now.getTime() return Math.ceil(diffTime / (1000 * 60 * 60 * 24)) } private static computeRenewalProgressPercent(domain: Domain): number { const ownershipDays = this.computeOwnershipDays(domain) const registrationDate = this.computeRegistrationDate(domain) const expirationDate = this.computeExpirationDate(domain) const totalDays = Math.ceil((expirationDate.getTime() - registrationDate.getTime()) / (1000 * 60 * 60 * 24)) return Math.min(100, Math.max(0, (ownershipDays / totalDays) * 100)) } private static isExpiringSoon(domain: Domain, thresholdDays: number = 90): boolean { return this.computeDaysUntilExpiration(domain) <= thresholdDays } private static extractTLD(domain: Domain): string { const lastDot = domain.domain.lastIndexOf('.') return lastDot !== -1 ? domain.domain.substring(lastDot) : '' } /** * Enriches a domain with computed metrics including expiration dates, * ownership duration, and renewal progress. * * @param domain - The domain to enrich * @returns {DomainWithMetrics} Domain with added properties: * - `expirationDate: Date` - Computed expiration date * - `registrationDate: Date` - Initial registration date * - `ownershipDays: number` - Total days owned * - `ownershipYears: number` - Total years owned (floor) * - `ownershipMonths: number` - Total months owned * - `daysUntilExpiration: number` - Days until domain expires * - `renewalProgressPercent: number` - 0-100 percent through renewal period * - `isExpiringSoon: boolean` - Expires within 90 days * - `nextRenewalDate: Date` - Date of next renewal (same as expirationDate) * - `tld: string` - Top-level domain (e.g., '.com', '.dev') */ static enrichDomain(domain: Domain): DomainWithMetrics { const expirationDate = this.computeExpirationDate(domain) const registrationDate = this.computeRegistrationDate(domain) return { ...domain, expirationDate, registrationDate, ownershipDays: this.computeOwnershipDays(domain), ownershipYears: this.computeOwnershipYears(domain), ownershipMonths: this.computeOwnershipMonths(domain), daysUntilExpiration: this.computeDaysUntilExpiration(domain), renewalProgressPercent: this.computeRenewalProgressPercent(domain), isExpiringSoon: this.isExpiringSoon(domain), nextRenewalDate: expirationDate, tld: this.extractTLD(domain) } } /** * Retrieves all domains from the data store. * * @returns {Domain[]} Array of all domain specifications */ static getAllDomains(): Domain[] { return domainData } /** * Retrieves all domains with enriched metrics. * * @returns {DomainWithMetrics[]} Array of all domains with computed properties */ static getAllDomainsEnriched(): DomainWithMetrics[] { return domainData.map(domain => this.enrichDomain(domain)) } /** * Retrieves a single domain by its full domain name. * * @param domainName - The full domain name (e.g., 'aidxn.com') * @returns {DomainWithMetrics | null} Enriched domain or null if not found */ static getDomainByName(domainName: string): DomainWithMetrics | null { const domain = domainData.find(d => d.domain === domainName) return domain ? this.enrichDomain(domain) : null } /** * Filters domains based on multiple criteria. * * @param filters - Filter criteria * @param filters.status - Domain status to filter by ('active' | 'parked' | 'reserved') * @param filters.category - Domain category to filter by ('personal' | 'service' | 'project' | 'fun' | 'legacy') * @param filters.registrar - Registrar name to filter by * @param filters.expiringSoon - Filter by expiration status (within 90 days) * @param filters.autoRenew - Filter by auto-renewal setting * @returns {DomainWithMetrics[]} Array of enriched domains matching all specified filters * * @example * ```ts * const activeDomains = DomainService.filterDomains({ status: 'active' }) * const expiringDomains = DomainService.filterDomains({ expiringSoon: true }) * ``` */ static filterDomains( filters: { status?: Domain['status'] category?: Domain['category'] registrar?: string expiringSoon?: boolean autoRenew?: boolean } ): DomainWithMetrics[] { let filtered = domainData if (filters.status) { filtered = filtered.filter(d => d.status === filters.status) } if (filters.category) { filtered = filtered.filter(d => d.category === filters.category) } if (filters.registrar) { filtered = filtered.filter(d => d.registrar === filters.registrar) } if (filters.autoRenew !== undefined) { filtered = filtered.filter(d => d.autoRenew === filters.autoRenew) } const enriched = filtered.map(d => this.enrichDomain(d)) if (filters.expiringSoon !== undefined) { return enriched.filter(d => d.isExpiringSoon === filters.expiringSoon) } return enriched } /** * Sorts an array of domains by a specified property. * * @param domains - Array of domains to sort * @param sortBy - Property key to sort by (type-safe) * @param order - Sort direction: 'asc' (ascending) or 'desc' (descending), default: 'asc' * @returns {DomainWithMetrics[]} New sorted array (does not mutate original) * * @remarks * Creates a shallow copy before sorting to avoid mutating the original array. * Handles comparison of all value types (string, number, Date, boolean). * * @example * ```ts * const domains = DomainService.getAllDomainsEnriched() * * // Sort by expiration date (soonest first) * const byExpiration = DomainService.sortDomains(domains, 'daysUntilExpiration', 'asc') * * // Sort by domain name alphabetically * const byName = DomainService.sortDomains(domains, 'domain', 'asc') * * // Sort by ownership duration (longest first) * const byOwnership = DomainService.sortDomains(domains, 'ownershipDays', 'desc') * ``` * * @public */ static sortDomains( domains: DomainWithMetrics[], sortBy: keyof DomainWithMetrics, order: SortOrder = 'asc' ): DomainWithMetrics[] { return [...domains].sort((a, b) => { const aVal = a[sortBy] const bVal = b[sortBy] if (aVal === bVal) return 0 const comparison = aVal < bVal ? -1 : 1 return order === 'asc' ? comparison : -comparison }) } /** * Groups all domains by their category. * * @returns {Record} Object mapping each category to its domains * * @remarks * Returns an object with category keys ('personal', 'service', 'project', 'fun', 'legacy'), * each containing an array of enriched domains in that category. * * All categories are present in the result, even if empty. * * @example * ```ts * const grouped = DomainService.groupDomainsByCategory() * * console.log(`Personal domains: ${grouped.personal.length}`) * console.log(`Service domains: ${grouped.service.length}`) * * // Iterate through personal domains * grouped.personal.forEach(domain => { * console.log(`${domain.domain} - ${domain.description}`) * }) * * // Find most expensive category * const categoryCosts = Object.entries(grouped).map(([category, domains]) => ({ * category, * totalDomains: domains.length * })) * ``` * * @public */ static groupDomainsByCategory(): Record { const grouped: Record = { personal: [], service: [], project: [], fun: [], legacy: [] } const enriched = this.getAllDomainsEnriched() enriched.forEach(domain => { grouped[domain.category].push(domain) }) return grouped } /** * Computes statistics about my domain portfolio. * * @returns {DomainPortfolioStats} Statistics object with counts, breakdowns, and aggregates * * @remarks * Provides a complete statistical overview including: * - Total domain count * - Counts by status (active, parked, reserved) * - Domains expiring within 90 days * - Auto-renewal counts * - Breakdown by category * - Breakdown by registrar * * This method performs a single pass through the domain list for efficiency. * * @example * ```ts * const stats = DomainService.getDomainStats() * * console.log(`Portfolio Overview:`) * console.log(` Total: ${stats.total}`) * console.log(` Active: ${stats.active}`) * console.log(` Expiring Soon: ${stats.expiringSoon}`) * console.log(` Auto-Renew Enabled: ${stats.autoRenew}`) * * // Category breakdown * console.log(`\nBy Category:`) * Object.entries(stats.byCategory).forEach(([category, count]) => { * console.log(` ${category}: ${count}`) * }) * * // Registrar analysis * console.log(`\nBy Registrar:`) * Object.entries(stats.byRegistrar) * .sort(([,a], [,b]) => b - a) * .forEach(([registrar, count]) => { * console.log(` ${registrar}: ${count}`) * }) * ``` * * @public */ static getDomainStats(): DomainPortfolioStats { const enriched = this.getAllDomainsEnriched() return { total: enriched.length, active: enriched.filter(d => d.status === 'active').length, parked: enriched.filter(d => d.status === 'parked').length, reserved: enriched.filter(d => d.status === 'reserved').length, expiringSoon: enriched.filter(d => d.isExpiringSoon).length, autoRenew: enriched.filter(d => d.autoRenew).length, byCategory: { personal: enriched.filter(d => d.category === 'personal').length, service: enriched.filter(d => d.category === 'service').length, project: enriched.filter(d => d.category === 'project').length, fun: enriched.filter(d => d.category === 'fun').length, legacy: enriched.filter(d => d.category === 'legacy').length, }, byRegistrar: enriched.reduce((acc, d) => { acc[d.registrar] = (acc[d.registrar] || 0) + 1 return acc }, {} as Record) } } } /** * Domain portfolio statistics result. * * @remarks * Breakdown of domain metrics returned by {@link DomainService.getDomainStats}. * * @example * ```ts * const stats: DomainPortfolioStats = DomainService.getDomainStats() * ``` * * @category Types * @public */ export interface DomainPortfolioStats { /** Total number of domains in portfolio */ total: number /** Number of active domains */ active: number /** Number of parked domains */ parked: number /** Number of reserved domains */ reserved: number /** Number of domains expiring within 90 days */ expiringSoon: number /** Number of domains with auto-renew enabled */ autoRenew: number /** Domain counts by category */ byCategory: { personal: number service: number project: number fun: number legacy: number } /** Domain counts by registrar */ byRegistrar: Record }