Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/paypal

src/invoices.ts

253 lines · 7.3 KB · TypeScript
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)
}