/**
* 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<string, unknown>).name)
: 'PayPal API request failed'
const message =
data && typeof data === 'object' && 'message' in data
? String((data as Record<string, unknown>).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<string, string>
}
async function callAuthenticated(
fn: (input?: ScaffoldInput, options?: ScaffoldOptions) => Promise<Response>,
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)
}