rebreak-monorepo/backend/server/plugins/pro-trial-expiry-cron.ts
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

100 lines
2.7 KiB
TypeScript

/**
* Pro-Trial-Expiry-Cron
*
* Läuft alle 6h. Findet alle Profiles deren `pro_trial_expires_at` abgelaufen
* ist und die noch auf `plan = 'pro'` stehen.
*
* Revoke-Logik (CONSERVATIVE-Fallback):
* - Wenn `stripeSubId` IS NULL → revoke (zurück auf 'free')
* - Wenn `stripeSubId` gesetzt → NICHT revoken (User hat während Trial
* upgraded ODER hatte schon Stripe-Abo). Stripe-Webhook hält den State,
* der Trial läuft formal aus, der Abo-Plan überdauert.
*
* TODO (siehe project_demographic_pro_trial_reward.md): Stripe-API-Sync
* (subscription.status='active') als zusätzliche Sicherheit. Aktuell
* stripeSubId-presence als Indikator (kann stale sein). Wenn das Probleme
* macht: stripe.subscriptions.retrieve(subId) hinzufügen.
*
* Memory: project_demographic_pro_trial_reward.md
*/
import { consola } from "consola";
const SIX_HOURS = 6 * 60 * 60 * 1000;
export default defineNitroPlugin((nitro) => {
if (import.meta.dev) {
consola.info("[pro-trial-cron] Skipping cron in dev mode");
return;
}
consola.info("[pro-trial-cron] Starting (6h interval)");
// Initial run after 60s (let server boot)
const initialTimer = setTimeout(() => {
runRevoke().catch(() => {});
}, 60_000);
const interval = setInterval(() => {
runRevoke().catch(() => {});
}, SIX_HOURS);
nitro.hooks.hook("close", () => {
clearTimeout(initialTimer);
clearInterval(interval);
});
});
async function runRevoke() {
try {
const db = usePrisma();
const now = new Date();
// Find expired trials still on 'pro' (or 'standard' legacy)
const expired = await db.profile.findMany({
where: {
proTrialExpiresAt: { lt: now, not: null },
plan: { in: ["pro", "standard"] },
},
select: {
id: true,
stripeSubId: true,
proTrialExpiresAt: true,
},
});
if (expired.length === 0) {
consola.info("[pro-trial-cron] No expired trials");
return;
}
let revoked = 0;
let kept = 0;
for (const p of expired) {
// CONSERVATIVE: any stripeSubId presence keeps the user on pro
if (p.stripeSubId) {
kept++;
continue;
}
try {
await db.profile.update({
where: { id: p.id },
data: { plan: "free" },
});
revoked++;
} catch (err: any) {
consola.error(
`[pro-trial-cron] Failed to revoke user ${p.id}:`,
err?.message ?? err,
);
}
}
consola.success(
`[pro-trial-cron] revoked ${revoked} users from trial-pro, kept ${kept} (Stripe-Abo)`,
);
} catch (err: any) {
consola.error("[pro-trial-cron] run failed:", err?.message ?? err);
}
}