/** * Binect API client * * This module wraps the @binect/js library to provide a simplified API * for the Chrome extension. It delegates all API integration to the * upstream library. */ import { BinectClient, BinectApiError, BinectAuthError, DocumentStatus, isErroneous, getErrors, getStatusDescription, type Document as BinectDocument, type DocumentUploadOptions, EnvelopeType, FrankingType, } from '@binect/js'; // Re-export types for backward compatibility export interface Options { simplex?: boolean; // if false, it's duplex color?: boolean; // if false, it's black and white envelope?: 'DINLANG' | 'C4'; franking?: 'UNSPECIFIED' | 'STANDARD_FRANKING' | 'DV_FRANKING'; } // Document type matching what popup.ts expects export interface Document { id: number; filename: string; numberOfPages?: number; status: { code: number; text: string; }; documentType: 'Letter' | 'SerialLetter'; letter?: { letterType: 'LetterData' | 'Error'; letterData?: { recipientAddress: string; price: { priceBeforeTax: number; priceAfterTax: number; unit: string; taxInPercent: number; }; international: boolean; options: Options; }; errors?: Array<{ code: number; text: string; blankText: string; }>; }; } /** * Custom error class for Binect API errors * Wraps errors from @binect/js for backward compatibility */ export class BinectAPIError extends Error { constructor( message: string, public statusCode?: number, public response?: unknown ) { super(message); this.name = 'BinectAPIError'; } /** * Create from a @binect/js error */ static fromBinectError(error: BinectApiError | BinectAuthError): BinectAPIError { if (error instanceof BinectAuthError) { return new BinectAPIError('Invalid credentials', 401); } return new BinectAPIError( error.message, error.status, error.response ); } } /** * Convert ArrayBuffer to base64 string (browser-compatible) */ function arrayBufferToBase64(buffer: ArrayBuffer): string { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } /** * Map local Options to DocumentUploadOptions */ function mapOptions(options?: Options): Partial { if (!options) { return { simplex: false, // duplex by default color: false, // black and white by default }; } const mapped: Partial = { simplex: options.simplex ?? false, color: options.color ?? false, }; if (options.envelope) { mapped.envelope = options.envelope === 'C4' ? EnvelopeType.C4 : EnvelopeType.DINLANG; } if (options.franking) { switch (options.franking) { case 'STANDARD_FRANKING': mapped.franking = FrankingType.STANDARD_FRANKING; break; case 'DV_FRANKING': mapped.franking = FrankingType.DV_FRANKING; break; default: mapped.franking = FrankingType.UNSPECIFIED; } } return mapped; } /** * Convert BinectDocument to our Document interface */ function mapDocument(doc: BinectDocument): Document { // Build letter data if present let letterData: Document['letter'] = undefined; if (doc.letter) { const ld = doc.letter.letterData; letterData = { letterType: (doc.letter.letterType || 'LetterData') as 'LetterData' | 'Error', letterData: ld ? { recipientAddress: ld.recipientAddress || '', price: ld.price || { priceBeforeTax: 0, priceAfterTax: 0, unit: 'EUROCENT', taxInPercent: 0 }, international: ld.international || false, options: { simplex: ld.options?.simplex, color: ld.options?.color, envelope: ld.options?.envelope as 'DINLANG' | 'C4' | undefined, franking: ld.options?.franking as 'UNSPECIFIED' | 'STANDARD_FRANKING' | 'DV_FRANKING' | undefined, }, } : undefined, errors: doc.letter.errors?.map(e => ({ code: parseInt(e.code, 10) || 0, text: e.message, blankText: e.message, })), }; } return { id: doc.id, filename: doc.filename || 'document.pdf', numberOfPages: doc.numberOfPages, status: { code: doc.status.code, text: doc.status.text, }, documentType: (doc.documentType === 'SERIALLETTER' ? 'SerialLetter' : 'Letter') as 'Letter' | 'SerialLetter', letter: letterData, }; } /** * Upload PDF to Binect API * * Uses the @binect/js library to upload the PDF. * * @param pdfData - PDF file as ArrayBuffer * @param filename - Name of the PDF file * @param username - Binect username for authentication * @param password - Binect password for authentication * @param options - Optional printing options (simplex/duplex, color/bw, etc.) * @returns Document object with ID and status */ export async function uploadPDF( pdfData: ArrayBuffer, filename: string, username: string, password: string, options?: Options ): Promise { console.log('[Binect API] Uploading PDF to Binect via @binect/js...'); console.log('[Binect API] Filename:', filename); console.log('[Binect API] PDF size:', pdfData.byteLength, 'bytes'); try { // Create client with credentials const client = new BinectClient({ username, password, }); // Convert PDF to base64 console.log('[Binect API] Converting PDF to base64...'); const base64Content = arrayBufferToBase64(pdfData); console.log('[Binect API] Base64 length:', base64Content.length, 'characters'); // Map options const uploadOptions = mapOptions(options); console.log('[Binect API] Upload options:', uploadOptions); // Upload document const doc = await client.documents.upload({ content: base64Content, filename, ...uploadOptions, }); console.log('[Binect API] Upload successful!'); console.log('[Binect API] Document ID:', doc.id); console.log('[Binect API] Document status:', doc.status); // Log if document has errors (status ERRONEOUS) // But still return the document so we can track it and offer delete if (isErroneous(doc)) { console.warn('[Binect API] Document is erroneous:', doc.status.text); const errors = getErrors(doc); if (errors.length > 0) { console.warn('[Binect API] Document errors:', errors.map(e => e.message).join('; ')); } } return mapDocument(doc); } catch (error) { console.error('[Binect API] Upload error:', error); // Handle @binect/js errors if (error instanceof BinectAuthError) { throw new BinectAPIError('Invalid credentials', 401); } if (error instanceof BinectApiError) { // Map specific status codes if (error.status === 400) { throw new BinectAPIError( 'Invalid request. Please check the PDF format and size.', 400 ); } if (error.status === 413) { throw new BinectAPIError('File size exceeds limit (12 MB)', 413); } throw BinectAPIError.fromBinectError(error); } // Already a BinectAPIError if (error instanceof BinectAPIError) { throw error; } // Network or other errors if (error instanceof TypeError && error.message.includes('fetch')) { throw new BinectAPIError( 'Cannot reach Binect API. Please check your internet connection.' ); } throw new BinectAPIError( `Network error: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } /** * Test API connectivity by fetching account information * * @param username - Binect username * @param password - Binect password * @returns true if authentication successful, false otherwise */ export async function testConnection(username: string, password: string): Promise { console.log('[Binect API] Testing connection via @binect/js...'); try { const client = new BinectClient({ username, password, }); // Attempt to get account info await client.accounts.get(); console.log('[Binect API] Connection successful'); return true; } catch (error) { if (error instanceof BinectAuthError) { console.log('[Binect API] Authentication failed'); return false; } if (error instanceof BinectApiError) { console.warn('[Binect API] API error:', error.message); // If we get any other API error, credentials might still be valid // but there's another issue return false; } console.error('[Binect API] Connection test error:', error); return false; } } /** * Document status information returned by getDocumentStatus */ export interface DocumentStatusInfo { status: number; statusText: string; price?: number; recipientAddress?: string; errorDetails?: string; // Error details for erroneous documents } /** * Ship a document (place order for production) * * This announces the document for delivery, transitioning it from * SHIPPABLE to PRODUCTION_QUEUE. * * @param documentId - Binect document ID * @param username - Binect username * @param password - Binect password * @returns Updated status info */ export async function shipDocument( documentId: number, username: string, password: string ): Promise { console.log('[Binect API] Shipping document:', documentId); try { const client = new BinectClient({ username, password, }); // Send the document for production const sending = await client.sendings.send(String(documentId)); console.log('[Binect API] Document shipped successfully'); console.log('[Binect API] New status:', sending.status); return { status: sending.status, statusText: getStatusDescription(sending.status), price: sending.price, }; } catch (error) { console.error('[Binect API] Ship error:', error); if (error instanceof BinectAuthError) { throw new BinectAPIError('Invalid credentials', 401); } if (error instanceof BinectApiError) { // Check for insufficient balance (error code 2330) if (error.message.includes('2330') || error.message.includes('balance')) { throw new BinectAPIError('Insufficient account balance', 402); } throw BinectAPIError.fromBinectError(error); } if (error instanceof BinectAPIError) { throw error; } throw new BinectAPIError( `Failed to ship document: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } /** * Get current status of a document * * @param documentId - Binect document ID * @param username - Binect username * @param password - Binect password * @returns Current status info */ export async function getDocumentStatus( documentId: number, username: string, password: string ): Promise { console.log('[Binect API] Getting document status:', documentId); try { const client = new BinectClient({ username, password, }); // Fetch document details const doc = await client.documents.get(String(documentId)); console.log('[Binect API] Document status:', doc.status); // Extract price and recipient if available let price: number | undefined; let recipientAddress: string | undefined; let errorDetails: string | undefined; if (doc.letter?.letterData) { price = doc.letter.letterData.price?.priceAfterTax; recipientAddress = doc.letter.letterData.recipientAddress; } // Extract error details for erroneous documents if (isErroneous(doc)) { const errors = getErrors(doc); if (errors.length > 0) { errorDetails = errors.map(e => e.message).join('; '); console.log('[Binect API] Document errors:', errorDetails); } } return { status: doc.status.code, statusText: doc.status.text || getStatusDescription(doc.status.code), price, recipientAddress, errorDetails, }; } catch (error) { console.error('[Binect API] Get status error:', error); if (error instanceof BinectAuthError) { throw new BinectAPIError('Invalid credentials', 401); } if (error instanceof BinectApiError) { // Check for 404 - document not found (deleted on server) if (error.status === 404) { throw new BinectAPIError('Document not found on server', 404); } throw BinectAPIError.fromBinectError(error); } if (error instanceof BinectAPIError) { throw error; } throw new BinectAPIError( `Failed to get document status: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } /** * Server document info for sync */ export interface ServerDocument { id: number; filename: string; status: number; statusText: string; price?: number; recipientAddress?: string; errorDetails?: string; } /** * List all shippable documents from the server * * @param username - Binect username * @param password - Binect password * @returns Array of server documents */ export async function listServerDocuments( username: string, password: string ): Promise { console.log('[Binect API] Listing server documents...'); try { const client = new BinectClient({ username, password, }); // Get shippable documents (status 2) const shippableResponse = await client.documents.list(); const shippable = shippableResponse.items || []; // Get erroneous documents (status 7) const errorsResponse = await client.documents.listErrors(); const erroneous = errorsResponse.items || []; // Combine and map to our format const allDocs = [...shippable, ...erroneous]; console.log('[Binect API] Found', allDocs.length, 'documents on server (', shippable.length, 'shippable,', erroneous.length, 'erroneous)'); return allDocs.map(doc => { let errorDetails: string | undefined; if (isErroneous(doc)) { const errors = getErrors(doc); if (errors.length > 0) { errorDetails = errors.map(e => e.message).join('; '); } } return { id: doc.id, filename: doc.filename || 'document.pdf', status: doc.status.code, statusText: doc.status.text || getStatusDescription(doc.status.code), price: doc.letter?.letterData?.price?.priceAfterTax, recipientAddress: doc.letter?.letterData?.recipientAddress, errorDetails, }; }); } catch (error) { console.error('[Binect API] List documents error:', error); if (error instanceof BinectAuthError) { throw new BinectAPIError('Invalid credentials', 401); } if (error instanceof BinectApiError) { throw BinectAPIError.fromBinectError(error); } throw new BinectAPIError( `Failed to list documents: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } /** * Delete a document from the server * * @param documentId - Binect document ID * @param username - Binect username * @param password - Binect password */ export async function deleteDocument( documentId: number, username: string, password: string ): Promise { console.log('[Binect API] Deleting document:', documentId); try { const client = new BinectClient({ username, password, }); await client.documents.delete(String(documentId)); console.log('[Binect API] Document deleted successfully'); } catch (error) { console.error('[Binect API] Delete error:', error); if (error instanceof BinectAuthError) { throw new BinectAPIError('Invalid credentials', 401); } if (error instanceof BinectApiError) { if (error.status === 404) { // Already deleted, treat as success console.log('[Binect API] Document already deleted (404)'); return; } throw BinectAPIError.fromBinectError(error); } if (error instanceof BinectAPIError) { throw error; } throw new BinectAPIError( `Failed to delete document: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } // Re-export DocumentStatus enum for use in other modules export { DocumentStatus };