chahinebrini cddc4d0f26 feat(profile): DiGA-Demographics + Pro-Trial-Reward + 7 Profile-Endpoints
Schema:
- 8 neue Profile-Felder fuer DiGA-Demographics (birthYear/gender/maritalStatus/
  profession/bundesland/city + 2 consent-stamps demographicsConsentAt/
  demographicsWithdrawnAt)
- 4 Pro-Trial-Felder (proTrialStartedAt/ExpiresAt/Source/UsedAt) — Free-User
  bekommen 1 Woche Pro als Reward fuer DiGA-Daten-Pflege (siehe
  project_demographic_pro_trial_reward.md)
- lyra_voice_id (Legend-only Voice-Picker)
- diga_banner_dismissed_at (server-side persistence ueber Re-Install)
- last_install_at (Streak-Logic survives Re-Install)
- Migration 20260507_profile_demographics_and_trial: alle Felder optional,
  keine Backfill-Logik notwendig

Endpoints (alle auth-protected, scope=me):
- GET /api/profile/me/sos-insights
- GET /api/profile/me/cooldown-history
- GET /api/profile/me/approved-domains
- POST /api/profile/me/install-event (track app re-installs)
- POST /api/profile/me/diga-banner-dismiss
- PATCH /api/profile/me/demographics (consent-stamp + re-grant-after-withdrawal in tx)
- DELETE /api/profile/me/demographics (DSGVO right-to-be-forgotten)

Plugin:
- pro-trial-expiry-cron: 6h-Interval, conservative-fallback (revoke nur wenn
  kein stripeSubId), 60s initial-delay damit Server-boot nicht blockiert wird

Tests:
- vitest config + erste Test-Files (test-infrastructure setup)

Memory:
- feedback_demographics_user_initiated.md (Lyra darf NIE extrahieren)
- project_demographic_pro_trial_reward.md (Pro-Trial-Reward-Mechanik)
- project_profile_page_design.md (UI-Showpiece, eigene/fremde-Ansicht streng getrennt)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 21:14:06 +02:00

110 lines
3.0 KiB
TypeScript

/**
* GET /api/profile/me/sos-insights
*
* Aggregierte SOS-Stats der letzten 30 Tage für Profile-Page-LyraInsightsCard
* (project_profile_page_design.md §4).
*
* Source: rebreak.sos_sessions (existiert, schema.prisma `model SosSession`)
*
* Heuristik für `helpedBy`:
* - breathing: SUM(breathing_count > 0 ? 1 : 0) → anzahl sessions mit min. 1 Atemübung
* - game: SUM(jsonb_array_length(games_played) > 0)
* - talk: SUM(messages.length > 4) → mehr als 2 user-message-cycles
*
* Response shape (siehe PROFILE_PAGE_DESIGN.md):
* {
* last30Days: { sessions, overcome, overcomeRate },
* helpedBy: { breathing, game, talk, other },
* topEmotion: string | null,
* topEmotionFromUrgeLogs: boolean,
* }
*
* Empty-State: { sessions: 0, ...everything: 0, topEmotion: null }
* → Frontend rendert "noch keine SOS-Session"-EmptyState.
*/
import { requireUser } from "../../../utils/auth";
import { usePrisma } from "../../../utils/prisma";
const WINDOW_DAYS = 30;
type Helper = "breathing" | "game" | "talk" | "other";
export default defineEventHandler(async (event) => {
const user = await requireUser(event);
const db = usePrisma();
const since = new Date(Date.now() - WINDOW_DAYS * 24 * 60 * 60 * 1000);
const sessions = await db.sosSession.findMany({
where: { userId: user.id, startedAt: { gte: since } },
select: {
breathingCount: true,
gamesPlayed: true,
messages: true,
wasOvercome: true,
},
});
let overcome = 0;
const helpedBy: Record<Helper, number> = {
breathing: 0,
game: 0,
talk: 0,
other: 0,
};
for (const s of sessions) {
if (s.wasOvercome) overcome++;
let countedHelper = false;
if (s.breathingCount > 0) {
helpedBy.breathing++;
countedHelper = true;
}
const games = Array.isArray(s.gamesPlayed) ? s.gamesPlayed : [];
if (games.length > 0) {
helpedBy.game++;
countedHelper = true;
}
const msgs = Array.isArray(s.messages) ? s.messages : [];
if (msgs.length > 4) {
helpedBy.talk++;
countedHelper = true;
}
if (!countedHelper) helpedBy.other++;
}
// topEmotion via urge_logs (letzte 30 Tage) — most-frequent emotion
let topEmotion: string | null = null;
try {
const grouped = await db.urgeLog.groupBy({
by: ["emotion"],
where: { userId: user.id, timestamp: { gte: since } },
_count: { emotion: true },
orderBy: { _count: { emotion: "desc" } },
take: 1,
});
topEmotion = grouped[0]?.emotion ?? null;
} catch (e) {
// urge_logs query may fail on edge schemas — non-fatal
console.error("[sos-insights] urge_logs query failed (non-fatal):", e);
}
const sessionsCount = sessions.length;
const overcomeRate =
sessionsCount > 0 ? Math.round((overcome / sessionsCount) * 100) / 100 : 0;
return {
success: true,
data: {
last30Days: {
sessions: sessionsCount,
overcome,
overcomeRate,
},
helpedBy,
topEmotion,
windowDays: WINDOW_DAYS,
},
};
});