FRE-622: Wire analytics services to tRPC API layer with comprehensive router

Create analytics-router.ts with ~30 tRPC endpoints for KPI management, alert
rules, scheduled reports, cohort analysis, and NPS survey integration.
Register router in index.ts under 'analytics' namespace. Fix pre-existing
bugs in service files: snake_case to camelCase conversion, missing non-null
assertions, and incorrect DB access patterns.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Paperclip Agent
2026-04-27 22:55:15 -04:00
committed by Michael Freno
parent bc897f8845
commit 408d94f731
6 changed files with 461 additions and 20 deletions

View File

@@ -40,7 +40,7 @@ export async function createCohort(
};
const result = await db.insert(cohorts).values(cohort).returning();
return result[0];
return result[0]!;
}
export async function addCohortMember(
@@ -103,7 +103,7 @@ export async function getCohortAnalysis(
};
return {
cohort,
cohort: cohort!,
retention,
metrics,
};

View File

@@ -42,7 +42,7 @@ export async function submitNPSResponse(
};
const result = await db.insert(npsResponses).values(response).returning();
return result[0];
return result[0]!;
}
export async function calculateNPS(
@@ -122,7 +122,7 @@ export async function getNPSOverTime(
const grouped: Record<string, NPSResponse[]> = {};
for (const response of responses) {
const date = response.created_at;
const date = response.createdAt;
const key =
granularity === "weekly"
? getWeekKey(date)
@@ -186,7 +186,7 @@ export function generateNPSSurveyInAppPrompt(): { question: string; scale: strin
function getWeekKey(date: Date): string {
const start = new Date(date);
start.setDate(start.getDate() - start.getDay());
return start.toISOString().split("T")[0];
return start.toISOString().split("T")[0]!;
}
function getMonthKey(date: Date): string {

View File

@@ -178,7 +178,7 @@ export async function createScheduledReport(
lastRunAt: null,
nextRunAt: computeNextRun(input.schedule),
}).returning();
return result[0];
return result[0]!;
}
export async function getActiveScheduledReports(

View File

@@ -134,7 +134,7 @@ export async function sendSlackAlert(
type: "header",
text: {
type: "plain_text",
text: `🚨 KPI Alert: ${alert.kpi_key}`,
text: `🚨 KPI Alert: ${alert.kpiKey}`,
emoji: true,
},
},
@@ -147,7 +147,7 @@ export async function sendSlackAlert(
},
{
type: "mrkdwn",
text: `*Current Value:*\n${alert.kpi_value.toFixed(2)}`,
text: `*Current Value:*\n${alert.kpiValue.toFixed(2)}`,
},
{
type: "mrkdwn",
@@ -155,7 +155,7 @@ export async function sendSlackAlert(
},
{
type: "mrkdwn",
text: `*Time:*\n${new Date(alert.created_at?.getTime() ?? Date.now()).toISOString()}`,
text: `*Time:*\n${new Date(alert.createdAt.getTime()).toISOString()}`,
},
],
},
@@ -196,22 +196,14 @@ export async function sendSlackAlert(
}
async function markAlertSent(alertId: number): Promise<void> {
const db = await getDb();
if (db) {
try {
const { db } = await import("../../db/config/migrations");
await db
.update(alerts)
.set({ wasSent: true, sentAt: new Date() })
.where(eq(alerts.id, alertId));
}
}
async function getDb(): Promise<DrizzleDB | undefined> {
try {
const { createDatabaseManager } = await import("../../db/config/database");
const manager = createDatabaseManager();
return manager.getDb();
} catch {
return undefined;
console.error("Failed to mark alert as sent:", alertId);
}
}