feat: add server modules (stripe, contract, ai, facebook)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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];
|
||||
}
|
||||
@@ -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>`;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user