/** * Configuration loader for TimeSafari test apps * * Loads configuration from JSON files and provides typed access * to TimeSafari-specific settings, Endorser.ch API endpoints, * and test data. * * @author Matthew Raymer * @version 1.0.0 */ export interface TimeSafariConfig { timesafari: { appId: string; appName: string; version: string; description: string; }; endorser: { baseUrl: string; apiVersion: string; endpoints: { offers: string; offersToPlans: string; plansLastUpdated: string; notificationsBundle: string; }; authentication: { type: string; token: string; headers: Record; }; pagination: { defaultLimit: number; maxLimit: number; hitLimitThreshold: number; }; }; notificationTypes: { offers: { enabled: boolean; types: string[]; }; projects: { enabled: boolean; types: string[]; }; people: { enabled: boolean; types: string[]; }; items: { enabled: boolean; types: string[]; }; }; scheduling: { contentFetch: { schedule: string; time: string; description: string; }; userNotification: { schedule: string; time: string; description: string; }; }; testData: { userDid: string; starredPlanIds: string[]; lastKnownOfferId: string; lastKnownPlanId: string; mockOffers: any[]; mockProjects: any[]; }; callbacks: { offers: { enabled: boolean; localHandler: string; }; projects: { enabled: boolean; localHandler: string; }; people: { enabled: boolean; localHandler: string; }; items: { enabled: boolean; localHandler: string; }; communityAnalytics: { enabled: boolean; endpoint: string; headers: Record; }; }; observability: { enableLogging: boolean; logLevel: string; enableMetrics: boolean; enableHealthChecks: boolean; }; } /** * Configuration loader class */ export class ConfigLoader { private static instance: ConfigLoader; private config: TimeSafariConfig | null = null; private constructor() {} /** * Get singleton instance */ public static getInstance(): ConfigLoader { if (!ConfigLoader.instance) { ConfigLoader.instance = new ConfigLoader(); } return ConfigLoader.instance; } /** * Load configuration from JSON file */ public async loadConfig(): Promise { if (this.config) { return this.config; } try { // In a real app, this would fetch from a config file // For test apps, we'll use a hardcoded config this.config = { timesafari: { appId: "app.timesafari.test", appName: "TimeSafari Test", version: "1.0.0", description: "Test app for TimeSafari Daily Notification Plugin integration" }, endorser: { baseUrl: "http://localhost:3001", apiVersion: "v2", endpoints: { offers: "/api/v2/report/offers", offersToPlans: "/api/v2/report/offersToPlansOwnedByMe", plansLastUpdated: "/api/v2/report/plansLastUpdatedBetween", notificationsBundle: "/api/v2/report/notifications/bundle" }, authentication: { type: "Bearer", token: "test-jwt-token-12345", headers: { "Authorization": "Bearer test-jwt-token-12345", "Content-Type": "application/json", "X-Privacy-Level": "user-controlled" } }, pagination: { defaultLimit: 50, maxLimit: 100, hitLimitThreshold: 50 } }, notificationTypes: { offers: { enabled: true, types: [ "new_to_me", "changed_to_me", "new_to_projects", "changed_to_projects", "new_to_favorites", "changed_to_favorites" ] }, projects: { enabled: true, types: [ "local_new", "local_changed", "content_interest_new", "favorited_changed" ] }, people: { enabled: true, types: [ "local_new", "local_changed", "content_interest_new", "favorited_changed", "contacts_changed" ] }, items: { enabled: true, types: [ "local_new", "local_changed", "favorited_changed" ] } }, scheduling: { contentFetch: { schedule: "0 8 * * *", time: "08:00", description: "8 AM daily - fetch community updates" }, userNotification: { schedule: "0 9 * * *", time: "09:00", description: "9 AM daily - notify users of community updates" } }, testData: { userDid: "did:example:testuser123", starredPlanIds: [ "plan-community-garden", "plan-local-food", "plan-sustainability" ], lastKnownOfferId: "01HSE3R9MAC0FT3P3KZ382TWV7", lastKnownPlanId: "01HSE3R9MAC0FT3P3KZ382TWV8", mockOffers: [ { jwtId: "01HSE3R9MAC0FT3P3KZ382TWV7", handleId: "offer-web-dev-001", offeredByDid: "did:example:offerer123", recipientDid: "did:example:testuser123", objectDescription: "Web development services for community project", unit: "USD", amount: 1000, amountGiven: 500, amountGivenConfirmed: 250 } ], mockProjects: [ { plan: { jwtId: "01HSE3R9MAC0FT3P3KZ382TWV8", handleId: "plan-community-garden", name: "Community Garden Project", description: "Building a community garden for local food production", issuerDid: "did:example:issuer123", agentDid: "did:example:agent123" }, wrappedClaimBefore: null } ] }, callbacks: { offers: { enabled: true, localHandler: "handleOffersNotification" }, projects: { enabled: true, localHandler: "handleProjectsNotification" }, people: { enabled: true, localHandler: "handlePeopleNotification" }, items: { enabled: true, localHandler: "handleItemsNotification" }, communityAnalytics: { enabled: true, endpoint: "http://localhost:3001/api/analytics/community-events", headers: { "Content-Type": "application/json", "X-Privacy-Level": "aggregated" } } }, observability: { enableLogging: true, logLevel: "debug", enableMetrics: true, enableHealthChecks: true } }; return this.config; } catch (error) { console.error('Failed to load configuration:', error); throw error; } } /** * Get configuration */ public getConfig(): TimeSafariConfig { if (!this.config) { throw new Error('Configuration not loaded. Call loadConfig() first.'); } return this.config; } /** * Get Endorser.ch API URL for a specific endpoint */ public getEndorserUrl(endpoint: keyof TimeSafariConfig['endorser']['endpoints']): string { const config = this.getConfig(); return `${config.endorser.baseUrl}${config.endorser.endpoints[endpoint]}`; } /** * Get authentication headers */ public getAuthHeaders(): Record { const config = this.getConfig(); return config.endorser.authentication.headers; } /** * Get test data */ public getTestData() { const config = this.getConfig(); return config.testData; } /** * Get notification types for a specific category */ public getNotificationTypes(category: keyof TimeSafariConfig['notificationTypes']) { const config = this.getConfig(); return config.notificationTypes[category]; } } /** * Logger utility for test apps */ export class TestLogger { private logLevel: string; private logs: string[] = []; constructor(logLevel: string = 'debug') { this.logLevel = logLevel; } private shouldLog(level: string): boolean { const levels = ['error', 'warn', 'info', 'debug']; return levels.indexOf(level) <= levels.indexOf(this.logLevel); } private addToLogs(level: string, message: string, data?: any): void { const timestamp = new Date().toISOString(); const logEntry = `[${timestamp}] [${level.toUpperCase()}] ${message}`; this.logs.push(logEntry); // Keep only last 1000 logs to prevent memory issues if (this.logs.length > 1000) { this.logs = this.logs.slice(-1000); } } public debug(message: string, data?: any) { if (this.shouldLog('debug')) { console.log(`[DEBUG] ${message}`, data || ''); this.addToLogs('debug', message, data); } } public info(message: string, data?: any) { if (this.shouldLog('info')) { console.log(`[INFO] ${message}`, data || ''); this.addToLogs('info', message, data); } } public warn(message: string, data?: any) { if (this.shouldLog('warn')) { console.warn(`[WARN] ${message}`, data || ''); this.addToLogs('warn', message, data); } } public error(message: string, data?: any) { if (this.shouldLog('error')) { console.error(`[ERROR] ${message}`, data || ''); this.addToLogs('error', message, data); } } public getLogs(): string[] { return [...this.logs]; } public clearLogs(): void { this.logs = []; } public exportLogs(): string { return this.logs.join('\n'); } } /** * Mock DailyNotificationService for test apps */ export class MockDailyNotificationService { private config: TimeSafariConfig; private logger: TestLogger; private isInitialized = false; constructor(config: TimeSafariConfig) { this.config = config; this.logger = new TestLogger(config.observability.logLevel); } /** * Initialize the service */ public async initialize(): Promise { this.logger.info('Initializing Mock DailyNotificationService'); this.isInitialized = true; this.logger.info('Mock DailyNotificationService initialized successfully'); } /** * Schedule dual notification (content fetch + user notification) */ public async scheduleDualNotification(config: any): Promise { if (!this.isInitialized) { throw new Error('Service not initialized'); } this.logger.info('Scheduling dual notification', config); // Simulate content fetch if (config.contentFetch?.enabled) { await this.simulateContentFetch(config.contentFetch); } // Simulate user notification if (config.userNotification?.enabled) { await this.simulateUserNotification(config.userNotification); } this.logger.info('Dual notification scheduled successfully'); } /** * Register callback */ public async registerCallback(name: string, callback: Function): Promise { this.logger.info(`Registering callback: ${name}`); // In a real implementation, this would register the callback this.logger.info(`Callback ${name} registered successfully`); } /** * Get dual schedule status */ public async getDualScheduleStatus(): Promise { return { contentFetch: { enabled: true, lastFetch: new Date().toISOString(), nextFetch: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() }, userNotification: { enabled: true, lastNotification: new Date().toISOString(), nextNotification: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() } }; } /** * Cancel all notifications */ public async cancelAllNotifications(): Promise { this.logger.info('Cancelling all notifications'); this.logger.info('All notifications cancelled successfully'); } /** * Simulate content fetch using Endorser.ch API patterns */ private async simulateContentFetch(config: any): Promise { this.logger.info('Simulating content fetch from Endorser.ch API'); try { // Simulate parallel API requests const testData = this.config.testData; // Mock offers to person const offersToPerson = { data: testData.mockOffers, hitLimit: false }; // Mock offers to projects const offersToProjects = { data: [], hitLimit: false }; // Mock starred project changes const starredChanges = { data: testData.mockProjects, hitLimit: false }; this.logger.info('Content fetch simulation completed', { offersToPerson: offersToPerson.data.length, offersToProjects: offersToProjects.data.length, starredChanges: starredChanges.data.length }); // Call success callback if provided if (config.callbacks?.onSuccess) { await config.callbacks.onSuccess({ offersToPerson, offersToProjects, starredChanges }); } } catch (error) { this.logger.error('Content fetch simulation failed', error); if (config.callbacks?.onError) { await config.callbacks.onError(error); } } } /** * Simulate user notification */ private async simulateUserNotification(config: any): Promise { this.logger.info('Simulating user notification', { title: config.title, body: config.body, time: config.schedule }); this.logger.info('User notification simulation completed'); } }