From 327943bc18126fa4c448f96c2cc26517f275a93f Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 16 Jan 2026 22:41:43 +0100 Subject: [PATCH] Server sync, erroneous doc handling, and @binect/js v0.1.0 integration - Add server sync to discover documents uploaded elsewhere (fixes missing basket documents issue) - Handle erroneous uploads: preserve binectDocumentId for delete button - Add "Delete from server" button for erroneous and canceled documents - Remove archive button for active documents (in_basket, in_production) - Auto-restore archived documents that have active status - Refactor to use @binect/js v0.1.0 features: - DocumentStatus enum instead of magic numbers - isErroneous(), getErrors() helper functions - getStatusDescription() for status text - Add binect-js improvement requirements document Co-Authored-By: Claude Sonnet 4.5 --- docs/binect-js-improvements.md | 294 +++++++++++++++++++++++++++++++ src/background/service-worker.ts | 69 +++++++- src/popup/popup.css | 17 ++ src/popup/popup.ts | 231 ++++++++++++++++++++---- src/utils/binect-api.ts | 190 ++++++++++++++++---- src/utils/pdf-queue.ts | 7 +- 6 files changed, 739 insertions(+), 69 deletions(-) create mode 100644 docs/binect-js-improvements.md diff --git a/docs/binect-js-improvements.md b/docs/binect-js-improvements.md new file mode 100644 index 0000000..4f8ec31 --- /dev/null +++ b/docs/binect-js-improvements.md @@ -0,0 +1,294 @@ +# @binect/js Library Improvement Requirements + +**Version:** 1.1 +**Date:** 2026-01-16 +**Author:** BinectChrome Development Team +**Status:** Updated after v0.1.0 review + +--- + +## Executive Summary + +This document outlines suggested improvements to the `@binect/js` library based on real-world integration experience building the BinectChrome browser extension. + +**Update (v1.1):** After reviewing `@binect/js` v0.1.0, most requirements have been addressed. This document now reflects the current status and remaining gaps. + +--- + +## Requirements Status Summary + +| Requirement | Status | Notes | +|-------------|--------|-------| +| REQ-1: Status Constants | ✅ **ADDRESSED** | `DocumentStatus` enum exported | +| REQ-2: listAll() Method | ❌ **OPEN** | Still requires 2 API calls | +| REQ-3: Error Accessibility | ✅ **ADDRESSED** | Helper functions added | +| REQ-4: Document ListResponse | ✅ **ADDRESSED** | JSDoc comments improved | +| REQ-5: Pagination Docs | ⚠️ **PARTIAL** | Interface exists, no fetchAll helper | +| REQ-6: Error Type Definitions | ✅ **ADDRESSED** | `ValidationMessage` interface | + +--- + +## Addressed Requirements + +### REQ-1: Export Document Status Constants ✅ + +**Status:** Fully addressed in v0.1.0 + +The library now exports a `DocumentStatus` enum: + +```typescript +export enum DocumentStatus { + IN_PREPARATION = 1, + SHIPPABLE = 2, + PRODUCTION_QUEUE = 3, + PRINTING = 4, + SENT = 5, + CANCELED = 6, + ERRONEOUS = 7 +} +``` + +**Usage:** +```typescript +import { DocumentStatus } from '@binect/js'; + +if (doc.status.code === DocumentStatus.ERRONEOUS) { + // Handle erroneous document +} +``` + +--- + +### REQ-3: Improve Error Information Accessibility ✅ + +**Status:** Fully addressed in v0.1.0 + +The library now exports comprehensive helper functions: + +```typescript +// Status predicates +isShippable(doc) // status === 2 +isErroneous(doc) // status === 7 +isInPreparation(doc) // status === 1 +isInProductionQueue(doc) // status === 3 +isPrinting(doc) // status === 4 +isSent(doc) // status === 5 +isCanceled(doc) // status === 6 +isTerminal(doc) // status in [5, 6, 7] +isCancelable(doc) // status in [3, 4] + +// Error extraction +getErrors(doc) // ValidationMessage[] of type 'ERROR' +getWarnings(doc) // ValidationMessage[] of type 'WARNING' +getInfoMessages(doc) // ValidationMessage[] of type 'INFO' +hasErrors(doc) // boolean +hasWarnings(doc) // boolean + +// Status description +getStatusDescription(status) // Human-readable string +``` + +**Usage:** +```typescript +import { isErroneous, getErrors } from '@binect/js'; + +if (isErroneous(doc)) { + const errors = getErrors(doc); + console.error('Errors:', errors.map(e => e.message).join('; ')); +} +``` + +--- + +### REQ-4: Document ListResponse Structure ✅ + +**Status:** Addressed in v0.1.0 + +The `ListResponse` interface now has clear JSDoc documentation: + +```typescript +/** + * List response wrapper + */ +export interface ListResponse { + items: T[]; + total: number; + limit: number; + offset: number; +} +``` + +--- + +### REQ-6: Improve Type Definitions for Error Objects ✅ + +**Status:** Addressed in v0.1.0 + +The `ValidationMessage` interface is now properly typed: + +```typescript +export interface ValidationMessage { + type: 'INFO' | 'WARNING' | 'ERROR'; + code: string; + message: string; + page?: number; +} +``` + +--- + +## Open Requirements + +### REQ-2: Add Method to List All Documents ❌ + +**Status:** NOT ADDRESSED +**Priority:** Medium + +#### Problem Statement + +There is still no single method to retrieve all documents regardless of status: + +```typescript +// Current: Multiple calls still required +const shippable = await client.documents.list(); // Only status 2 +const erroneous = await client.documents.listErrors(); // Only status 7 +// Still missing: status 1, 3, 4, 5, 6 +``` + +#### API Limitation + +This may be a limitation of the Binect REST API itself, not the JS library. The API only provides: +- `GET /documents` - Returns shippable documents (status 2) +- `GET /documents/errors` - Returns erroneous documents (status 7) + +There is no endpoint to list documents in other states (in_preparation, in_production, sent, canceled). + +#### Proposed Solutions + +**Option A: Library-level aggregation** (if API supports individual document lookup) +```typescript +// Library could provide a helper that fetches known IDs +async listByIds(documentIds: string[]): Promise; +``` + +**Option B: Document the limitation** +- Clearly document which documents each endpoint returns +- Explain that documents in production (3, 4) cannot be listed, only queried by ID +- Provide example of tracking document IDs locally + +**Option C: API Enhancement Request** +- Request Binect API team to add `GET /documents/all` or status filter parameter: + ``` + GET /documents?status=1,2,3,4,5,6,7 + ``` + +#### Impact on BinectChrome + +Currently, BinectChrome can only discover: +- Documents ready to ship (status 2) +- Documents with errors (status 7) + +Documents in production (3, 4), sent (5), or canceled (6) can only be tracked if we uploaded them and stored their IDs locally. + +--- + +### REQ-5: Document and Improve Pagination ⚠️ + +**Status:** PARTIALLY ADDRESSED +**Priority:** Low + +#### What's Addressed + +- `PaginationOptions` interface is exported +- Methods accept pagination parameters + +```typescript +export interface PaginationOptions { + limit?: number; + offset?: number; +} +``` + +#### What's Missing + +1. **Documentation of default values** - What is the default limit? +2. **fetchAll helper** - No built-in way to fetch all pages automatically + +#### Proposed Addition + +```typescript +// Optional helper for fetching all pages +async function fetchAllDocuments(client: BinectClient): Promise { + const allDocs: Document[] = []; + let offset = 0; + const limit = 100; + + while (true) { + const response = await client.documents.list({ limit, offset }); + allDocs.push(...response.items); + if (response.items.length < limit) break; + offset += limit; + } + + return allDocs; +} +``` + +This could be added to the helpers module as `fetchAll()` or similar. + +--- + +## New Features in v0.1.0 + +The following features were added that weren't in our original requirements: + +### Polling Utilities + +```typescript +import { pollUntil, waitForShippable } from '@binect/js'; + +// Wait for document to become shippable or erroneous +const doc = await waitForShippable( + () => client.documents.get(docId), + { intervalMs: 2000, maxAttempts: 30 } +); + +// Generic polling +const result = await pollUntil( + () => fetchSomething(), + (result) => result.status === 'complete', + { intervalMs: 1000 } +); +``` + +### Encoding Helpers + +```typescript +import { fileToBase64, bufferToBase64 } from '@binect/js'; + +// Browser: File/Blob to base64 +const base64 = await fileToBase64(file); + +// Node.js: Buffer to base64 +const base64 = bufferToBase64(buffer); +``` + +--- + +## Recommendations for BinectChrome + +Based on the updated library, we should: + +1. **Refactor to use `DocumentStatus` enum** instead of magic numbers +2. **Use helper functions** like `isErroneous()`, `getErrors()` instead of manual checks +3. **Use `getStatusDescription()`** for human-readable status text +4. **Consider using `waitForShippable()`** for upload flow instead of manual polling + +--- + +## Revision History + +| Version | Date | Author | Changes | +|---------|------|--------|---------| +| 1.0 | 2026-01-16 | BinectChrome Team | Initial draft | +| 1.1 | 2026-01-16 | BinectChrome Team | Updated after v0.1.0 review - marked addressed requirements | diff --git a/src/background/service-worker.ts b/src/background/service-worker.ts index 811f05e..6ccd91f 100644 --- a/src/background/service-worker.ts +++ b/src/background/service-worker.ts @@ -18,10 +18,11 @@ import { dismissPDF, removePDF, cleanupOldEntries, + syncFromServer, PDFStatus, PDFStatusMeta } from '../utils/pdf-queue'; -import { shipDocument, getDocumentStatus } from '../utils/binect-api'; +import { shipDocument, getDocumentStatus, deleteDocument, listServerDocuments } from '../utils/binect-api'; /** * Initialize extension on install @@ -267,6 +268,72 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { return true; } + // Delete a document from the server + if (request.action === 'deleteServerDocument') { + const { documentId, username, password } = request as { + documentId: number; + username: string; + password: string; + }; + + deleteDocument(documentId, username, password) + .then(() => { + sendResponse({ success: true }); + }) + .catch(error => { + sendResponse({ + success: false, + error: error instanceof Error ? error.message : 'Failed to delete document' + }); + }); + return true; + } + + // List all documents from the server (for sync) + if (request.action === 'listServerDocuments') { + const { username, password } = request as { + username: string; + password: string; + }; + + listServerDocuments(username, password) + .then(documents => { + sendResponse({ success: true, documents }); + }) + .catch(error => { + sendResponse({ + success: false, + error: error instanceof Error ? error.message : 'Failed to list documents' + }); + }); + return true; + } + + // Sync a server document to local proxy (create or update) + if (request.action === 'syncFromServer') { + const { binectDocumentId, filename, binectStatusCode, binectStatusText, price, recipientAddress, errorDetails } = request as { + binectDocumentId: number; + filename: string; + binectStatusCode: number; + binectStatusText: string; + price?: number; + recipientAddress?: string; + errorDetails?: string; + }; + + syncFromServer(binectDocumentId, filename, binectStatusCode, binectStatusText, price, recipientAddress, errorDetails) + .then(proxy => { + sendResponse({ success: true, proxy }); + }) + .catch(error => { + sendResponse({ + success: false, + error: error instanceof Error ? error.message : 'Failed to sync document' + }); + }); + return true; + } + // Legacy handlers for backward compatibility if (request.action === 'getLastPDF') { getPendingPDFs().then(entries => { diff --git a/src/popup/popup.css b/src/popup/popup.css index 0307f68..2f09bee 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -611,6 +611,23 @@ body { background: var(--binect-blue-deep); } +/* Delete from server button */ +.btn-delete-server { + padding: 4px 8px; + font-size: 10px; + background: transparent; + color: var(--red); + border: 1px solid var(--red); + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; +} + +.btn-delete-server:hover { + background: var(--red); + color: white; +} + /* Order button */ .btn-order-item { padding: 6px 12px; diff --git a/src/popup/popup.ts b/src/popup/popup.ts index 02cc4f4..4d1bad3 100644 --- a/src/popup/popup.ts +++ b/src/popup/popup.ts @@ -253,6 +253,27 @@ async function loadPDFQueue() { pdfQueue = response?.entries || []; console.log('[Popup] Got', pdfQueue.length, 'live entries from background'); + // Sync with server to discover documents uploaded elsewhere or missing locally + if (currentCredentials) { + await syncWithServer(); + } + + // Auto-restore any archived documents that are in active states (in_basket, in_production) + // These should never be archived as it confuses user expectations + const archivedResponse = await chrome.runtime.sendMessage({ action: 'getArchivedProxies' }); + const archivedEntries: DocumentProxy[] = archivedResponse?.entries || []; + for (const entry of archivedEntries) { + if (entry.binectStatus === 'in_basket' || entry.binectStatus === 'in_production') { + console.log('[Popup] Auto-restoring active document from archive:', entry.filename); + await chrome.runtime.sendMessage({ action: 'restoreProxy', id: entry.id }); + } + } + + // Reload live proxies after potential restorations and server sync + response = await chrome.runtime.sendMessage({ action: 'getLiveProxies' }); + pdfQueue = response?.entries || []; + console.log('[Popup] After auto-restore and sync, got', pdfQueue.length, 'live entries'); + // Check current tab for PDF and add to persistent queue via background const currentTabPDF = await checkCurrentTabForPDF(); if (currentTabPDF) { @@ -289,6 +310,70 @@ async function loadPDFQueue() { renderPDFList(); } +/** + * Sync local proxies with server documents + * Creates local proxies for documents that exist on server but not locally + */ +async function syncWithServer() { + if (!currentCredentials) { + console.log('[Popup] No credentials, skipping server sync'); + return; + } + + try { + console.log('[Popup] Syncing with Binect server...'); + + // Get list of documents from server + const result = await chrome.runtime.sendMessage({ + action: 'listServerDocuments', + username: currentCredentials.username, + password: currentCredentials.password + }); + + console.log('[Popup] listServerDocuments result:', result); + + if (!result.success || !result.documents) { + console.warn('[Popup] Failed to list server documents:', result.error); + return; + } + + const serverDocs = result.documents as Array<{ + id: number; + filename: string; + status: number; + statusText: string; + price?: number; + recipientAddress?: string; + errorDetails?: string; + }>; + + console.log('[Popup] Found', serverDocs.length, 'documents on server'); + for (const doc of serverDocs) { + console.log('[Popup] Server doc:', doc.id, doc.filename, 'status:', doc.status, doc.statusText); + } + + // Sync each server document to local proxy + for (const doc of serverDocs) { + console.log('[Popup] Syncing doc', doc.id, 'to local proxy...'); + const syncResult = await chrome.runtime.sendMessage({ + action: 'syncFromServer', + binectDocumentId: doc.id, + filename: doc.filename, + binectStatusCode: doc.status, + binectStatusText: doc.statusText, + price: doc.price, + recipientAddress: doc.recipientAddress, + errorDetails: doc.errorDetails + }); + console.log('[Popup] Sync result for doc', doc.id, ':', syncResult); + } + + console.log('[Popup] Server sync complete'); + } catch (error) { + console.error('[Popup] Server sync error:', error); + } +} + /** * Check recent downloads for PDFs (fallback mechanism) */ @@ -428,33 +513,44 @@ function renderPDFItem(pdf: DocumentProxy, section: 'pending' | 'basket' | 'prod let actionsHtml = ''; + // Check if document can be deleted from server (erroneous or canceled) + const canDeleteFromServer = pdf.binectDocumentId && (pdf.binectStatusCode === 7 || pdf.binectStatusCode === 6); + switch (section) { case 'pending': actionsHtml = ` - + ${canDeleteFromServer + ? `` + : `` + } `; break; case 'basket': + // No archive button for in_basket - these are active documents actionsHtml = ` - `; break; case 'production': - // Archive button only - actionsHtml = ` - - `; + // No archive button for in_production - these are active documents + actionsHtml = ''; break; case 'completed': - actionsHtml = ` - - `; + // For sent/canceled documents - offer delete from server if applicable + if (canDeleteFromServer) { + actionsHtml = ` + + `; + } else { + actionsHtml = ` + + `; + } break; case 'archived': actionsHtml = ` @@ -526,6 +622,14 @@ function setupPDFListEventListeners() { }); }); + // Delete from server buttons + pdfList.querySelectorAll('.btn-delete-server').forEach(btn => { + btn.addEventListener('click', (e) => { + const id = (e.target as HTMLElement).dataset.id; + if (id) handleDeleteFromServer(id); + }); + }); + // Delete/Remove buttons pdfList.querySelectorAll('.btn-dismiss').forEach(btn => { btn.addEventListener('click', (e) => { @@ -647,13 +751,14 @@ async function handleSendPDF(id: string) { currentCredentials.password ); - // Track successful transfer + // Track transfer await addTrackingEntry({ timestamp: Date.now(), sourceDomain: pdf.sourceDomain, destinationUrl: 'https://api.binect.de/binectapi/v1/documents', pdfSize: pdfBytes.byteLength, - result: 'success' + result: document.status.code === 7 ? 'failure' : 'success', + errorMessage: document.status.code === 7 ? document.status.text : undefined }); // Update last use timestamp @@ -672,27 +777,57 @@ async function handleSendPDF(id: string) { meta.recipientAddress = document.letter.letterData.recipientAddress; } - // Update status to in_basket (document is now SHIPPABLE) - await chrome.runtime.sendMessage({ - action: 'updatePDFStatus', - id, - status: 'in_basket', - meta - }); + // Check if document is erroneous (status 7) + if (document.status.code === 7) { + // Extract error message + let errorMessage = document.status.text || 'Document has errors'; + if (document.letter?.errors && document.letter.errors.length > 0) { + errorMessage = document.letter.errors.map(e => e.text || e.blankText).join('; '); + } + meta.errorMessage = errorMessage; - // Update local state - pdf.binectStatus = 'in_basket'; - pdf.binectDocumentId = document.id; - pdf.binectStatusCode = document.status.code; - pdf.binectStatusText = document.status.text; - pdf.contentHash = contentHash; - if (document.letter?.letterData) { - pdf.price = document.letter.letterData.price?.priceAfterTax; - pdf.recipientAddress = document.letter.letterData.recipientAddress; + // Update status to failed (erroneous) + await chrome.runtime.sendMessage({ + action: 'updatePDFStatus', + id, + status: 'failed', + meta + }); + + // Update local state + pdf.binectStatus = 'failed'; + pdf.binectDocumentId = document.id; + pdf.binectStatusCode = document.status.code; + pdf.binectStatusText = document.status.text; + pdf.contentHash = contentHash; + pdf.errorMessage = errorMessage; + renderPDFList(); + + showStatus(`Document has errors: ${errorMessage}`, 'error'); + } else { + // Document is shippable (status 2) - in basket + // Update status to in_basket + await chrome.runtime.sendMessage({ + action: 'updatePDFStatus', + id, + status: 'in_basket', + meta + }); + + // Update local state + pdf.binectStatus = 'in_basket'; + pdf.binectDocumentId = document.id; + pdf.binectStatusCode = document.status.code; + pdf.binectStatusText = document.status.text; + pdf.contentHash = contentHash; + if (document.letter?.letterData) { + pdf.price = document.letter.letterData.price?.priceAfterTax; + pdf.recipientAddress = document.letter.letterData.recipientAddress; + } + renderPDFList(); + + showStatus(`Uploaded! Ready to send (${(pdf.price || 0) / 100} €)`, 'success'); } - renderPDFList(); - - showStatus(`Uploaded! Ready to order (${(pdf.price || 0) / 100} €)`, 'success'); // Start auto-refresh sequence startAutoRefresh(); @@ -961,6 +1096,42 @@ async function handleRestorePDF(id: string) { setTimeout(() => hideStatus(), 2000); } +/** + * Handle delete from server (for erroneous or canceled documents) + */ +async function handleDeleteFromServer(id: string) { + const pdf = pdfQueue.find(p => p.id === id); + if (!pdf || !currentCredentials || !pdf.binectDocumentId) { + return; + } + + try { + const result = await chrome.runtime.sendMessage({ + action: 'deleteServerDocument', + documentId: pdf.binectDocumentId, + username: currentCredentials.username, + password: currentCredentials.password + }); + + if (!result.success) { + throw new Error(result.error || 'Failed to delete from server'); + } + + // Remove from local queue after successful server deletion + await chrome.runtime.sendMessage({ action: 'removePDF', id }); + pdfQueue = pdfQueue.filter(p => p.id !== id); + renderPDFList(); + + showStatus('Document deleted from server', 'success'); + setTimeout(() => hideStatus(), 2000); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Delete failed'; + showStatus(errorMessage, 'error'); + setTimeout(() => hideStatus(), 3000); + } +} + /** * Handle delete PDF (permanently remove) */ diff --git a/src/utils/binect-api.ts b/src/utils/binect-api.ts index 053010f..567b98b 100644 --- a/src/utils/binect-api.ts +++ b/src/utils/binect-api.ts @@ -10,6 +10,10 @@ import { BinectClient, BinectApiError, BinectAuthError, + DocumentStatus, + isErroneous, + getErrors, + getStatusDescription, type Document as BinectDocument, type DocumentUploadOptions, EnvelopeType, @@ -225,25 +229,14 @@ export async function uploadPDF( console.log('[Binect API] Document ID:', doc.id); console.log('[Binect API] Document status:', doc.status); - // Check if document has errors - if (doc.letter?.letterType === 'Error' && doc.letter.errors) { - console.warn('[Binect API] Document has errors:', doc.letter.errors); - const errorMessages = doc.letter.errors.map(e => e.message).join('; '); - throw new BinectAPIError( - `Document validation failed: ${errorMessages}`, - 200, - doc - ); - } - - // Status code 7 = erroneous - if (doc.status.code === 7) { - console.error('[Binect API] Document is erroneous:', doc.status.text); - throw new BinectAPIError( - `Document is erroneous: ${doc.status.text}`, - 200, - doc - ); + // 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); @@ -369,7 +362,7 @@ export async function shipDocument( return { status: sending.status, - statusText: getStatusText(sending.status), + statusText: getStatusDescription(sending.status), price: sending.price, }; } catch (error) { @@ -433,15 +426,18 @@ export async function getDocumentStatus( recipientAddress = doc.letter.letterData.recipientAddress; } - // Extract error details for erroneous documents (status 7) - if (doc.status.code === 7 && doc.letter?.errors && doc.letter.errors.length > 0) { - errorDetails = doc.letter.errors.map(e => e.message).join('; '); - console.log('[Binect API] Document errors:', errorDetails); + // 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 || getStatusText(doc.status.code), + statusText: doc.status.text || getStatusDescription(doc.status.code), price, recipientAddress, errorDetails, @@ -472,17 +468,139 @@ export async function getDocumentStatus( } /** - * Get human-readable status text for a Binect status code + * Server document info for sync */ -function getStatusText(statusCode: number): string { - switch (statusCode) { - case 1: return 'In preparation'; - case 2: return 'Ready to ship'; - case 3: return 'In production queue'; - case 4: return 'Printing'; - case 5: return 'Sent'; - case 6: return 'Canceled'; - case 7: return 'Has errors'; - default: return 'Unknown status'; +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) + console.log('[Binect API] Fetching shippable documents...'); + const shippableResponse = await client.documents.list(); + console.log('[Binect API] Shippable response:', JSON.stringify(shippableResponse)); + const shippable = shippableResponse.items || []; + console.log('[Binect API] Found', shippable.length, 'shippable documents'); + + // Get erroneous documents (status 7) + console.log('[Binect API] Fetching erroneous documents...'); + const errorsResponse = await client.documents.listErrors(); + console.log('[Binect API] Errors response:', JSON.stringify(errorsResponse)); + const erroneous = errorsResponse.items || []; + console.log('[Binect API] Found', erroneous.length, 'erroneous documents'); + + // Combine and map to our format + const allDocs = [...shippable, ...erroneous]; + + console.log('[Binect API] Total documents on server:', allDocs.length); + + 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 }; diff --git a/src/utils/pdf-queue.ts b/src/utils/pdf-queue.ts index 8ae6464..319d1e8 100644 --- a/src/utils/pdf-queue.ts +++ b/src/utils/pdf-queue.ts @@ -205,7 +205,8 @@ export async function syncFromServer( binectStatusCode: number, binectStatusText: string, price?: number, - recipientAddress?: string + recipientAddress?: string, + errorMessage?: string ): Promise { const state = await loadQueue(); @@ -219,6 +220,7 @@ export async function syncFromServer( proxy.binectStatus = mapBinectStatusCode(binectStatusCode); if (price !== undefined) proxy.price = price; if (recipientAddress) proxy.recipientAddress = recipientAddress; + if (errorMessage) proxy.errorMessage = errorMessage; } else { // Create new proxy from server data proxy = { @@ -234,7 +236,8 @@ export async function syncFromServer( binectStatusCode, binectStatusText, price, - recipientAddress + recipientAddress, + errorMessage }; state.entries.unshift(proxy); }