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>
100 lines
2.7 KiB
TypeScript
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);
|
|
}
|
|
}
|