generated from coulomb/repo-seed
Add Binect SDK implementation, Explorer, and test suite
SDK (@binect/js): - BinectClient with domain sub-clients (documents, sendings, accounts, attachments, invoices) - HTTP Basic Auth, native fetch only (no runtime dependencies) - TypeScript types matching Binect API vocabulary - Status predicates and polling helpers in helpers.ts - Structured error handling (BinectApiError, BinectAuthError) Explorer: - Standalone browser-based API explorer (explorer/index.html) - Interactive testing without code Tests: - Unit tests for client, types, errors, helpers, http - E2E tests for upload/delete and send/cancel workflows Also includes: - Architecture Decision Records (ADRs) - Example DIN 5008 letter PDFs for testing - API specification research notes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
75
src/client.ts
Normal file
75
src/client.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { HttpClient, DEFAULT_BASE_URL } from './http.js';
|
||||
import { DocumentsClient } from './clients/documents.js';
|
||||
import { AttachmentsClient } from './clients/attachments.js';
|
||||
import { SendingsClient } from './clients/sendings.js';
|
||||
import { AccountsClient } from './clients/accounts.js';
|
||||
import { InvoicesClient } from './clients/invoices.js';
|
||||
import type { BinectClientConfig } from './types.js';
|
||||
|
||||
/**
|
||||
* Main client for interacting with the Binect API.
|
||||
*
|
||||
* Provides access to all API domains through sub-clients:
|
||||
* - documents: Upload, manage, and preview documents
|
||||
* - attachments: Manage standalone attachments
|
||||
* - sendings: Announce, send, and track mail dispatch
|
||||
* - accounts: Access account info, options, and journals
|
||||
* - invoices: List and download invoices
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const client = new BinectClient({
|
||||
* username: 'user@example.com',
|
||||
* password: 'your-password',
|
||||
* });
|
||||
*
|
||||
* // Upload a document
|
||||
* const doc = await client.documents.upload({
|
||||
* content: base64PdfContent,
|
||||
* color: false,
|
||||
* duplex: true,
|
||||
* });
|
||||
*
|
||||
* // Send when ready
|
||||
* if (doc.status === DocumentStatus.SHIPPABLE) {
|
||||
* await client.sendings.send(doc.documentId);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class BinectClient {
|
||||
private readonly http: HttpClient;
|
||||
|
||||
/** Client for document operations */
|
||||
public readonly documents: DocumentsClient;
|
||||
|
||||
/** Client for attachment operations */
|
||||
public readonly attachments: AttachmentsClient;
|
||||
|
||||
/** Client for sending/shipment operations */
|
||||
public readonly sendings: SendingsClient;
|
||||
|
||||
/** Client for account operations */
|
||||
public readonly accounts: AccountsClient;
|
||||
|
||||
/** Client for invoice operations */
|
||||
public readonly invoices: InvoicesClient;
|
||||
|
||||
/**
|
||||
* Creates a new Binect API client.
|
||||
*
|
||||
* @param config - Client configuration including credentials
|
||||
*/
|
||||
constructor(config: BinectClientConfig) {
|
||||
this.http = new HttpClient({
|
||||
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
});
|
||||
|
||||
this.documents = new DocumentsClient(this.http);
|
||||
this.attachments = new AttachmentsClient(this.http);
|
||||
this.sendings = new SendingsClient(this.http);
|
||||
this.accounts = new AccountsClient(this.http);
|
||||
this.invoices = new InvoicesClient(this.http);
|
||||
}
|
||||
}
|
||||
129
src/clients/accounts.ts
Normal file
129
src/clients/accounts.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import type { HttpClient } from '../http.js';
|
||||
import type {
|
||||
AccountInfo,
|
||||
PersonalData,
|
||||
PersonalDataUpdate,
|
||||
AccountPrintOptions,
|
||||
Coworker,
|
||||
JournalEntry,
|
||||
ListResponse,
|
||||
} from '../types.js';
|
||||
|
||||
/**
|
||||
* Client for account-related API operations.
|
||||
* Handles account info, personal data, print options, and journal access.
|
||||
*/
|
||||
export class AccountsClient {
|
||||
constructor(private readonly http: HttpClient) {}
|
||||
|
||||
/**
|
||||
* Get account balance and credit information.
|
||||
* GET /accounts
|
||||
*
|
||||
* @returns Account info including credit balance
|
||||
*/
|
||||
async get(): Promise<AccountInfo> {
|
||||
return this.http.request<AccountInfo>({
|
||||
method: 'GET',
|
||||
path: '/accounts',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get personal data for the account.
|
||||
* GET /accounts/personaldata
|
||||
*
|
||||
* @returns Personal data including contact information
|
||||
*/
|
||||
async getPersonalData(): Promise<PersonalData> {
|
||||
return this.http.request<PersonalData>({
|
||||
method: 'GET',
|
||||
path: '/accounts/personaldata',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update personal data for the account.
|
||||
* PATCH /accounts/personaldata
|
||||
*
|
||||
* @param data - Fields to update
|
||||
* @returns Updated personal data
|
||||
*/
|
||||
async updatePersonalData(data: PersonalDataUpdate): Promise<PersonalData> {
|
||||
return this.http.request<PersonalData>({
|
||||
method: 'PATCH',
|
||||
path: '/accounts/personaldata',
|
||||
body: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default print options for the account.
|
||||
* GET /accounts/options
|
||||
*
|
||||
* @returns Default print options
|
||||
*/
|
||||
async getOptions(): Promise<AccountPrintOptions> {
|
||||
return this.http.request<AccountPrintOptions>({
|
||||
method: 'GET',
|
||||
path: '/accounts/options',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update default print options for the account.
|
||||
* PUT /accounts/options
|
||||
*
|
||||
* @param options - New default print options
|
||||
* @returns Updated print options
|
||||
*/
|
||||
async updateOptions(options: AccountPrintOptions): Promise<AccountPrintOptions> {
|
||||
return this.http.request<AccountPrintOptions>({
|
||||
method: 'PUT',
|
||||
path: '/accounts/options',
|
||||
body: options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List coworkers associated with this account.
|
||||
* GET /accounts/coworkers
|
||||
*
|
||||
* @returns List of coworkers
|
||||
*/
|
||||
async getCoworkers(): Promise<ListResponse<Coworker>> {
|
||||
return this.http.request<ListResponse<Coworker>>({
|
||||
method: 'GET',
|
||||
path: '/accounts/coworkers',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get journal/transaction entries for a coworker in a specific month.
|
||||
* GET /accounts/coworkers/{debitornumber}/journal/{month}
|
||||
*
|
||||
* @param debitornumber - The coworker's debitor number
|
||||
* @param month - Month in YYYY-MM format
|
||||
* @returns List of journal entries
|
||||
*/
|
||||
async getCoworkerJournal(debitornumber: string, month: string): Promise<ListResponse<JournalEntry>> {
|
||||
return this.http.request<ListResponse<JournalEntry>>({
|
||||
method: 'GET',
|
||||
path: `/accounts/coworkers/${encodeURIComponent(debitornumber)}/journal/${encodeURIComponent(month)}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get journal/transaction entries for the account in a specific month.
|
||||
* GET /accounts/journal/{month}
|
||||
*
|
||||
* @param month - Month in YYYY-MM format
|
||||
* @returns List of journal entries
|
||||
*/
|
||||
async getJournal(month: string): Promise<ListResponse<JournalEntry>> {
|
||||
return this.http.request<ListResponse<JournalEntry>>({
|
||||
method: 'GET',
|
||||
path: `/accounts/journal/${encodeURIComponent(month)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
110
src/clients/attachments.ts
Normal file
110
src/clients/attachments.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { HttpClient } from '../http.js';
|
||||
import type { Attachment, AttachmentUploadOptions, ListResponse, PaginationOptions } from '../types.js';
|
||||
|
||||
/**
|
||||
* Client for attachment-related API operations.
|
||||
* Handles standalone attachment upload, retrieval, and management.
|
||||
*/
|
||||
export class AttachmentsClient {
|
||||
constructor(private readonly http: HttpClient) {}
|
||||
|
||||
/**
|
||||
* Upload a new attachment.
|
||||
* POST /attachments
|
||||
*
|
||||
* @param options - Attachment upload options including base64-encoded PDF content
|
||||
* @returns The created attachment
|
||||
*/
|
||||
async upload(options: AttachmentUploadOptions): Promise<Attachment> {
|
||||
return this.http.request<Attachment>({
|
||||
method: 'POST',
|
||||
path: '/attachments',
|
||||
body: options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List all attachments.
|
||||
* GET /attachments
|
||||
*
|
||||
* @param pagination - Optional pagination parameters
|
||||
* @returns List of attachments
|
||||
*/
|
||||
async list(pagination?: PaginationOptions): Promise<ListResponse<Attachment>> {
|
||||
return this.http.request<ListResponse<Attachment>>({
|
||||
method: 'GET',
|
||||
path: '/attachments',
|
||||
query: pagination,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific attachment by ID.
|
||||
* GET /attachments/{attachmentID}
|
||||
*
|
||||
* @param attachmentId - The attachment ID
|
||||
* @returns The attachment details
|
||||
*/
|
||||
async get(attachmentId: string): Promise<Attachment> {
|
||||
return this.http.request<Attachment>({
|
||||
method: 'GET',
|
||||
path: `/attachments/${encodeURIComponent(attachmentId)}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an attachment.
|
||||
* DELETE /attachments/{attachmentID}
|
||||
*
|
||||
* @param attachmentId - The attachment ID to delete
|
||||
*/
|
||||
async delete(attachmentId: string): Promise<void> {
|
||||
await this.http.request<void>({
|
||||
method: 'DELETE',
|
||||
path: `/attachments/${encodeURIComponent(attachmentId)}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PDF preview of an attachment.
|
||||
* GET /attachments/{attachmentID}/pdf
|
||||
*
|
||||
* @param attachmentId - The attachment ID
|
||||
* @returns Response containing PDF data
|
||||
*/
|
||||
async getPdf(attachmentId: string): Promise<Response> {
|
||||
return this.http.requestRaw({
|
||||
method: 'GET',
|
||||
path: `/attachments/${encodeURIComponent(attachmentId)}/pdf`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documents that have this attachment.
|
||||
* GET /attachments/{attachmentID}/documents
|
||||
*
|
||||
* @param attachmentId - The attachment ID
|
||||
* @returns Array of document IDs
|
||||
*/
|
||||
async getDocuments(attachmentId: string): Promise<string[]> {
|
||||
return this.http.request<string[]>({
|
||||
method: 'GET',
|
||||
path: `/attachments/${encodeURIComponent(attachmentId)}/documents`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach this attachment to multiple documents.
|
||||
* PATCH /attachments/{attachmentID}/documents
|
||||
*
|
||||
* @param attachmentId - The attachment ID
|
||||
* @param documentIds - Array of document IDs to attach to
|
||||
*/
|
||||
async attachToDocuments(attachmentId: string, documentIds: string[]): Promise<void> {
|
||||
await this.http.request<void>({
|
||||
method: 'PATCH',
|
||||
path: `/attachments/${encodeURIComponent(attachmentId)}/documents`,
|
||||
body: { documentIds },
|
||||
});
|
||||
}
|
||||
}
|
||||
352
src/clients/documents.ts
Normal file
352
src/clients/documents.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
import type { HttpClient } from '../http.js';
|
||||
import type {
|
||||
Document,
|
||||
DocumentUploadOptions,
|
||||
DocumentUploadRequestBody,
|
||||
DocumentTransformation,
|
||||
CoverPageOptions,
|
||||
DocumentAttribute,
|
||||
ListResponse,
|
||||
PaginationOptions,
|
||||
} from '../types.js';
|
||||
|
||||
/**
|
||||
* Client for document-related API operations.
|
||||
* Handles document upload, retrieval, modification, and preview.
|
||||
*/
|
||||
export class DocumentsClient {
|
||||
constructor(private readonly http: HttpClient) {}
|
||||
|
||||
/**
|
||||
* Upload a new document (letter or serial letter).
|
||||
* POST /documents
|
||||
*
|
||||
* @param options - Document upload options including base64-encoded PDF content
|
||||
* @returns The created document with validation results
|
||||
*/
|
||||
async upload(options: DocumentUploadOptions): Promise<Document> {
|
||||
// Transform user-friendly options to API request body format
|
||||
const requestBody: DocumentUploadRequestBody = {
|
||||
content: {
|
||||
filename: options.filename,
|
||||
content: options.content,
|
||||
},
|
||||
};
|
||||
|
||||
// Add options if any are specified
|
||||
if (options.simplex !== undefined || options.color !== undefined ||
|
||||
options.envelope !== undefined || options.franking !== undefined ||
|
||||
options.productionCountry !== undefined) {
|
||||
requestBody.options = {
|
||||
simplex: options.simplex,
|
||||
color: options.color,
|
||||
envelope: options.envelope,
|
||||
franking: options.franking,
|
||||
productionCountry: options.productionCountry,
|
||||
};
|
||||
}
|
||||
|
||||
// Add attributes if specified
|
||||
if (options.attributes) {
|
||||
requestBody.attributes = options.attributes;
|
||||
}
|
||||
|
||||
// Add split params if specified
|
||||
if (options.splitToken !== undefined || options.pagesPerLetter !== undefined) {
|
||||
requestBody.splitParams = {
|
||||
splitToken: options.splitToken,
|
||||
splitAfterNumberOfPages: options.pagesPerLetter,
|
||||
};
|
||||
}
|
||||
|
||||
// Add response format if specified
|
||||
if (options.responseFormat !== undefined) {
|
||||
requestBody.responseFormat = options.responseFormat;
|
||||
}
|
||||
|
||||
return this.http.request<Document>({
|
||||
method: 'POST',
|
||||
path: '/documents',
|
||||
body: requestBody,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List all shippable documents (status 2).
|
||||
* GET /documents
|
||||
*
|
||||
* @param pagination - Optional pagination parameters
|
||||
* @returns List of shippable documents
|
||||
*/
|
||||
async list(pagination?: PaginationOptions): Promise<ListResponse<Document>> {
|
||||
return this.http.request<ListResponse<Document>>({
|
||||
method: 'GET',
|
||||
path: '/documents',
|
||||
query: pagination,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List all documents with errors (status 7).
|
||||
* GET /documents/errors
|
||||
*
|
||||
* @param pagination - Optional pagination parameters
|
||||
* @returns List of erroneous documents
|
||||
*/
|
||||
async listErrors(pagination?: PaginationOptions): Promise<ListResponse<Document>> {
|
||||
return this.http.request<ListResponse<Document>>({
|
||||
method: 'GET',
|
||||
path: '/documents/errors',
|
||||
query: pagination,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific document by ID.
|
||||
* GET /documents/{documentID}
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @returns The document details
|
||||
*/
|
||||
async get(documentId: string): Promise<Document> {
|
||||
return this.http.request<Document>({
|
||||
method: 'GET',
|
||||
path: `/documents/${encodeURIComponent(documentId)}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a document.
|
||||
* DELETE /documents/{documentID}
|
||||
*
|
||||
* @param documentId - The document ID to delete
|
||||
*/
|
||||
async delete(documentId: string): Promise<void> {
|
||||
await this.http.request<void>({
|
||||
method: 'DELETE',
|
||||
path: `/documents/${encodeURIComponent(documentId)}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all attributes for a document.
|
||||
* GET /documents/{documentID}/attributes
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @returns Key-value attributes
|
||||
*/
|
||||
async getAttributes(documentId: string): Promise<Record<string, string>> {
|
||||
return this.http.request<Record<string, string>>({
|
||||
method: 'GET',
|
||||
path: `/documents/${encodeURIComponent(documentId)}/attributes`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set attributes for a document.
|
||||
* POST /documents/{documentID}/attributes
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @param attributes - Array of key-value attributes to set
|
||||
*/
|
||||
async setAttributes(documentId: string, attributes: DocumentAttribute[]): Promise<void> {
|
||||
await this.http.request<void>({
|
||||
method: 'POST',
|
||||
path: `/documents/${encodeURIComponent(documentId)}/attributes`,
|
||||
body: attributes,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific attribute value.
|
||||
* GET /documents/{documentID}/attributes/{key}
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @param key - The attribute key
|
||||
* @returns The attribute value
|
||||
*/
|
||||
async getAttribute(documentId: string, key: string): Promise<string> {
|
||||
return this.http.request<string>({
|
||||
method: 'GET',
|
||||
path: `/documents/${encodeURIComponent(documentId)}/attributes/${encodeURIComponent(key)}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a specific attribute value.
|
||||
* PUT /documents/{documentID}/attributes/{key}
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @param key - The attribute key
|
||||
* @param value - The new attribute value
|
||||
*/
|
||||
async updateAttribute(documentId: string, key: string, value: string): Promise<void> {
|
||||
await this.http.request<void>({
|
||||
method: 'PUT',
|
||||
path: `/documents/${encodeURIComponent(documentId)}/attributes/${encodeURIComponent(key)}`,
|
||||
body: { value },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific attribute.
|
||||
* DELETE /documents/{documentID}/attributes/{key}
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @param key - The attribute key to delete
|
||||
*/
|
||||
async deleteAttribute(documentId: string, key: string): Promise<void> {
|
||||
await this.http.request<void>({
|
||||
method: 'DELETE',
|
||||
path: `/documents/${encodeURIComponent(documentId)}/attributes/${encodeURIComponent(key)}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply transformations (scaling/offset) to a document.
|
||||
* PUT /documents/{documentID}/transformations
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @param transformation - Transformation parameters
|
||||
* @returns The updated document
|
||||
*/
|
||||
async applyTransformation(
|
||||
documentId: string,
|
||||
transformation: DocumentTransformation
|
||||
): Promise<Document> {
|
||||
return this.http.request<Document>({
|
||||
method: 'PUT',
|
||||
path: `/documents/${encodeURIComponent(documentId)}/transformations`,
|
||||
body: transformation,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert transformations to original document.
|
||||
* DELETE /documents/{documentID}/transformations
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @returns The updated document
|
||||
*/
|
||||
async revertTransformation(documentId: string): Promise<Document> {
|
||||
return this.http.request<Document>({
|
||||
method: 'DELETE',
|
||||
path: `/documents/${encodeURIComponent(documentId)}/transformations`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a cover page to a document.
|
||||
* PUT /documents/{documentID}/coverpage
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @param options - Cover page options with base64-encoded PDF
|
||||
* @returns The updated document
|
||||
*/
|
||||
async addCoverPage(documentId: string, options: CoverPageOptions): Promise<Document> {
|
||||
return this.http.request<Document>({
|
||||
method: 'PUT',
|
||||
path: `/documents/${encodeURIComponent(documentId)}/coverpage`,
|
||||
body: options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the cover page from a document.
|
||||
* DELETE /documents/{documentID}/coverpage
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @returns The updated document
|
||||
*/
|
||||
async removeCoverPage(documentId: string): Promise<Document> {
|
||||
return this.http.request<Document>({
|
||||
method: 'DELETE',
|
||||
path: `/documents/${encodeURIComponent(documentId)}/coverpage`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PDF preview of a document.
|
||||
* GET /documents/{documentID}/pdf
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @returns Response containing PDF data
|
||||
*/
|
||||
async getPdf(documentId: string): Promise<Response> {
|
||||
return this.http.requestRaw({
|
||||
method: 'GET',
|
||||
path: `/documents/${encodeURIComponent(documentId)}/pdf`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PNG preview of a document (first page).
|
||||
* GET /documents/{documentID}/png
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @returns Response containing PNG data
|
||||
*/
|
||||
async getPng(documentId: string): Promise<Response> {
|
||||
return this.http.requestRaw({
|
||||
method: 'GET',
|
||||
path: `/documents/${encodeURIComponent(documentId)}/png`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attachments for a document.
|
||||
* GET /documents/{documentID}/attachments
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @returns Array of attachment IDs
|
||||
*/
|
||||
async getAttachments(documentId: string): Promise<string[]> {
|
||||
return this.http.request<string[]>({
|
||||
method: 'GET',
|
||||
path: `/documents/${encodeURIComponent(documentId)}/attachments`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an attachment to a document.
|
||||
* POST /documents/{documentID}/attachments
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @param attachmentId - The attachment ID to add
|
||||
*/
|
||||
async addAttachment(documentId: string, attachmentId: string): Promise<void> {
|
||||
await this.http.request<void>({
|
||||
method: 'POST',
|
||||
path: `/documents/${encodeURIComponent(documentId)}/attachments`,
|
||||
body: { attachmentId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update attachments for a document (replace all).
|
||||
* PATCH /documents/{documentID}/attachments
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @param attachmentIds - Array of attachment IDs
|
||||
*/
|
||||
async updateAttachments(documentId: string, attachmentIds: string[]): Promise<void> {
|
||||
await this.http.request<void>({
|
||||
method: 'PATCH',
|
||||
path: `/documents/${encodeURIComponent(documentId)}/attachments`,
|
||||
body: { attachmentIds },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all attachments from a document.
|
||||
* DELETE /documents/{documentID}/attachments
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
*/
|
||||
async removeAttachments(documentId: string): Promise<void> {
|
||||
await this.http.request<void>({
|
||||
method: 'DELETE',
|
||||
path: `/documents/${encodeURIComponent(documentId)}/attachments`,
|
||||
});
|
||||
}
|
||||
}
|
||||
5
src/clients/index.ts
Normal file
5
src/clients/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { DocumentsClient } from './documents.js';
|
||||
export { AttachmentsClient } from './attachments.js';
|
||||
export { SendingsClient } from './sendings.js';
|
||||
export { AccountsClient } from './accounts.js';
|
||||
export { InvoicesClient } from './invoices.js';
|
||||
53
src/clients/invoices.ts
Normal file
53
src/clients/invoices.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { HttpClient } from '../http.js';
|
||||
import type { Invoice, InvoiceDetail, ListResponse, PaginationOptions } from '../types.js';
|
||||
|
||||
/**
|
||||
* Client for invoice-related API operations.
|
||||
* Handles invoice listing, details, and PDF download.
|
||||
*/
|
||||
export class InvoicesClient {
|
||||
constructor(private readonly http: HttpClient) {}
|
||||
|
||||
/**
|
||||
* List all invoices.
|
||||
* GET /invoices
|
||||
*
|
||||
* @param pagination - Optional pagination parameters
|
||||
* @returns List of invoices
|
||||
*/
|
||||
async list(pagination?: PaginationOptions): Promise<ListResponse<Invoice>> {
|
||||
return this.http.request<ListResponse<Invoice>>({
|
||||
method: 'GET',
|
||||
path: '/invoices',
|
||||
query: pagination,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoice details including transactions.
|
||||
* GET /invoices/{invoiceNumber}
|
||||
*
|
||||
* @param invoiceNumber - The invoice number
|
||||
* @returns Invoice details with transaction entries
|
||||
*/
|
||||
async get(invoiceNumber: string): Promise<InvoiceDetail> {
|
||||
return this.http.request<InvoiceDetail>({
|
||||
method: 'GET',
|
||||
path: `/invoices/${encodeURIComponent(invoiceNumber)}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download invoice as PDF.
|
||||
* GET /invoices/{invoiceNumber}/pdf
|
||||
*
|
||||
* @param invoiceNumber - The invoice number
|
||||
* @returns Response containing PDF data
|
||||
*/
|
||||
async getPdf(invoiceNumber: string): Promise<Response> {
|
||||
return this.http.requestRaw({
|
||||
method: 'GET',
|
||||
path: `/invoices/${encodeURIComponent(invoiceNumber)}/pdf`,
|
||||
});
|
||||
}
|
||||
}
|
||||
153
src/clients/sendings.ts
Normal file
153
src/clients/sendings.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import type { HttpClient } from '../http.js';
|
||||
import type {
|
||||
Sending,
|
||||
Document,
|
||||
DocumentUploadAndSendOptions,
|
||||
ListResponse,
|
||||
PaginationOptions,
|
||||
BatchStatusResponse,
|
||||
DocumentStatus,
|
||||
} from '../types.js';
|
||||
|
||||
/**
|
||||
* Client for sending/shipment-related API operations.
|
||||
* Handles document dispatch, cancellation, and status tracking.
|
||||
*/
|
||||
export class SendingsClient {
|
||||
constructor(private readonly http: HttpClient) {}
|
||||
|
||||
/**
|
||||
* Announce multiple documents for delivery.
|
||||
* POST /sendings
|
||||
*
|
||||
* @param documentIds - Array of document IDs to announce (as numbers or numeric strings)
|
||||
* @returns Array of sending confirmations
|
||||
*/
|
||||
async announce(documentIds: (string | number)[]): Promise<Sending[]> {
|
||||
// API expects a raw array of integers
|
||||
const ids = documentIds.map((id) => (typeof id === 'string' ? parseInt(id, 10) : id));
|
||||
return this.http.request<Sending[]>({
|
||||
method: 'POST',
|
||||
path: '/sendings',
|
||||
body: ids,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List all sendings/shipments (statuses 3-7).
|
||||
* GET /sendings
|
||||
*
|
||||
* @param pagination - Optional pagination parameters
|
||||
* @returns List of sendings
|
||||
*/
|
||||
async list(pagination?: PaginationOptions): Promise<ListResponse<Sending>> {
|
||||
return this.http.request<ListResponse<Sending>>({
|
||||
method: 'GET',
|
||||
path: '/sendings',
|
||||
query: pagination,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel multiple announced sendings.
|
||||
* PUT /sendings
|
||||
*
|
||||
* @param documentIds - Array of document IDs to cancel (as numbers or numeric strings)
|
||||
* @returns Array of updated sendings
|
||||
*/
|
||||
async cancelMultiple(documentIds: (string | number)[]): Promise<Sending[]> {
|
||||
// API expects a raw array of integers
|
||||
const ids = documentIds.map((id) => (typeof id === 'string' ? parseInt(id, 10) : id));
|
||||
return this.http.request<Sending[]>({
|
||||
method: 'PUT',
|
||||
path: '/sendings',
|
||||
body: ids,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger sending for a single document.
|
||||
* POST /sendings/{documentID}
|
||||
*
|
||||
* @param documentId - The document ID to send
|
||||
* @returns The sending confirmation
|
||||
*/
|
||||
async send(documentId: string): Promise<Sending> {
|
||||
return this.http.request<Sending>({
|
||||
method: 'POST',
|
||||
path: `/sendings/${encodeURIComponent(documentId)}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sending status for a specific document.
|
||||
* GET /sendings/{documentID}
|
||||
*
|
||||
* @param documentId - The document ID
|
||||
* @returns The sending details
|
||||
*/
|
||||
async get(documentId: string): Promise<Sending> {
|
||||
return this.http.request<Sending>({
|
||||
method: 'GET',
|
||||
path: `/sendings/${encodeURIComponent(documentId)}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a specific sending.
|
||||
* PUT /sendings/{documentID}
|
||||
*
|
||||
* @param documentId - The document ID to cancel
|
||||
* @returns The updated sending
|
||||
*/
|
||||
async cancel(documentId: string): Promise<Sending> {
|
||||
return this.http.request<Sending>({
|
||||
method: 'PUT',
|
||||
path: `/sendings/${encodeURIComponent(documentId)}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a sending record.
|
||||
* DELETE /sendings/{documentID}
|
||||
*
|
||||
* @param documentId - The document ID to delete
|
||||
*/
|
||||
async delete(documentId: string): Promise<void> {
|
||||
await this.http.request<void>({
|
||||
method: 'DELETE',
|
||||
path: `/sendings/${encodeURIComponent(documentId)}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a document and immediately send it.
|
||||
* POST /sendings/document
|
||||
*
|
||||
* @param options - Document upload options
|
||||
* @returns The created and sent document
|
||||
*/
|
||||
async uploadAndSend(options: DocumentUploadAndSendOptions): Promise<Document> {
|
||||
return this.http.request<Document>({
|
||||
method: 'POST',
|
||||
path: '/sendings/document',
|
||||
body: options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get batch status for multiple documents.
|
||||
* GET /sendings/status
|
||||
*
|
||||
* @param documentIds - Array of document IDs to check
|
||||
* @returns Map of document IDs to their statuses
|
||||
*/
|
||||
async getStatus(documentIds: string[]): Promise<Record<string, DocumentStatus>> {
|
||||
const response = await this.http.request<BatchStatusResponse>({
|
||||
method: 'GET',
|
||||
path: '/sendings/status',
|
||||
query: { documentIds: documentIds.join(',') },
|
||||
});
|
||||
return response.statuses;
|
||||
}
|
||||
}
|
||||
71
src/errors.ts
Normal file
71
src/errors.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { ApiErrorResponse } from './types.js';
|
||||
|
||||
/**
|
||||
* Error thrown when the Binect API returns a non-success response.
|
||||
* Preserves HTTP status, endpoint, and parsed response body.
|
||||
*/
|
||||
export class BinectApiError extends Error {
|
||||
/** HTTP status code */
|
||||
public readonly status: number;
|
||||
/** API endpoint that was called */
|
||||
public readonly endpoint: string;
|
||||
/** HTTP method used */
|
||||
public readonly method: string;
|
||||
/** Parsed error response from API (when available) */
|
||||
public readonly response: ApiErrorResponse | null;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
status: number,
|
||||
endpoint: string,
|
||||
method: string,
|
||||
response: ApiErrorResponse | null = null
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'BinectApiError';
|
||||
this.status = status;
|
||||
this.endpoint = endpoint;
|
||||
this.method = method;
|
||||
this.response = response;
|
||||
|
||||
// Maintains proper stack trace for where error was thrown (V8 engines)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, BinectApiError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a detailed string representation of the error
|
||||
*/
|
||||
toDetailedString(): string {
|
||||
const parts = [
|
||||
`BinectApiError: ${this.message}`,
|
||||
` Status: ${this.status}`,
|
||||
` Endpoint: ${this.method} ${this.endpoint}`,
|
||||
];
|
||||
|
||||
if (this.response) {
|
||||
if (this.response.error) {
|
||||
parts.push(` Error: ${this.response.error}`);
|
||||
}
|
||||
if (this.response.message) {
|
||||
parts.push(` Message: ${this.response.message}`);
|
||||
}
|
||||
if (this.response.details && this.response.details.length > 0) {
|
||||
parts.push(` Details: ${this.response.details.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when credentials are invalid or missing
|
||||
*/
|
||||
export class BinectAuthError extends BinectApiError {
|
||||
constructor(endpoint: string, method: string, response: ApiErrorResponse | null = null) {
|
||||
super('Authentication failed: Invalid credentials', 401, endpoint, method, response);
|
||||
this.name = 'BinectAuthError';
|
||||
}
|
||||
}
|
||||
274
src/helpers.ts
Normal file
274
src/helpers.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* 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<string> {
|
||||
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<T>(
|
||||
fn: () => Promise<T>,
|
||||
condition: (result: T) => boolean,
|
||||
options: PollOptions = {}
|
||||
): Promise<T> {
|
||||
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<Document>,
|
||||
options: PollOptions = {}
|
||||
): Promise<Document> {
|
||||
return pollUntil(
|
||||
getDocument,
|
||||
(doc) => isShippable(doc) || isErroneous(doc),
|
||||
options
|
||||
);
|
||||
}
|
||||
183
src/http.ts
Normal file
183
src/http.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { BinectApiError, BinectAuthError } from './errors.js';
|
||||
import type { ApiErrorResponse } from './types.js';
|
||||
|
||||
/**
|
||||
* Default base URL for the Binect API
|
||||
*/
|
||||
export const DEFAULT_BASE_URL = 'https://app.binect.de/binectapi/v1';
|
||||
|
||||
/**
|
||||
* Encodes credentials for HTTP Basic Authentication.
|
||||
* Works in both browser and Node.js environments.
|
||||
*/
|
||||
export function encodeBasicAuth(username: string, password: string): string {
|
||||
const credentials = `${username}:${password}`;
|
||||
|
||||
// Use Buffer in Node.js, btoa in browser
|
||||
if (typeof Buffer !== 'undefined') {
|
||||
return Buffer.from(credentials, 'utf-8').toString('base64');
|
||||
}
|
||||
|
||||
// Browser environment
|
||||
return btoa(credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP client configuration
|
||||
*/
|
||||
export interface HttpClientConfig {
|
||||
baseUrl: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request options
|
||||
*/
|
||||
export interface RequestOptions {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
path: string;
|
||||
body?: unknown;
|
||||
query?: Record<string, string | number | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Low-level HTTP client for Binect API requests.
|
||||
* Handles authentication, request formatting, and error parsing.
|
||||
*/
|
||||
export class HttpClient {
|
||||
private readonly baseUrl: string;
|
||||
private readonly authHeader: string;
|
||||
|
||||
constructor(config: HttpClientConfig) {
|
||||
this.baseUrl = config.baseUrl;
|
||||
this.authHeader = `Basic ${encodeBasicAuth(config.username, config.password)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an HTTP request to the Binect API
|
||||
*/
|
||||
async request<T>(options: RequestOptions): Promise<T> {
|
||||
const url = this.buildUrl(options.path, options.query);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: this.authHeader,
|
||||
Accept: 'application/json',
|
||||
};
|
||||
|
||||
const init: RequestInit = {
|
||||
method: options.method,
|
||||
headers,
|
||||
};
|
||||
|
||||
if (options.body !== undefined) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
init.body = JSON.stringify(options.body);
|
||||
}
|
||||
|
||||
const response = await fetch(url, init);
|
||||
|
||||
if (!response.ok) {
|
||||
await this.handleErrorResponse(response, options.path, options.method);
|
||||
}
|
||||
|
||||
// Handle empty responses (204 No Content, etc.)
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (!text) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
// Some endpoints may return non-JSON even with JSON content-type
|
||||
// Check if response looks like JSON before parsing
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
return JSON.parse(text) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request and returns raw response (for binary data like PDFs)
|
||||
*/
|
||||
async requestRaw(options: RequestOptions): Promise<Response> {
|
||||
const url = this.buildUrl(options.path, options.query);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: this.authHeader,
|
||||
};
|
||||
|
||||
const init: RequestInit = {
|
||||
method: options.method,
|
||||
headers,
|
||||
};
|
||||
|
||||
const response = await fetch(url, init);
|
||||
|
||||
if (!response.ok) {
|
||||
await this.handleErrorResponse(response, options.path, options.method);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the full URL with query parameters
|
||||
*/
|
||||
private buildUrl(path: string, query?: Record<string, string | number | undefined>): string {
|
||||
// Ensure proper URL construction by concatenating base and path
|
||||
// Remove trailing slash from base and leading slash from path to avoid double slashes
|
||||
const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
||||
const cleanPath = path.startsWith('/') ? path : `/${path}`;
|
||||
const url = new URL(`${base}${cleanPath}`);
|
||||
|
||||
if (query) {
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value !== undefined) {
|
||||
url.searchParams.set(key, String(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles error responses from the API
|
||||
*/
|
||||
private async handleErrorResponse(
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
method: string
|
||||
): Promise<never> {
|
||||
let errorResponse: ApiErrorResponse | null = null;
|
||||
let rawText = '';
|
||||
|
||||
try {
|
||||
rawText = await response.text();
|
||||
if (rawText) {
|
||||
// Try to parse as JSON
|
||||
const trimmed = rawText.trim();
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
errorResponse = JSON.parse(rawText) as ApiErrorResponse;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// JSON parsing failed - keep the raw text for the error message
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new BinectAuthError(endpoint, method, errorResponse);
|
||||
}
|
||||
|
||||
// Use structured error message if available, otherwise use raw text or generic message
|
||||
const message = errorResponse?.message ?? errorResponse?.error ?? (rawText || `HTTP ${response.status} error`);
|
||||
|
||||
throw new BinectApiError(message, response.status, endpoint, method, errorResponse);
|
||||
}
|
||||
}
|
||||
95
src/index.ts
Normal file
95
src/index.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Binect-JS SDK
|
||||
*
|
||||
* A JavaScript/TypeScript wrapper for the Binect API
|
||||
* to send PDF documents as physical mail.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
// Main client
|
||||
export { BinectClient } from './client.js';
|
||||
|
||||
// Sub-clients (for advanced usage)
|
||||
export {
|
||||
DocumentsClient,
|
||||
AttachmentsClient,
|
||||
SendingsClient,
|
||||
AccountsClient,
|
||||
InvoicesClient,
|
||||
} from './clients/index.js';
|
||||
|
||||
// Types
|
||||
export {
|
||||
// Enums
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
FrankingType,
|
||||
ProductionCountry,
|
||||
ResponseFormat,
|
||||
// Request types
|
||||
type DocumentUploadOptions,
|
||||
type DocumentUploadAndSendOptions,
|
||||
type DocumentTransformation,
|
||||
type CoverPageOptions,
|
||||
type DocumentAttribute,
|
||||
type PaginationOptions,
|
||||
type SendingAnnounceOptions,
|
||||
type SendingCancelOptions,
|
||||
type PersonalDataUpdate,
|
||||
type AccountPrintOptions,
|
||||
type AttachmentUploadOptions,
|
||||
// Response types
|
||||
type ValidationMessage,
|
||||
type ExtractedAddress,
|
||||
type PriceInfo,
|
||||
type DocumentStatusInfo,
|
||||
type LetterData,
|
||||
type Letter,
|
||||
type Document,
|
||||
type ListResponse,
|
||||
type Attachment,
|
||||
type Sending,
|
||||
type AccountInfo,
|
||||
type PersonalData,
|
||||
type Coworker,
|
||||
type JournalEntry,
|
||||
type Invoice,
|
||||
type InvoiceDetail,
|
||||
type BatchStatusResponse,
|
||||
type ApiErrorResponse,
|
||||
// Config
|
||||
type BinectClientConfig,
|
||||
} from './types.js';
|
||||
|
||||
// Errors
|
||||
export { BinectApiError, BinectAuthError } from './errors.js';
|
||||
|
||||
// Convenience helpers
|
||||
export {
|
||||
// Status predicates
|
||||
isShippable,
|
||||
isErroneous,
|
||||
isInPreparation,
|
||||
isInProductionQueue,
|
||||
isPrinting,
|
||||
isSent,
|
||||
isCanceled,
|
||||
isTerminal,
|
||||
isCancelable,
|
||||
// Validation helpers
|
||||
getErrors,
|
||||
getWarnings,
|
||||
getInfoMessages,
|
||||
hasErrors,
|
||||
hasWarnings,
|
||||
// Status description
|
||||
getStatusDescription,
|
||||
// Base64 utilities
|
||||
fileToBase64,
|
||||
bufferToBase64,
|
||||
// Polling utilities
|
||||
pollUntil,
|
||||
waitForShippable,
|
||||
type PollOptions,
|
||||
} from './helpers.js';
|
||||
446
src/types.ts
Normal file
446
src/types.ts
Normal file
@@ -0,0 +1,446 @@
|
||||
/**
|
||||
* Binect API Type Definitions
|
||||
* Based on API specification v0.9.9
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Enums
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Document status codes as defined by the Binect API
|
||||
*/
|
||||
export enum DocumentStatus {
|
||||
/** Document is being prepared/validated */
|
||||
IN_PREPARATION = 1,
|
||||
/** Document is ready to be shipped */
|
||||
SHIPPABLE = 2,
|
||||
/** Document is in production queue */
|
||||
PRODUCTION_QUEUE = 3,
|
||||
/** Document is being printed */
|
||||
PRINTING = 4,
|
||||
/** Document has been sent */
|
||||
SENT = 5,
|
||||
/** Document was canceled */
|
||||
CANCELED = 6,
|
||||
/** Document has errors */
|
||||
ERRONEOUS = 7,
|
||||
}
|
||||
|
||||
/**
|
||||
* Envelope type options
|
||||
*/
|
||||
export enum EnvelopeType {
|
||||
DINLANG = 'DINLANG',
|
||||
C4 = 'C4',
|
||||
}
|
||||
|
||||
/**
|
||||
* Franking type options
|
||||
*/
|
||||
export enum FrankingType {
|
||||
UNSPECIFIED = 'UNSPECIFIED',
|
||||
STANDARD_FRANKING = 'STANDARD_FRANKING',
|
||||
DV_FRANKING = 'DV_FRANKING',
|
||||
}
|
||||
|
||||
/**
|
||||
* Production country options
|
||||
*/
|
||||
export enum ProductionCountry {
|
||||
UNSPECIFIED = 'UNSPECIFIED',
|
||||
DE = 'DE',
|
||||
AT = 'AT',
|
||||
}
|
||||
|
||||
/**
|
||||
* Response format options for document upload
|
||||
*/
|
||||
export enum ResponseFormat {
|
||||
/** Complete validation results (default) */
|
||||
FULL = 'FULL',
|
||||
/** Minimal response; validation runs asynchronously */
|
||||
SHORT = 'SHORT',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Request Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Options for uploading a document
|
||||
*/
|
||||
export interface DocumentUploadOptions {
|
||||
/** Base64-encoded PDF content */
|
||||
content: string;
|
||||
/** Filename for the document (optional) */
|
||||
filename?: string;
|
||||
/** Whether to print in color (default: false) */
|
||||
color?: boolean;
|
||||
/** Whether to print simplex/single-sided (default: false, meaning duplex) */
|
||||
simplex?: boolean;
|
||||
/** Envelope type */
|
||||
envelope?: EnvelopeType;
|
||||
/** Franking type */
|
||||
franking?: FrankingType;
|
||||
/** Production country */
|
||||
productionCountry?: ProductionCountry;
|
||||
/** Response format */
|
||||
responseFormat?: ResponseFormat;
|
||||
/** Number of pages per letter for serial letter splitting */
|
||||
pagesPerLetter?: number;
|
||||
/** Token for serial letter splitting */
|
||||
splitToken?: string;
|
||||
/** Custom attributes as key-value pairs */
|
||||
attributes?: DocumentAttribute[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal API request body for document upload
|
||||
* @internal
|
||||
*/
|
||||
export interface DocumentUploadRequestBody {
|
||||
content: {
|
||||
filename?: string;
|
||||
content: string;
|
||||
};
|
||||
options?: {
|
||||
simplex?: boolean;
|
||||
color?: boolean;
|
||||
envelope?: EnvelopeType;
|
||||
franking?: FrankingType;
|
||||
productionCountry?: ProductionCountry;
|
||||
};
|
||||
attributes?: DocumentAttribute[];
|
||||
splitParams?: {
|
||||
splitToken?: string;
|
||||
splitAfterNumberOfPages?: number;
|
||||
};
|
||||
responseFormat?: ResponseFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for uploading and immediately sending a document
|
||||
*/
|
||||
export interface DocumentUploadAndSendOptions extends DocumentUploadOptions {
|
||||
/** Send immediately after upload */
|
||||
send?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformation parameters for document scaling/offset
|
||||
*/
|
||||
export interface DocumentTransformation {
|
||||
/** Horizontal offset in mm */
|
||||
offsetX?: number;
|
||||
/** Vertical offset in mm */
|
||||
offsetY?: number;
|
||||
/** Scale factor (1.0 = 100%) */
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cover page parameters
|
||||
*/
|
||||
export interface CoverPageOptions {
|
||||
/** Base64-encoded PDF content for cover page */
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Key-value attribute
|
||||
*/
|
||||
export interface DocumentAttribute {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination options for list endpoints
|
||||
*/
|
||||
export interface PaginationOptions {
|
||||
/** Maximum number of results to return */
|
||||
limit?: number;
|
||||
/** Number of results to skip */
|
||||
offset?: number;
|
||||
/** Index signature for compatibility with query parameters */
|
||||
[key: string]: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for announcing documents for sending
|
||||
*/
|
||||
export interface SendingAnnounceOptions {
|
||||
/** Document IDs to announce */
|
||||
documentIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for canceling sendings
|
||||
*/
|
||||
export interface SendingCancelOptions {
|
||||
/** Document IDs to cancel */
|
||||
documentIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Personal data update options
|
||||
*/
|
||||
export interface PersonalDataUpdate {
|
||||
company?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
street?: string;
|
||||
houseNumber?: string;
|
||||
zipCode?: string;
|
||||
city?: string;
|
||||
country?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Account print options
|
||||
*/
|
||||
export interface AccountPrintOptions {
|
||||
color?: boolean;
|
||||
duplex?: boolean;
|
||||
envelope?: EnvelopeType;
|
||||
franking?: FrankingType;
|
||||
productionCountry?: ProductionCountry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attachment upload options
|
||||
*/
|
||||
export interface AttachmentUploadOptions {
|
||||
/** Base64-encoded PDF content */
|
||||
content: string;
|
||||
/** Name for the attachment */
|
||||
name?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Response Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Validation message from document processing
|
||||
*/
|
||||
export interface ValidationMessage {
|
||||
type: 'INFO' | 'WARNING' | 'ERROR';
|
||||
code: string;
|
||||
message: string;
|
||||
page?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address extracted from document
|
||||
*/
|
||||
export interface ExtractedAddress {
|
||||
name?: string;
|
||||
company?: string;
|
||||
street?: string;
|
||||
houseNumber?: string;
|
||||
zipCode?: string;
|
||||
city?: string;
|
||||
country?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Price information from API
|
||||
*/
|
||||
export interface PriceInfo {
|
||||
priceBeforeTax: number;
|
||||
priceAfterTax: number;
|
||||
unit: 'EUROCENT' | string;
|
||||
taxInPercent: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Document status from API
|
||||
*/
|
||||
export interface DocumentStatusInfo {
|
||||
code: DocumentStatus;
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Letter data from API response
|
||||
*/
|
||||
export interface LetterData {
|
||||
recipientAddress?: string;
|
||||
price?: PriceInfo;
|
||||
international?: boolean;
|
||||
options?: {
|
||||
simplex?: boolean;
|
||||
color?: boolean;
|
||||
franking?: FrankingType;
|
||||
productionCountry?: ProductionCountry;
|
||||
envelope?: EnvelopeType;
|
||||
};
|
||||
attributes?: DocumentAttribute[];
|
||||
attachments?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Letter from API response
|
||||
*/
|
||||
export interface Letter {
|
||||
letterType?: string;
|
||||
letterData?: LetterData;
|
||||
errors?: ValidationMessage[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Document response from API
|
||||
*/
|
||||
export interface Document {
|
||||
/** Document ID (numeric) */
|
||||
id: number;
|
||||
/** Filename of uploaded document */
|
||||
filename?: string;
|
||||
/** Number of pages in document */
|
||||
numberOfPages: number;
|
||||
/** Document status */
|
||||
status: DocumentStatusInfo;
|
||||
/** Type of document */
|
||||
documentType?: 'LETTER' | 'SERIALLETTER' | string;
|
||||
/** Letter details (for single letters) */
|
||||
letter?: Letter;
|
||||
/** Array of letters (for serial letters) */
|
||||
letters?: Letter[];
|
||||
}
|
||||
|
||||
/**
|
||||
* List response wrapper
|
||||
*/
|
||||
export interface ListResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attachment response from API
|
||||
*/
|
||||
export interface Attachment {
|
||||
attachmentId: string;
|
||||
name: string;
|
||||
pageCount: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sending/shipment response from API
|
||||
*/
|
||||
export interface Sending {
|
||||
documentId: string;
|
||||
status: DocumentStatus;
|
||||
price?: number;
|
||||
trackingId?: string;
|
||||
shippedAt?: string;
|
||||
deliveredAt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Account balance/credit information
|
||||
*/
|
||||
export interface AccountInfo {
|
||||
credit: number;
|
||||
currency: string;
|
||||
debitornumber: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Personal data response
|
||||
*/
|
||||
export interface PersonalData {
|
||||
company?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
street?: string;
|
||||
houseNumber?: string;
|
||||
zipCode?: string;
|
||||
city?: string;
|
||||
country?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
debitornumber: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coworker information
|
||||
*/
|
||||
export interface Coworker {
|
||||
debitornumber: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Journal/transaction entry
|
||||
*/
|
||||
export interface JournalEntry {
|
||||
date: string;
|
||||
description: string;
|
||||
amount: number;
|
||||
balance: number;
|
||||
documentId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice summary
|
||||
*/
|
||||
export interface Invoice {
|
||||
invoiceNumber: string;
|
||||
date: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice detail with transactions
|
||||
*/
|
||||
export interface InvoiceDetail extends Invoice {
|
||||
entries: JournalEntry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch status check response
|
||||
*/
|
||||
export interface BatchStatusResponse {
|
||||
statuses: Record<string, DocumentStatus>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Error Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* API error response structure
|
||||
*/
|
||||
export interface ApiErrorResponse {
|
||||
error?: string;
|
||||
message?: string;
|
||||
details?: string[];
|
||||
validationErrors?: ValidationMessage[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Client Configuration
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Configuration options for BinectClient
|
||||
*/
|
||||
export interface BinectClientConfig {
|
||||
/** Binect username (email) */
|
||||
username: string;
|
||||
/** Binect password */
|
||||
password: string;
|
||||
/** Base URL override (default: https://app.binect.de/binectapi/v1) */
|
||||
baseUrl?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user