import type { WidgetOptions, WidgetSamplerLog } from "../domain"; import type { BlockReason, WidgetDisplayResult } from "../domain/WidgetDisplayResult"; import type { WidgetStateManager } from "./WidgetStateManager"; export class WidgetValidationService { private stateManager: WidgetStateManager; constructor(stateManager: WidgetStateManager) { this.stateManager = stateManager; } async shouldDisplayForTransactionAlreadyAnswered(transactionId?: string): Promise { if (!transactionId) return { canDisplay: true }; try { const alreadyAnswered = await this.stateManager.hasAnsweredTransaction(transactionId); if (alreadyAnswered) { return { canDisplay: false, blockReason: "BLOCKED_BY_TRANSACTION_ALREADY_ANSWERED" }; } } catch (error) { console.error("Error reading answered transaction log:", error); } return { canDisplay: true }; } async shouldDisplayWidget(widgetOptions: WidgetOptions, transactionId?: string): Promise { if (widgetOptions.enabled === false) { return { canDisplay: false, blockReason: "BLOCKED_BY_DISABLED" }; } const now = Date.now(); const userLog = await this.resolveUserLog(transactionId, now); const maxAttemptsResult = this.checkMaxAttempts(widgetOptions, userLog); if (!maxAttemptsResult.canDisplay) return maxAttemptsResult; const intervalResult = this.checkIntervals(widgetOptions, userLog, now); if (!intervalResult.canDisplay) return intervalResult; const samplingResult = this.checkSampling(widgetOptions); if (!samplingResult.canDisplay) return samplingResult; return { canDisplay: true }; } private async resolveUserLog(transactionId?: string, now: number = Date.now()): Promise { let userLog = await this.getLog(); if (!userLog.lastFirstAccess) userLog = await this.initializeFirstAccess(userLog, now); if (transactionId && userLog.lastExperienceId !== transactionId) { userLog = { ...userLog, attempts: 0, lastExperienceId: transactionId }; await this.saveLog(userLog); } return userLog; } private checkMaxAttempts(widgetOptions: WidgetOptions, userLog: WidgetSamplerLog): WidgetDisplayResult { if (widgetOptions.maxAttemptsAfterDismiss !== undefined && widgetOptions.maxAttemptsAfterDismiss > 0) { if (userLog.attempts >= widgetOptions.maxAttemptsAfterDismiss) { return { canDisplay: false, blockReason: "BLOCKED_BY_MAX_ATTEMPTS" }; } } return { canDisplay: true }; } private checkIntervals(widgetOptions: WidgetOptions, userLog: WidgetSamplerLog, now: number): WidgetDisplayResult { const dayInMilliseconds = 86400000; const checks: Array<{ timestamp: number | undefined; waitDays: number | undefined; blockReason: BlockReason; }> = [ { timestamp: userLog.lastDisplayAttempt, waitDays: widgetOptions.waitDaysAfterWidgetDisplayAttempt, blockReason: "BLOCKED_BY_WIDGET_DISPLAY_ATTEMPT_INTERVAL", }, { timestamp: userLog.lastFirstAccess, waitDays: widgetOptions.waitDaysAfterWidgetFirstAccess, blockReason: "BLOCKED_BY_WIDGET_FIRST_ACCESS_INTERVAL", }, { timestamp: userLog.lastDisplay, waitDays: widgetOptions.waitDaysAfterWidgetDisplay, blockReason: "BLOCKED_BY_WIDGET_DISPLAY_INTERVAL", }, { timestamp: userLog.lastDismiss, waitDays: widgetOptions.waitDaysAfterWidgetDismiss, blockReason: "BLOCKED_BY_WIDGET_DISMISS_INTERVAL", }, { timestamp: userLog.lastSubmit, waitDays: widgetOptions.waitDaysAfterWidgetSubmit, blockReason: "BLOCKED_BY_WIDGET_SUBMIT_INTERVAL", }, { timestamp: userLog.lastPartialSubmit, waitDays: widgetOptions.waitDaysAfterWidgetPartialSubmit, blockReason: "BLOCKED_BY_WIDGET_PARTIAL_SUBMIT_INTERVAL", }, ]; for (const check of checks) { if (this.isWithinInterval(check.timestamp, check.waitDays, now, dayInMilliseconds)) { return { canDisplay: false, blockReason: check.blockReason }; } } return { canDisplay: true }; } private checkSampling(widgetOptions: WidgetOptions): WidgetDisplayResult { if (widgetOptions.samplingPercentage !== undefined && widgetOptions.samplingPercentage >= 0 && widgetOptions.samplingPercentage < 100) { const random = Math.random() * 100; if (random >= widgetOptions.samplingPercentage) { return { canDisplay: false, blockReason: "BLOCKED_BY_SAMPLING" }; } } return { canDisplay: true }; } private async saveLog(userLog: WidgetSamplerLog): Promise { try { await this.stateManager.saveLogs(userLog); } catch (error) { console.error("Error writing widget log:", error); } } private async initializeFirstAccess(userLog: WidgetSamplerLog, now: number): Promise { const updatedLog = { ...userLog, lastFirstAccess: now }; await this.saveLog(updatedLog); return updatedLog; } private async getLog(): Promise { try { return await this.stateManager.getLogs(); } catch (error) { console.error("Error reading widget log:", error); return { attempts: 0, answeredTransactionIds: [] }; } } private isWithinInterval(timestamp: number | undefined, waitDays: number | undefined, now: number, dayInMilliseconds: number): boolean { if (!timestamp || timestamp <= 0) return false; if (!waitDays || waitDays <= 0) return false; const elapsed = now - timestamp; return elapsed < waitDays * dayInMilliseconds; } }