import { paypalFetch } from "./client" // We encode the app user id + target plan into PayPal's `custom_id` so webhooks // and the return handler can resolve who/what a subscription or order is for, // without trusting query params. Format: ":". export function encodeCustomId(userId: string, plan: string): string { return `${userId}:${plan}` } export function decodeCustomId(customId: string | null | undefined): { userId: string; plan: string } | null { if (!customId) return null const idx = customId.lastIndexOf(":") if (idx <= 0) return null return { userId: customId.slice(0, idx), plan: customId.slice(idx + 1) } } function approveUrl(links: Array<{ rel: string; href: string }> | undefined): string | undefined { return links?.find((l) => l.rel === "approve" || l.rel === "payer-action")?.href } const BRAND = "Property Management Network" /** Create a recurring subscription; returns its id + the PayPal approval URL. */ export async function createSubscription(params: { planId: string userId: string plan: string email?: string | null returnUrl: string cancelUrl: string }): Promise<{ id: string; approveUrl?: string }> { const res = await paypalFetch("/v1/billing/subscriptions", { method: "POST", body: JSON.stringify({ plan_id: params.planId, custom_id: encodeCustomId(params.userId, params.plan), subscriber: params.email ? { email_address: params.email } : undefined, application_context: { brand_name: BRAND, user_action: "SUBSCRIBE_NOW", shipping_preference: "NO_SHIPPING", return_url: params.returnUrl, cancel_url: params.cancelUrl, }, }), }) if (!res.ok) throw new Error(`PayPal createSubscription failed: ${res.status} ${await res.text().catch(() => "")}`) const json = (await res.json()) as { id: string; links?: Array<{ rel: string; href: string }> } return { id: json.id, approveUrl: approveUrl(json.links) } } /** Create a one-time order (used for the Lifetime plan). */ export async function createOrder(params: { amount: number userId: string plan: string returnUrl: string cancelUrl: string }): Promise<{ id: string; approveUrl?: string }> { const res = await paypalFetch("/v2/checkout/orders", { method: "POST", body: JSON.stringify({ intent: "CAPTURE", purchase_units: [ { amount: { currency_code: "USD", value: params.amount.toFixed(2) }, custom_id: encodeCustomId(params.userId, params.plan), description: `${BRAND} — Lifetime`, }, ], application_context: { brand_name: BRAND, user_action: "PAY_NOW", shipping_preference: "NO_SHIPPING", return_url: params.returnUrl, cancel_url: params.cancelUrl, }, }), }) if (!res.ok) throw new Error(`PayPal createOrder failed: ${res.status} ${await res.text().catch(() => "")}`) const json = (await res.json()) as { id: string; links?: Array<{ rel: string; href: string }> } return { id: json.id, approveUrl: approveUrl(json.links) } } /** Capture an approved order. Returns the captured order (status COMPLETED). */ export async function captureOrder(orderId: string): Promise<{ status: string custom_id?: string } | null> { const res = await paypalFetch(`/v2/checkout/orders/${orderId}/capture`, { method: "POST", body: "{}", }) if (!res.ok) return null const json = (await res.json()) as { status: string purchase_units?: Array<{ custom_id?: string; payments?: { captures?: Array<{ custom_id?: string }> } }> } const unit = json.purchase_units?.[0] const custom_id = unit?.custom_id ?? unit?.payments?.captures?.[0]?.custom_id return { status: json.status, custom_id } } export type PaypalSubscription = { id: string status: string custom_id?: string billing_info?: { next_billing_time?: string } } export async function getSubscription(id: string): Promise { const res = await paypalFetch(`/v1/billing/subscriptions/${id}`, { method: "GET" }) if (!res.ok) return null return (await res.json()) as PaypalSubscription } export async function cancelSubscription(id: string, reason = "Cancelled by subscriber"): Promise { const res = await paypalFetch(`/v1/billing/subscriptions/${id}/cancel`, { method: "POST", body: JSON.stringify({ reason }), }) // 204 = cancelled; 422 = already inactive (treat as success so the UI settles). return res.ok || res.status === 204 || res.status === 422 }