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