diff --git a/backend/src/services/monitoring-service.ts b/backend/src/services/monitoring-service.ts index 75317a1..f4801af 100644 --- a/backend/src/services/monitoring-service.ts +++ b/backend/src/services/monitoring-service.ts @@ -8,6 +8,14 @@ export interface SubscriptionMetrics { total_monthly_revenue: number; } +export interface TrialMetrics { + active_trials: number; + trials_expiring_in_7_days: number; + saved_by_syncro: number; // trials cancelled before auto-charge after receiving a reminder + intentional_conversions: number; + automatic_conversions: number; +} + export interface RenewalMetrics { total_delivery_attempts: number; success_rate: number; @@ -164,6 +172,52 @@ export class MonitoringService { }; })()); } + + /** + * Get trial-specific metrics including "saved by SYNCRO" count + */ + async getTrialMetrics(): Promise { + try { + const now = new Date().toISOString(); + const in7Days = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); + + const [ + { count: activeTrials }, + { count: expiringTrials }, + { data: conversionEvents }, + ] = await Promise.all([ + supabase + .from('subscriptions') + .select('*', { count: 'exact', head: true }) + .eq('is_trial', true) + .in('status', ['active', 'trial']) + .gt('trial_ends_at', now), + supabase + .from('subscriptions') + .select('*', { count: 'exact', head: true }) + .eq('is_trial', true) + .in('status', ['active', 'trial']) + .gt('trial_ends_at', now) + .lte('trial_ends_at', in7Days), + supabase + .from('trial_conversion_events') + .select('conversion_type, saved_by_syncro'), + ]); + + const events = conversionEvents ?? []; + + return { + active_trials: activeTrials ?? 0, + trials_expiring_in_7_days: expiringTrials ?? 0, + saved_by_syncro: events.filter((e) => e.saved_by_syncro).length, + intentional_conversions: events.filter((e) => e.conversion_type === 'intentional').length, + automatic_conversions: events.filter((e) => e.conversion_type === 'automatic').length, + }; + } catch (error) { + logger.error('Error fetching trial metrics:', error); + throw error; + } + } } export const monitoringService = new MonitoringService(); diff --git a/backend/src/services/reminder-engine.ts b/backend/src/services/reminder-engine.ts index 4ce4e30..f803b1f 100644 --- a/backend/src/services/reminder-engine.ts +++ b/backend/src/services/reminder-engine.ts @@ -226,7 +226,9 @@ export class ReminderEngine { return; } - const renewalDate = subscription.active_until || new Date().toISOString(); + const renewalDate = reminder.reminder_type === 'trial_expiry' + ? (subscription.trial_ends_at || new Date().toISOString()) + : (subscription.active_until || new Date().toISOString()); const payload: NotificationPayload = { title: `${subscription.name} Renewal Reminder`, body: `${subscription.name} will renew in ${reminder.days_before} day${reminder.days_before > 1 ? 's' : ''}`, diff --git a/backend/src/types/reminder.ts b/backend/src/types/reminder.ts index ec1a50e..22c3afd 100644 --- a/backend/src/types/reminder.ts +++ b/backend/src/types/reminder.ts @@ -52,6 +52,12 @@ export interface Subscription { credit_card_required: boolean; created_at: string; updated_at: string; + // Trial tracking + is_trial: boolean; + trial_ends_at: string | null; + trial_converts_to_price: number | null; + credit_card_required: boolean; + website_url: string | null; } export interface UserProfile { diff --git a/backend/src/types/subscription.ts b/backend/src/types/subscription.ts index c267205..84fa5ce 100644 --- a/backend/src/types/subscription.ts +++ b/backend/src/types/subscription.ts @@ -28,6 +28,11 @@ export interface Subscription { paused_at: string | null; resume_at: string | null; pause_reason: string | null; + // Trial tracking fields + is_trial: boolean; + trial_ends_at: string | null; + trial_converts_to_price: number | null; + credit_card_required: boolean; } export interface SubscriptionCreateInput { @@ -47,6 +52,11 @@ export interface SubscriptionCreateInput { visibility?: 'private' | 'team'; tags?: string[]; email_account_id?: string; + // Trial fields + is_trial?: boolean; + trial_ends_at?: string; + trial_converts_to_price?: number; + credit_card_required?: boolean; } export interface SubscriptionUpdateInput { @@ -67,6 +77,11 @@ export interface SubscriptionUpdateInput { paused_at?: string | null; resume_at?: string | null; pause_reason?: string | null; + // Trial fields + is_trial?: boolean; + trial_ends_at?: string | null; + trial_converts_to_price?: number | null; + credit_card_required?: boolean; } /** Allowlist of fields a user is permitted to update. diff --git a/client/components/pages/subscriptions.tsx b/client/components/pages/subscriptions.tsx index cf93c9e..2efe6c6 100644 --- a/client/components/pages/subscriptions.tsx +++ b/client/components/pages/subscriptions.tsx @@ -166,6 +166,11 @@ export default function SubscriptionsPage({ const hasNoSubscriptions = !subscriptions || subscriptions.length === 0 const hasNoResults = filtered.length === 0 && subscriptions && subscriptions.length > 0 + // Active trials sorted by urgency (soonest expiry first) + const activeTrials = (subscriptions || []) + .filter((s: any) => s.isTrial && s.trialEndsAt) + .sort((a: any, b: any) => new Date(a.trialEndsAt).getTime() - new Date(b.trialEndsAt).getTime()) + if (hasNoSubscriptions) { return ( + {/* Active Trials Section */} + {activeTrials.length > 0 && ( +
+

+

+
+ {activeTrials.map((sub: any) => { + const daysLeft = Math.ceil((new Date(sub.trialEndsAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24)) + const urgencyColor = daysLeft <= 1 ? "text-red-600" : daysLeft <= 3 ? "text-orange-500" : "text-yellow-600" + const urgencyBg = daysLeft <= 1 ? (darkMode ? "bg-red-900/20 border-red-700" : "bg-red-50 border-red-200") : daysLeft <= 3 ? (darkMode ? "bg-orange-900/20 border-orange-700" : "bg-orange-50 border-orange-200") : (darkMode ? "bg-yellow-900/20 border-yellow-700" : "bg-yellow-50 border-yellow-200") + return ( +
+
+ +
+
+

{sub.name}

+ Trial +
+

+ {daysLeft === 0 ? "Expires TODAY at midnight" : `Expires in ${daysLeft} day${daysLeft > 1 ? "s" : ""}`} +

+ {sub.priceAfterTrial && ( +

+ Auto-charges ${sub.priceAfterTrial}/{sub.billingCycle || "month"} after trial +

+ )} +
+
+
+
+

+ {daysLeft === 0 ? "Today" : `${daysLeft}d`} +

+

remaining

+
+ {onCancelTrial && ( + + )} + {onConvertTrial && ( + + )} +
+
+ ) + })} +
+
+ )} + {/* Subscriptions List */} {!hasNoResults && ( <> @@ -767,6 +845,26 @@ export function SubscriptionCard({
+ {sub.isTrial && onCancelTrial && ( + + )} + {sub.isTrial && onConvertTrial && ( + + )}