419 lines
No EOL
14 KiB
TypeScript
419 lines
No EOL
14 KiB
TypeScript
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<Domain['category'], DomainWithMetrics[]>} 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<Domain['category'], DomainWithMetrics[]> {
|
|
const grouped: Record<Domain['category'], DomainWithMetrics[]> = {
|
|
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<string, number>)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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<string, number>
|
|
} |