/** * 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 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 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; } // ============================================================================ // 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 ); }