/** * Convenience Layer - Optional Helpers * * These helpers are purely additive and do not replace core API methods. * They provide convenient predicates and utilities for common operations. */ import { DocumentStatus, type Document, type ListResponse, type Sending, type ValidationMessage } from './types.js'; /** * Helper to get status code from document or sending */ function getStatusCode(doc: Document | Sending): DocumentStatus { // Document has status.code, Sending might have status directly if (typeof doc.status === 'object' && 'code' in doc.status) { return doc.status.code; } return doc.status as unknown as DocumentStatus; } // ============================================================================ // Status Predicates // ============================================================================ /** * Check if a document is shippable (status 2). */ export function isShippable(doc: Document | Sending): boolean { return getStatusCode(doc) === DocumentStatus.SHIPPABLE; } /** * Check if a document has errors (status 7). */ export function isErroneous(doc: Document | Sending): boolean { return getStatusCode(doc) === DocumentStatus.ERRONEOUS; } /** * Check if a document is still being prepared (status 1). */ export function isInPreparation(doc: Document | Sending): boolean { return getStatusCode(doc) === DocumentStatus.IN_PREPARATION; } /** * Check if a document is in the production queue (status 3). */ export function isInProductionQueue(doc: Document | Sending): boolean { return getStatusCode(doc) === DocumentStatus.PRODUCTION_QUEUE; } /** * Check if a document is currently being printed (status 4). */ export function isPrinting(doc: Document | Sending): boolean { return getStatusCode(doc) === DocumentStatus.PRINTING; } /** * Check if a document has been sent (status 5). */ export function isSent(doc: Document | Sending): boolean { return getStatusCode(doc) === DocumentStatus.SENT; } /** * Check if a document was canceled (status 6). */ export function isCanceled(doc: Document | Sending): boolean { return getStatusCode(doc) === DocumentStatus.CANCELED; } /** * Check if a document is in production (status 3 or 4). * This includes documents in the production queue and those currently printing. */ export function isInProduction(doc: Document | Sending): boolean { const status = getStatusCode(doc); return ( status === DocumentStatus.PRODUCTION_QUEUE || status === DocumentStatus.PRINTING ); } /** * Check if a document is in a terminal state (sent, canceled, or erroneous). */ export function isTerminal(doc: Document | Sending): boolean { const status = getStatusCode(doc); return ( status === DocumentStatus.SENT || status === DocumentStatus.CANCELED || status === DocumentStatus.ERRONEOUS ); } /** * Check if a document can still be canceled (in queue or printing). */ export function isCancelable(doc: Document | Sending): boolean { const status = getStatusCode(doc); return ( status === DocumentStatus.PRODUCTION_QUEUE || status === DocumentStatus.PRINTING ); } // ============================================================================ // Validation Helpers // ============================================================================ /** * Get all validation messages from a document. */ function getValidationMessages(doc: Document): ValidationMessage[] { return doc.letter?.errors ?? []; } /** * Extract error messages from validation results. */ export function getErrors(doc: Document): ValidationMessage[] { return getValidationMessages(doc).filter((m: ValidationMessage) => m.type === 'ERROR'); } /** * Extract warning messages from validation results. */ export function getWarnings(doc: Document): ValidationMessage[] { return getValidationMessages(doc).filter((m: ValidationMessage) => m.type === 'WARNING'); } /** * Extract info messages from validation results. */ export function getInfoMessages(doc: Document): ValidationMessage[] { return getValidationMessages(doc).filter((m: ValidationMessage) => m.type === 'INFO'); } /** * Check if document has any validation errors. */ export function hasErrors(doc: Document): boolean { return getErrors(doc).length > 0; } /** * Check if document has any validation warnings. */ export function hasWarnings(doc: Document): boolean { return getWarnings(doc).length > 0; } /** * Get a summary of all error messages from a document. * Returns undefined if the document has no errors. * * @param doc - The document to extract error messages from * @param separator - Separator between messages (default: '; ') * @returns Concatenated error messages or undefined if no errors * * @example * ```typescript * const doc = await client.documents.get(id); * if (isErroneous(doc)) { * console.error('Upload failed:', getErrorSummary(doc)); * } * ``` */ export function getErrorSummary(doc: Document, separator: string = '; '): string | undefined { const errors = getErrors(doc); if (errors.length === 0) { return undefined; } return errors.map((e) => e.message).join(separator); } // ============================================================================ // Status Description // ============================================================================ /** * Get human-readable description of document status. */ export function getStatusDescription(status: DocumentStatus): string { switch (status) { case DocumentStatus.IN_PREPARATION: return 'In preparation'; case DocumentStatus.SHIPPABLE: return 'Ready to ship'; case DocumentStatus.PRODUCTION_QUEUE: return 'In production queue'; case DocumentStatus.PRINTING: return 'Printing'; case DocumentStatus.SENT: return 'Sent'; case DocumentStatus.CANCELED: return 'Canceled'; case DocumentStatus.ERRONEOUS: return 'Has errors'; default: return 'Unknown status'; } } // ============================================================================ // Base64 Utilities // ============================================================================ /** * Encode a file/blob to base64 string (browser environment). * Returns a promise that resolves to the base64-encoded content. */ export function fileToBase64(file: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (): void => { const result = reader.result as string; // Remove data URL prefix (e.g., "data:application/pdf;base64,") const base64 = result.split(',')[1]; if (base64) { resolve(base64); } else { reject(new Error('Failed to extract base64 content')); } }; reader.onerror = (): void => reject(reader.error); reader.readAsDataURL(file); }); } /** * Encode a Buffer to base64 string (Node.js environment). */ export function bufferToBase64(buffer: Buffer): string { return buffer.toString('base64'); } // ============================================================================ // Polling Utilities (Opt-in, no default behavior) // ============================================================================ /** * Options for polling operations. */ export interface PollOptions { /** Interval between polls in milliseconds (default: 2000) */ intervalMs?: number; /** Maximum number of poll attempts (default: 30) */ maxAttempts?: number; /** Abort signal for cancellation */ signal?: AbortSignal; } /** * Poll until a condition is met. * This is an opt-in utility - no automatic polling occurs. * * @param fn - Function to poll that returns the current state * @param condition - Condition to check against the result * @param options - Polling options * @returns The final result when condition is met * @throws Error if max attempts exceeded or aborted */ export async function pollUntil( fn: () => Promise, condition: (result: T) => boolean, options: PollOptions = {} ): Promise { const intervalMs = options.intervalMs ?? 2000; const maxAttempts = options.maxAttempts ?? 30; for (let attempt = 0; attempt < maxAttempts; attempt++) { if (options.signal?.aborted) { throw new Error('Polling aborted'); } const result = await fn(); if (condition(result)) { return result; } if (attempt < maxAttempts - 1) { await new Promise((resolve) => setTimeout(resolve, intervalMs)); } } throw new Error(`Polling exceeded maximum attempts (${maxAttempts})`); } /** * Wait for a document to reach a shippable state. * Convenience wrapper around pollUntil. * * @param getDocument - Function that fetches the document * @param options - Polling options * @returns The document when it becomes shippable or erroneous */ export async function waitForShippable( getDocument: () => Promise, options: PollOptions = {} ): Promise { return pollUntil( getDocument, (doc) => isShippable(doc) || isErroneous(doc), options ); } // ============================================================================ // Pagination Utilities // ============================================================================ /** * Options for fetchAllPages helper. */ export interface FetchAllPagesOptions { /** Number of items per page (default: 100) */ pageSize?: number; /** Abort signal for cancellation */ signal?: AbortSignal; } /** * Fetch all pages from a paginated list endpoint. * This is an opt-in convenience helper for retrieving complete result sets. * * @param fetchPage - Function that fetches a single page given limit and offset * @param options - Pagination options * @returns Array of all items from all pages * * @example * ```typescript * // Fetch all shippable documents * const allDocs = await fetchAllPages( * (limit, offset) => client.documents.list({ limit, offset }) * ); * * // Fetch all erroneous documents with custom page size * const allErrors = await fetchAllPages( * (limit, offset) => client.documents.listErrors({ limit, offset }), * { pageSize: 50 } * ); * ``` */ export async function fetchAllPages( fetchPage: (limit: number, offset: number) => Promise>, options: FetchAllPagesOptions = {} ): Promise { const pageSize = options.pageSize ?? 100; const allItems: T[] = []; let offset = 0; while (true) { if (options.signal?.aborted) { throw new Error('Fetch aborted'); } const response = await fetchPage(pageSize, offset); allItems.push(...response.items); // Check if we've fetched all items if (response.items.length < pageSize || allItems.length >= response.total) { break; } offset += pageSize; } return allItems; }