Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199// Gives Feed, 26.02.08// 🎁 Fetches recent successful gives from Stripe// Includes both one-time checkout sessions and subscription invoice renewals// Endpoint: GET /api/gives
import Stripe from "stripe";import { respond } from "../../backend/http.mjs";
const dev = process.env.CONTEXT !== "production";
// Cache to avoid hammering Stripe APIlet cachedGives = null;let cacheTimestamp = 0;const CACHE_TTL = 60000; // 1 minute cache
export async function handler(event, context) { // Handle CORS preflight if (event.httpMethod === "OPTIONS") { return respond(200, {}); }
if (event.httpMethod !== "GET") { return respond(405, { error: "Method not allowed" }); }
// Always use production Stripe key for gives feed (show real donations) const stripeKey = process.env.STRIPE_API_PRIV_KEY;
if (!stripeKey) { // Return empty gives in dev when Stripe isn't configured if (dev) { return respond(200, { gives: [], total: 0, cached: false, dev: true }); } return respond(500, { error: "Stripe not configured" }); }
// Return cached data if fresh const now = Date.now(); if (cachedGives && (now - cacheTimestamp) < CACHE_TTL) { return respond(200, cachedGives); }
const stripe = Stripe(stripeKey);
try { const limit = parseInt(event.queryStringParameters?.limit) || 100; // Only show gives from 2025 onwards (when give page launched) const minDate = new Date('2025-01-01').getTime() / 1000; // Unix timestamp // 1. Fetch checkout sessions for one-time gives and initial subscription signups const sessions = await stripe.checkout.sessions.list({ limit: 100, // Stripe max per page status: 'complete', created: { gte: minDate }, });
// Only include sessions with give-specific metadata // (excludes print/mug/news-toll sessions that also use mode:'payment') const giftSessions = sessions.data.filter(session => { const type = (session.metadata || {}).type; return type === 'gift' || type === 'subscription'; });
// Track subscription IDs from checkout sessions to avoid duplicates const checkoutSubIds = new Set( giftSessions .filter(s => s.subscription) .map(s => typeof s.subscription === 'string' ? s.subscription : s.subscription.id) );
// 2. Fetch recent paid subscription invoices (captures recurring renewals) // Subscription renewals don't create new checkout sessions — only invoices. let invoiceGives = []; try { const invoices = await stripe.invoices.list({ limit: 50, status: 'paid', created: { gte: minDate }, });
invoiceGives = invoices.data .filter(inv => { // Only subscription invoices if (!inv.subscription) return false; const subId = typeof inv.subscription === 'string' ? inv.subscription : inv.subscription.id; // Skip the initial create invoice if we already have a checkout session for it if (inv.billing_reason === 'subscription_create' && checkoutSubIds.has(subId)) { return false; } return true; }) .map(inv => { const currency = (inv.currency || 'usd').toUpperCase(); const amount = inv.amount_paid / 100; let amountDisplay; if (currency === 'DKK') { amountDisplay = `${Math.round(amount)} kr`; } else { amountDisplay = `$${amount.toFixed(amount % 1 === 0 ? 0 : 2)}`; } return { id: inv.id, amount, amountDisplay, currency, isRecurring: true, note: '', // Renewal invoices don't carry the original checkout note createdAt: (inv.status_transitions?.paid_at || inv.created) * 1000, }; }); } catch (e) { console.log('Could not fetch subscription invoices:', e.message); }
// 3. Convert checkout sessions to gives const sessionGives = giftSessions.slice(0, limit).map(session => { const metadata = session.metadata || {}; const currency = (session.currency || 'usd').toUpperCase(); const amount = session.amount_total / 100; const isRecurring = metadata.type === 'subscription'; // Note comes from custom_fields (checkout form), not metadata const customFields = session.custom_fields || []; const noteField = customFields.find(f => f.key === 'note'); const note = noteField?.text?.value || ''; // Format amount based on currency let amountDisplay; if (currency === 'DKK') { amountDisplay = `${Math.round(amount)} kr`; } else { amountDisplay = `$${amount.toFixed(amount % 1 === 0 ? 0 : 2)}`; }
return { id: session.id, amount, amountDisplay, currency, isRecurring, note, createdAt: session.created * 1000, // Convert to JS timestamp }; });
// 4. Merge, sort by date (newest first), and limit const gives = [...sessionGives, ...invoiceGives] .sort((a, b) => b.createdAt - a.createdAt) .slice(0, limit);
// Calculate some stats const totalAmount = gives.reduce((sum, g) => { // Convert to USD for total (approximate) const usdAmount = g.currency === 'DKK' ? g.amount / 7 : g.amount; return sum + usdAmount; }, 0);
// Calculate this month's total (in USD) const now = new Date(); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1).getTime(); const monthlyGives = gives.filter(g => g.createdAt >= startOfMonth); const monthlyTotalUSD = monthlyGives.reduce((sum, g) => { const usdAmount = g.currency === 'DKK' ? g.amount / 7 : g.amount; return sum + usdAmount; }, 0);
// Count active monthly subscribers let activeSubscribers = 0; try { const subscriptions = await stripe.subscriptions.list({ status: 'active', limit: 100, }); activeSubscribers = subscriptions.data.length; } catch (e) { console.log('Could not fetch subscriptions:', e.message); }
const result = { gives, count: gives.length, monthlyCount: monthlyGives.length, monthlyTotalUSD: Math.round(monthlyTotalUSD), totalUSD: Math.round(totalAmount), activeSubscribers, lastUpdated: new Date().toISOString(), };
// Cache the result cachedGives = result; cacheTimestamp = now;
return respond(200, result); } catch (error) { console.error("Gives feed error:", error); return respond(500, { error: error.message }); }}