feat: add server modules (stripe, contract, ai, facebook)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 11:59:31 -05:00
co-authored by Claude Sonnet 4.6
parent 3669c0d4f8
commit 9dac6f9f8b
4 changed files with 294 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
import { db } from './db';
import { aiJobs } from './schema';
import { eq } from 'drizzle-orm';
export async function queueNameGenerationJob(params: {
species: string;
theme?: string;
count?: number;
}): Promise<number> {
const result = await db.insert(aiJobs).values({
type: 'pet-names',
input: {
species: params.species,
theme: params.theme || 'classic',
count: params.count || 10
},
status: 'pending'
});
return Number(result[0].insertId);
}
export async function getJobStatus(jobId: number) {
const result = await db
.select()
.from(aiJobs)
.where(eq(aiJobs.id, jobId))
.limit(1);
if (result.length === 0) return null;
return result[0];
}
+66
View File
@@ -0,0 +1,66 @@
export function generateAdoptionContract(params: {
orgName: string;
orgCity: string;
orgState: string;
petName: string;
species: string;
breed: string;
sex: string;
age: string;
adopterName: string;
adopterEmail: string;
adoptionFee: number;
agreementItems: string[];
date: string;
}): string {
const items = params.agreementItems
.map((item, i) => `<li>${i + 1}. ${item}</li>`)
.join('\n');
return `<!DOCTYPE html>
<html>
<head><title>Adoption Contract — ${params.petName}</title>
<style>
body { font-family: Georgia, serif; max-width: 800px; margin: 0 auto; padding: 40px; }
h1 { text-align: center; }
.info { margin: 20px 0; }
.info dt { font-weight: bold; display: inline; }
.info dd { display: inline; margin: 0 20px 0 5px; }
ol { line-height: 1.8; }
.signatures { margin-top: 60px; display: flex; justify-content: space-between; }
.sig-line { border-top: 1px solid #000; width: 200px; padding-top: 5px; }
</style>
</head>
<body>
<h1>${params.orgName}</h1>
<h2 style="text-align: center;">Adoption Contract</h2>
<dl class="info">
<dt>Pet Name:</dt><dd>${params.petName}</dd>
<dt>Species:</dt><dd>${params.species}</dd>
<dt>Breed:</dt><dd>${params.breed || 'Unknown'}</dd>
<dt>Sex:</dt><dd>${params.sex}</dd>
<dt>Age:</dt><dd>${params.age || 'Unknown'}</dd>
</dl>
<dl class="info">
<dt>Adopter:</dt><dd>${params.adopterName}</dd>
<dt>Email:</dt><dd>${params.adopterEmail}</dd>
<dt>Adoption Fee:</dt><dd>$${(params.adoptionFee / 100).toFixed(2)}</dd>
<dt>Date:</dt><dd>${params.date}</dd>
</dl>
<h3>Terms of Adoption</h3>
<ol>${items}</ol>
<div class="signatures">
<div>
<div class="sig-line">Adopter Signature</div>
</div>
<div>
<div class="sig-line">${params.orgName} Representative</div>
</div>
</div>
</body>
</html>`;
}
+102
View File
@@ -0,0 +1,102 @@
import { db } from './db';
import { siteSettings } from './schema';
import { eq } from 'drizzle-orm';
function getFacebookConfig() {
const appId = process.env.FACEBOOK_APP_ID;
const appSecret = process.env.FACEBOOK_APP_SECRET;
if (!appId || !appSecret) return null;
return { appId, appSecret };
}
export function isFacebookConfigured(): boolean {
return getFacebookConfig() !== null;
}
export function getFacebookAuthUrl(redirectUri: string): string | null {
const config = getFacebookConfig();
if (!config) return null;
const params = new URLSearchParams({
client_id: config.appId,
redirect_uri: redirectUri,
scope: 'pages_manage_posts,pages_read_engagement'
});
return `https://www.facebook.com/v19.0/dialog/oauth?${params}`;
}
export async function exchangeCodeForToken(code: string, redirectUri: string): Promise<string | null> {
const config = getFacebookConfig();
if (!config) return null;
const params = new URLSearchParams({
client_id: config.appId,
client_secret: config.appSecret,
redirect_uri: redirectUri,
code
});
const res = await fetch(`https://graph.facebook.com/v19.0/oauth/access_token?${params}`);
if (!res.ok) return null;
const data = await res.json();
return data.access_token || null;
}
export async function getLongLivedToken(shortToken: string): Promise<string | null> {
const config = getFacebookConfig();
if (!config) return null;
const params = new URLSearchParams({
grant_type: 'fb_exchange_token',
client_id: config.appId,
client_secret: config.appSecret,
fb_exchange_token: shortToken
});
const res = await fetch(`https://graph.facebook.com/v19.0/oauth/access_token?${params}`);
if (!res.ok) return null;
const data = await res.json();
return data.access_token || null;
}
export async function getPages(userToken: string): Promise<Array<{ id: string; name: string; access_token: string }>> {
const res = await fetch(`https://graph.facebook.com/v19.0/me/accounts?access_token=${userToken}`);
if (!res.ok) return [];
const data = await res.json();
return data.data || [];
}
export async function postToPage(pageId: string, pageToken: string, message: string, link?: string): Promise<boolean> {
const body: Record<string, string> = { message, access_token: pageToken };
if (link) body.link = link;
const res = await fetch(`https://graph.facebook.com/v19.0/${pageId}/feed`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
return res.ok;
}
export async function getFacebookSettings(): Promise<{ pageId: string; pageToken: string; pageName: string } | null> {
const result = await db
.select()
.from(siteSettings)
.where(eq(siteSettings.key, 'facebook'))
.limit(1);
if (result.length === 0) return null;
try {
const val = JSON.parse(result[0].value);
if (!val.pageId || !val.pageToken) return null;
return val;
} catch {
return null;
}
}
+94
View File
@@ -0,0 +1,94 @@
import Stripe from 'stripe';
function getStripe(): Stripe | null {
if (!process.env.STRIPE_SECRET_KEY) return null;
return new Stripe(process.env.STRIPE_SECRET_KEY);
}
export async function createDonationCheckout(params: {
amount: number;
donorEmail?: string;
petId?: number;
petName?: string;
successUrl: string;
cancelUrl: string;
}): Promise<string | null> {
const stripe = getStripe();
if (!stripe) return null;
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'payment',
customer_email: params.donorEmail,
line_items: [{
price_data: {
currency: 'usd',
product_data: {
name: params.petName
? `Donation for ${params.petName}`
: 'Donation'
},
unit_amount: params.amount
},
quantity: 1
}],
metadata: {
type: 'donation',
petId: params.petId?.toString() || ''
},
success_url: params.successUrl,
cancel_url: params.cancelUrl
});
return session.url;
}
export async function createAdoptionFeeCheckout(params: {
amount: number;
petId: number;
petName: string;
adopterEmail: string;
successUrl: string;
cancelUrl: string;
}): Promise<string | null> {
const stripe = getStripe();
if (!stripe) return null;
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'payment',
customer_email: params.adopterEmail,
line_items: [{
price_data: {
currency: 'usd',
product_data: {
name: `Adoption Fee — ${params.petName}`
},
unit_amount: params.amount
},
quantity: 1
}],
metadata: {
type: 'adoption_fee',
petId: params.petId.toString()
},
success_url: params.successUrl,
cancel_url: params.cancelUrl
});
return session.url;
}
export function constructWebhookEvent(
body: string,
signature: string
): Stripe.Event | null {
const stripe = getStripe();
if (!stripe || !process.env.STRIPE_WEBHOOK_SECRET) return null;
return stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
}