/** * Shared PayPal transport: client-credentials auth, OpenAPI wrappers, * dry-run previews, and setup-aware errors. * * Auth is secret-backed (not user OAuth). Defaults are `paypalClientId` and * `paypalClientSecret`. Extra accounts use `account: "work"` → * `paypalClientId-work` / `paypalClientSecret-work`. */ import { oauthClientCredentials } from 'kody:runtime' import { balancesGet, invoicesCreate, invoicesGet, invoicesList, invoicesSearchInvoices, invoicesSend, payoutsCreate, payoutsGet, payoutsItemGet, searchGet, type QueryInput, type ScaffoldInput, type ScaffoldOptions, } from './openapi-client.ts' import { LIVE_API_HOST, SANDBOX_API_HOST, clientIdSetupUrl, clientSecretSetupUrl, getPayPalApiBaseUrl, resolveClientIdSecretName, resolveClientSecretSecretName, type PayPalAuthInput, } from './paypal-helpers.ts' export { CLIENT_ID_SETUP_URL, CLIENT_SECRET_SETUP_URL, DASHBOARD_APPS_URL, DEFAULT_CLIENT_ID_SECRET, DEFAULT_CLIENT_SECRET_SECRET, LIVE_API_BASE_URL, LIVE_API_HOST, SANDBOX_API_BASE_URL, SANDBOX_API_HOST, assertNever, buildInvoiceBody, buildPayoutBody, buildPayoutItem, clientIdSetupUrl, clientSecretSetupUrl, getPayPalApiBaseUrl, includesText, moneyValue, mutationPreview, normalizeInvoiceSummary, normalizeMoney, normalizePayoutSummary, normalizeTransactionSummary, numericMoneyValue, parseAction, requireConfirm, resolveClientIdSecretName, resolveClientSecretSecretName, toPayPalDate, } from './paypal-helpers.ts' export type { MutationGuardInput, PayPalAuthInput, PayPalDryRun, PayPalEnvironment, PayoutBodyInput, PayoutItemInput, PayoutRecipientType, } from './paypal-helpers.ts' export class PayPalApiError extends Error { status: number statusText: string details: unknown setup: { clientIdUrl: string; clientSecretUrl: string; hosts: string[] } constructor( message: string, input: { status: number; statusText: string; details: unknown; auth?: PayPalAuthInput }, ) { super(message) this.name = 'PayPalApiError' this.status = input.status this.statusText = input.statusText this.details = input.details this.setup = { clientIdUrl: clientIdSetupUrl(resolveClientIdSecretName(input.auth)), clientSecretUrl: clientSecretSetupUrl(resolveClientSecretSecretName(input.auth)), hosts: [LIVE_API_HOST, SANDBOX_API_HOST], } } } /** * Mint a short-lived PayPal access token via oauthClientCredentials. * Token stays in-memory for the call; never log or return it. */ async function getAccessToken(input: PayPalAuthInput = {}) { const apiBaseUrl = getPayPalApiBaseUrl(input) const payload = await oauthClientCredentials({ tokenUrl: apiBaseUrl + '/v1/oauth2/token', clientIdSecret: resolveClientIdSecretName(input), clientSecretSecret: resolveClientSecretSecretName(input), scope: input.secretScope ?? 'user', authStyle: 'basic', }) const accessToken = payload && typeof payload.access_token === 'string' ? payload.access_token : null if (!accessToken) { throw new Error('PayPal token response did not include an access_token string.') } return accessToken } async function parseResponse(response: Response) { const text = await response.text() if (!text) return null try { return JSON.parse(text) } catch { return { raw: text } } } async function parseOkJson(response: Response, auth: PayPalAuthInput = {}) { const data = await parseResponse(response) if (!response.ok) { const errorName = data && typeof data === 'object' && 'name' in data ? String((data as Record).name) : 'PayPal API request failed' const message = data && typeof data === 'object' && 'message' in data ? String((data as Record).message) : errorName const hint = response.status === 401 ? ' Save paypalClientId and paypalClientSecret at the prefilled secrets URLs and approve api-m.paypal.com.' : '' throw new PayPalApiError(message + hint, { status: response.status, statusText: response.statusText, details: data, auth, }) } return data } type AuthCallOptions = PayPalAuthInput & { headers?: Record } async function callAuthenticated( fn: (input?: ScaffoldInput, options?: ScaffoldOptions) => Promise, scaffoldInput: ScaffoldInput, auth: AuthCallOptions = {}, ) { const accessToken = await getAccessToken(auth) const response = await fn( { ...scaffoldInput, headers: { Accept: 'application/json', Authorization: 'Bearer ' + accessToken, ...(scaffoldInput.headers ?? {}), ...(auth.headers ?? {}), }, }, { apiBaseUrl: getPayPalApiBaseUrl(auth) }, ) return parseOkJson(response, auth) } /** List invoices via OpenAPI slug `invoices_list`. */ export async function paypalListInvoices(input: PayPalAuthInput & { query?: QueryInput } = {}) { return callAuthenticated(invoicesList, { query: input.query }, input) } /** Get one invoice via OpenAPI slug `invoices_get`. */ export async function paypalGetInvoice(input: PayPalAuthInput & { invoiceId: string; query?: QueryInput }) { return callAuthenticated( invoicesGet, { params: { invoice_id: input.invoiceId }, query: input.query }, input, ) } /** Search invoices via OpenAPI slug `invoices_search_invoices` (read-only POST). */ export async function paypalSearchInvoices(input: PayPalAuthInput & { query?: QueryInput; body?: unknown } = {}) { return callAuthenticated( invoicesSearchInvoices, { query: input.query, body: input.body ?? {} }, input, ) } /** Create a draft invoice via OpenAPI slug `invoices_create`. */ export async function paypalCreateInvoice(input: PayPalAuthInput & { body: unknown; query?: QueryInput }) { return callAuthenticated( invoicesCreate, { query: input.query, body: input.body, headers: { Prefer: 'return=representation' }, }, input, ) } /** Send an invoice via OpenAPI slug `invoices_send`. */ export async function paypalSendInvoice( input: PayPalAuthInput & { invoiceId: string body?: unknown query?: QueryInput }, ) { return callAuthenticated( invoicesSend, { params: { invoice_id: input.invoiceId }, query: input.query, body: input.body ?? { send_to_recipient: true, send_to_invoicer: true }, }, input, ) } /** List transactions via OpenAPI slug `search_get`. */ export async function paypalListTransactions(input: PayPalAuthInput & { query?: QueryInput } = {}) { return callAuthenticated(searchGet, { query: input.query }, input) } /** List balances via OpenAPI slug `balances_get`. */ export async function paypalListBalances(input: PayPalAuthInput & { query?: QueryInput } = {}) { return callAuthenticated(balancesGet, { query: input.query }, input) } /** Create a payout batch via `payouts_create`. */ export async function paypalCreatePayout( input: PayPalAuthInput & { body: unknown; requestId?: string }, ) { return callAuthenticated( payoutsCreate, { body: input.body, headers: input.requestId ? { 'PayPal-Request-Id': input.requestId } : {}, }, input, ) } /** Get a payout batch via `payouts_get`. */ export async function paypalGetPayout(input: PayPalAuthInput & { payoutBatchId: string; query?: QueryInput }) { return callAuthenticated( payoutsGet, { params: { payout_batch_id: input.payoutBatchId }, query: input.query }, input, ) } /** Get one payout item via `payouts_item_get`. */ export async function paypalGetPayoutItem(input: PayPalAuthInput & { payoutItemId: string }) { return callAuthenticated(payoutsItemGet, { params: { payout_item_id: input.payoutItemId } }, input) }