import { and, desc, eq } from "drizzle-orm" import { db } from "@/lib/db" import { leases, signature_requests } from "@/lib/db/schema" import { readFile } from "@/lib/storage" import { docusign } from "./docusign" import { dropboxSign } from "./dropbox-sign" import type { ESignAdapter, ESignProvider } from "./types" export type { ESignProvider } from "./types" const ADAPTERS: Record = { docusign, dropbox_sign: dropboxSign } export function getAdapter(id: string): ESignAdapter | null { return id === "docusign" || id === "dropbox_sign" ? ADAPTERS[id] : null } export function listAdapters() { return (Object.keys(ADAPTERS) as ESignProvider[]).map((id) => ({ id, label: ADAPTERS[id].label, configured: ADAPTERS[id].configured() })) } export function anyEsignConfigured(): boolean { return listAdapters().some((a) => a.configured) } async function getDocumentBytes(documentUrl: string): Promise<{ bytes: Buffer; name: string }> { const prefix = "/api/files/" if (documentUrl.startsWith(prefix)) { const key = documentUrl.slice(prefix.length) return { bytes: await readFile(key), name: key.split("/").pop() ?? "lease.pdf" } } const res = await fetch(documentUrl) if (!res.ok) throw new Error("Could not fetch the lease document") return { bytes: Buffer.from(await res.arrayBuffer()), name: documentUrl.split("/").pop()?.split("?")[0] ?? "lease.pdf" } } export async function sendLeaseForSignature(ownerId: string, leaseId: string, provider: ESignProvider) { const adapter = getAdapter(provider) if (!adapter) throw new Error("Unknown provider") if (!adapter.configured()) throw new Error(`${adapter.label} is not configured`) const lease = await db.query.leases.findFirst({ where: and(eq(leases.id, leaseId), eq(leases.user_id, ownerId)), with: { tenant: { columns: { first_name: true, last_name: true, email: true } } }, }) if (!lease) throw new Error("Lease not found") if (!lease.document_url) throw new Error("Upload a lease document before sending it for signature") const email = lease.tenant?.email if (!email) throw new Error("The tenant has no email address on file") const signerName = `${lease.tenant?.first_name ?? ""} ${lease.tenant?.last_name ?? ""}`.trim() || "Tenant" const { bytes, name } = await getDocumentBytes(lease.document_url) try { const { externalId } = await adapter.send({ document: bytes, documentName: name, signerEmail: email, signerName, subject: "Please sign your lease agreement", message: "Your landlord has sent your lease agreement for electronic signature.", }) const [row] = await db .insert(signature_requests) .values({ user_id: ownerId, lease_id: leaseId, provider, external_id: externalId, status: "sent", signer_email: email, signer_name: signerName, document_name: name }) .returning() return row } catch (e) { const msg = (e as Error).message.slice(0, 500) await db .insert(signature_requests) .values({ user_id: ownerId, lease_id: leaseId, provider, status: "error", signer_email: email, signer_name: signerName, document_name: name, last_error: msg }) throw new Error(msg) } } export async function listRequestsForLease(ownerId: string, leaseId: string) { return db.query.signature_requests.findMany({ where: and(eq(signature_requests.user_id, ownerId), eq(signature_requests.lease_id, leaseId)), orderBy: desc(signature_requests.sent_at), }) } /** Update a request's status from an inbound provider webhook. */ export async function handleEsignWebhook(provider: string, body: string, headers: Headers) { const adapter = getAdapter(provider) if (!adapter) return const result = adapter.parseWebhook(body, headers) if (!result) return await db .update(signature_requests) .set({ status: result.status, completed_at: result.status === "signed" ? new Date().toISOString() : null, updated_at: new Date().toISOString(), }) .where(eq(signature_requests.external_id, result.externalId)) }