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:
2026-01-14 23:10:34 +01:00
parent 20462b48b3
commit b9aebb42f1
34 changed files with 6499 additions and 20 deletions

446
src/types.ts Normal file
View 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;
}