import {
buildInvoiceBody,
includesText,
mutationPreview,
normalizeInvoiceSummary,
type MutationGuardInput,
type PayPalAuthInput,
} from './paypal-helpers.ts'
import {
paypalCreateInvoice,
paypalGetInvoice,
paypalListInvoices,
paypalSearchInvoices,
paypalSendInvoice,
} from './paypal-core.ts'
export type ListInvoicesInput = PayPalAuthInput & {
page?: number
pageSize?: number
totalRequired?: boolean
fields?: string
status?: string
includeRaw?: boolean
}
export type SearchInvoicesInput = PayPalAuthInput & {
page?: number
pageSize?: number
totalRequired?: boolean
invoiceNumber?: string
recipientEmail?: string
status?: string
reference?: string
currencyCode?: string
amount?: string | number
rawCriteria?: Record<string, unknown>
includeRaw?: boolean
query?: string
}
export type GetInvoiceInput = PayPalAuthInput & {
id: string
includeRaw?: boolean
}
export function invoiceMatches(
summary: any,
input: {
amount?: string | number
status?: string
recipientEmail?: string
query?: string
currencyCode?: string
},
) {
if (input.status && summary.status !== input.status) return false
if (input.recipientEmail && !includesText(summary.recipient_email, input.recipientEmail)) return false
if (input.currencyCode && summary.currency_code !== input.currencyCode) return false
if (input.amount != null) {
const expected = Number(input.amount)
const amount = Number(summary.amount)
if (!Number.isFinite(expected) || !Number.isFinite(amount) || Math.abs(Math.abs(amount) - Math.abs(expected)) > 0.005) {
return false
}
}
if (input.query) {
const haystack = [
summary.id,
summary.invoice_number,
summary.status,
summary.recipient_email,
summary.recipient_name?.given_name,
summary.recipient_name?.surname,
summary.merchant_name,
summary.reference,
summary.memo,
summary.amount,
].join(' ')
if (!includesText(haystack, input.query)) return false
}
return true
}
function mapInvoiceItem(item: any, includeRaw?: boolean) {
const summary = normalizeInvoiceSummary(item)
return includeRaw ? { summary, raw: item } : summary
}
/**
* List recent PayPal invoices (OpenAPI `invoices_list`).
*/
export async function listInvoices(input: ListInvoicesInput = {}) {
const raw = await paypalListInvoices({
...input,
query: {
page: input.page ?? 1,
page_size: input.pageSize ?? 20,
total_required: input.totalRequired ?? true,
fields: input.fields,
},
})
const items = Array.isArray((raw as any)?.items) ? (raw as any).items : []
const mapped = items.map((item: any) => mapInvoiceItem(item, input.includeRaw))
const filtered = input.status
? mapped.filter((item: any) => (input.includeRaw ? item.summary?.status : item?.status) === input.status)
: mapped
return {
total_items: (raw as any)?.total_items ?? null,
total_pages: (raw as any)?.total_pages ?? null,
page: (raw as any)?.page ?? input.page ?? 1,
items: filtered,
}
}
/**
* Read one PayPal invoice by invoice id (OpenAPI `invoices_get`).
*/
export async function getInvoice(input: GetInvoiceInput) {
if (!input.id) throw new Error('getInvoice requires id.')
const raw = await paypalGetInvoice({ ...input, invoiceId: input.id })
const summary = normalizeInvoiceSummary(raw)
return input.includeRaw ? { summary, raw } : summary
}
/**
* Search PayPal invoices by invoice number, recipient email, status, reference, currency, or amount
* (OpenAPI `invoices_search_invoices`).
*/
export async function searchInvoices(input: SearchInvoicesInput = {}) {
const body: Record<string, unknown> = { ...(input.rawCriteria ?? {}) }
if (input.invoiceNumber) body.invoice_number = input.invoiceNumber
if (input.recipientEmail) body.recipient_email = input.recipientEmail
if (input.status) body.status = input.status
if (input.reference) body.reference = input.reference
if (input.currencyCode) body.currency_code = input.currencyCode
const raw = await paypalSearchInvoices({
...input,
query: {
page: input.page ?? 1,
page_size: input.pageSize ?? 20,
total_required: input.totalRequired ?? true,
},
body,
})
const items = Array.isArray((raw as any)?.items) ? (raw as any).items : []
const mapped = items.map((item: any) => mapInvoiceItem(item, input.includeRaw))
const filtered = mapped.filter((item: any) => invoiceMatches(input.includeRaw ? item.summary : item, input))
return {
total_items: (raw as any)?.total_items ?? null,
total_pages: (raw as any)?.total_pages ?? null,
page: (raw as any)?.page ?? input.page ?? 1,
items: filtered,
}
}
/**
* Find a single PayPal invoice. If id is provided, this reads that invoice directly; otherwise it searches.
*/
export async function findInvoice(input: SearchInvoicesInput & { id?: string } = {}) {
if (input.id) {
const invoice = await getInvoice({ ...input, id: input.id })
return { match: invoice, source: 'id' }
}
const result = await searchInvoices(input)
return {
...result,
match: result.items[0] ?? null,
source: 'search',
}
}
/**
* Return a compact invoice summary by id or search criteria.
*/
export async function summarizeInvoice(input: SearchInvoicesInput & { id?: string } = {}) {
const result = await findInvoice(input)
return result.match
}
export type CreateInvoiceInput = MutationGuardInput & {
invoiceNumber?: string
invoiceDate?: string
currencyCode?: string
memo?: string
note?: string
termType?: string
recipientEmail: string
recipientGivenName?: string
recipientSurname?: string
itemName: string
itemDescription?: string
amount: string | number
send?: boolean
includeRaw?: boolean
}
export { buildInvoiceBody } from './paypal-helpers.ts'
/**
* Create a PayPal invoice (draft unless `send` is true).
* Pass `dryRun: true` to preview. Sending also requires `confirm: true`.
*/
export async function createInvoice(input: CreateInvoiceInput) {
const body = buildInvoiceBody(input)
const preview = mutationPreview(input, {
action: input.send ? 'create and send invoice to ' + input.recipientEmail : 'create draft invoice',
method: 'POST',
path: '/v2/invoicing/invoices',
body,
requireConfirm: Boolean(input.send),
})
if (preview) return preview
const raw = await paypalCreateInvoice({ ...input, body })
const id = (raw as any)?.id
if (input.send) {
if (!id) throw new Error('PayPal create did not return an invoice id.')
await paypalSendInvoice({ ...input, invoiceId: id })
return await getInvoice({ ...input, id, includeRaw: input.includeRaw })
}
const summary = normalizeInvoiceSummary(raw)
return input.includeRaw ? { summary, raw } : summary
}
export type SendInvoiceInput = MutationGuardInput & {
id: string
includeRaw?: boolean
}
/**
* Send an existing PayPal draft invoice to the recipient.
* Pass `dryRun: true` to preview. Live send requires `confirm: true`.
*/
export async function sendInvoice(input: SendInvoiceInput) {
if (!input.id) throw new Error('sendInvoice requires id.')
const preview = mutationPreview(input, {
action: 'send invoice ' + input.id,
method: 'POST',
path: '/v2/invoicing/invoices/' + encodeURIComponent(input.id) + '/send',
body: { send_to_recipient: true, send_to_invoicer: true },
requireConfirm: true,
})
if (preview) return preview
await paypalSendInvoice({ ...input, invoiceId: input.id })
return await getInvoice({ ...input, id: input.id, includeRaw: input.includeRaw })
}
export default async function invoices(input: SearchInvoicesInput = {}) {
return await searchInvoices(input)
}