From 9dac6f9f8b5226367cea0dd6579fde6bbeb0d71f Mon Sep 17 00:00:00 2001 From: Justin Reiners Date: Thu, 26 Mar 2026 11:59:31 -0500 Subject: [PATCH] feat: add server modules (stripe, contract, ai, facebook) Co-Authored-By: Claude Sonnet 4.6 --- src/lib/server/ai.ts | 32 ++++++++++++ src/lib/server/contract.ts | 66 ++++++++++++++++++++++++ src/lib/server/facebook.ts | 102 +++++++++++++++++++++++++++++++++++++ src/lib/server/stripe.ts | 94 ++++++++++++++++++++++++++++++++++ 4 files changed, 294 insertions(+) create mode 100644 src/lib/server/ai.ts create mode 100644 src/lib/server/contract.ts create mode 100644 src/lib/server/facebook.ts create mode 100644 src/lib/server/stripe.ts diff --git a/src/lib/server/ai.ts b/src/lib/server/ai.ts new file mode 100644 index 0000000..0006f2d --- /dev/null +++ b/src/lib/server/ai.ts @@ -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 { + 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]; +} diff --git a/src/lib/server/contract.ts b/src/lib/server/contract.ts new file mode 100644 index 0000000..3262e16 --- /dev/null +++ b/src/lib/server/contract.ts @@ -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) => `
  • ${i + 1}. ${item}
  • `) + .join('\n'); + + return ` + +Adoption Contract — ${params.petName} + + + +

    ${params.orgName}

    +

    Adoption Contract

    + +
    +
    Pet Name:
    ${params.petName}
    +
    Species:
    ${params.species}
    +
    Breed:
    ${params.breed || 'Unknown'}
    +
    Sex:
    ${params.sex}
    +
    Age:
    ${params.age || 'Unknown'}
    +
    + +
    +
    Adopter:
    ${params.adopterName}
    +
    Email:
    ${params.adopterEmail}
    +
    Adoption Fee:
    $${(params.adoptionFee / 100).toFixed(2)}
    +
    Date:
    ${params.date}
    +
    + +

    Terms of Adoption

    +
      ${items}
    + +
    +
    +
    Adopter Signature
    +
    +
    +
    ${params.orgName} Representative
    +
    +
    + +`; +} diff --git a/src/lib/server/facebook.ts b/src/lib/server/facebook.ts new file mode 100644 index 0000000..6221b72 --- /dev/null +++ b/src/lib/server/facebook.ts @@ -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 { + 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 { + 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> { + 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 { + const body: Record = { 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; + } +} diff --git a/src/lib/server/stripe.ts b/src/lib/server/stripe.ts new file mode 100644 index 0000000..3b329dc --- /dev/null +++ b/src/lib/server/stripe.ts @@ -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 { + 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 { + 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 + ); +}