fn ($q) => $q->where('is_active', true), 'donations as cancelled_count' => fn ($q) => $q->where('is_active', false), ])->get(); $result = []; foreach ($campaigns as $c) { $donationIds = $c->donations()->pluck('id'); if ($donationIds->isEmpty()) { $result[] = [ 'campaign' => $c, 'subscribers' => 0, 'active' => 0, 'cancelled' => 0, 'total_payments' => 0, 'paid_payments' => 0, 'pending_payments' => 0, 'failed_payments' => 0, 'collected' => 0, 'pending_amount' => 0, 'avg_per_night' => 0, 'completion_rate' => 0, 'dates' => $c->dates ?? [], 'next_payment' => null, 'last_payment' => null, ]; continue; } $payments = DB::table('scheduled_giving_payments') ->whereIn('scheduled_giving_donation_id', $donationIds) ->whereNull('deleted_at') ->selectRaw(" COUNT(*) as total, SUM(CASE WHEN is_paid = 1 THEN 1 ELSE 0 END) as paid, SUM(CASE WHEN is_paid = 0 THEN 1 ELSE 0 END) as pending, SUM(CASE WHEN is_paid = 0 AND attempts > 0 THEN 1 ELSE 0 END) as failed, SUM(CASE WHEN is_paid = 1 THEN amount ELSE 0 END) as collected, SUM(CASE WHEN is_paid = 0 THEN amount ELSE 0 END) as pending_amount, AVG(CASE WHEN is_paid = 1 THEN amount ELSE NULL END) as avg_amount, MIN(CASE WHEN is_paid = 0 AND expected_at > NOW() THEN expected_at ELSE NULL END) as next_payment, MAX(CASE WHEN is_paid = 1 THEN expected_at ELSE NULL END) as last_payment ") ->first(); // Completion: subscribers who paid ALL their payments $totalNights = count($c->dates ?? []); $fullyPaid = 0; if ($totalNights > 0 && $donationIds->isNotEmpty()) { $ids = $donationIds->implode(','); $row = DB::selectOne("SELECT COUNT(*) as cnt FROM (SELECT scheduled_giving_donation_id FROM scheduled_giving_payments WHERE scheduled_giving_donation_id IN ({$ids}) AND deleted_at IS NULL GROUP BY scheduled_giving_donation_id HAVING SUM(is_paid) >= {$totalNights}) sub"); $fullyPaid = $row->cnt ?? 0; } $result[] = [ 'campaign' => $c, 'subscribers' => $c->donations_count, 'active' => $c->active_count, 'cancelled' => $c->cancelled_count, 'total_payments' => (int) $payments->total, 'paid_payments' => (int) $payments->paid, 'pending_payments' => (int) $payments->pending, 'failed_payments' => (int) $payments->failed, 'collected' => ($payments->collected ?? 0) / 100, 'pending_amount' => ($payments->pending_amount ?? 0) / 100, 'avg_per_night' => ($payments->avg_amount ?? 0) / 100, 'completion_rate' => $c->active_count > 0 ? round($fullyPaid / $c->active_count * 100, 1) : 0, 'fully_completed' => $fullyPaid, 'dates' => $c->dates ?? [], 'total_nights' => $totalNights, 'next_payment' => $payments->next_payment, 'last_payment' => $payments->last_payment, ]; } return $result; } /** * Global totals across all campaigns. */ public function getGlobalStats(): array { $row = DB::table('scheduled_giving_payments') ->whereNull('deleted_at') ->selectRaw(" COUNT(*) as total, SUM(CASE WHEN is_paid = 1 THEN 1 ELSE 0 END) as paid, SUM(CASE WHEN is_paid = 0 AND attempts > 0 THEN 1 ELSE 0 END) as failed, SUM(CASE WHEN is_paid = 1 THEN amount ELSE 0 END) / 100 as collected, SUM(CASE WHEN is_paid = 0 THEN amount ELSE 0 END) / 100 as pending ") ->first(); $totalSubs = ScheduledGivingDonation::count(); $activeSubs = ScheduledGivingDonation::where('is_active', true)->count(); return [ 'total_subscribers' => $totalSubs, 'active_subscribers' => $activeSubs, 'total_payments' => (int) ($row->total ?? 0), 'paid_payments' => (int) ($row->paid ?? 0), 'failed_payments' => (int) ($row->failed ?? 0), 'collected' => (float) ($row->collected ?? 0), 'pending' => (float) ($row->pending ?? 0), 'collection_rate' => $row->total > 0 ? round($row->paid / $row->total * 100, 1) : 0, ]; } /** * Recent failed payments needing attention. */ public function getFailedPayments(): array { return DB::table('scheduled_giving_payments as p') ->join('scheduled_giving_donations as d', 'd.id', '=', 'p.scheduled_giving_donation_id') ->join('scheduled_giving_campaigns as c', 'c.id', '=', 'd.scheduled_giving_campaign_id') ->leftJoin('customers as cu', 'cu.id', '=', 'd.customer_id') ->where('p.is_paid', false) ->where('p.attempts', '>', 0) ->whereNull('p.deleted_at') ->orderByDesc('p.updated_at') ->limit(15) ->get([ 'p.id as payment_id', 'p.amount', 'p.expected_at', 'p.attempts', 'd.id as donation_id', 'c.title as campaign', DB::raw("CONCAT(cu.first_name, ' ', cu.last_name) as donor_name"), 'cu.email as donor_email', ]) ->toArray(); } /** * Upcoming payments in the next 48 hours. */ public function getUpcomingPayments(): array { return DB::table('scheduled_giving_payments as p') ->join('scheduled_giving_donations as d', 'd.id', '=', 'p.scheduled_giving_donation_id') ->join('scheduled_giving_campaigns as c', 'c.id', '=', 'd.scheduled_giving_campaign_id') ->where('p.is_paid', false) ->where('p.attempts', 0) ->where('d.is_active', true) ->whereNull('p.deleted_at') ->whereBetween('p.expected_at', [now(), now()->addHours(48)]) ->selectRaw(" c.title as campaign, COUNT(*) as payment_count, SUM(p.amount) / 100 as total_amount, MIN(p.expected_at) as earliest ") ->groupBy('c.title') ->orderBy('earliest') ->get() ->toArray(); } }