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:
22
.eslintrc.json
Normal file
22
.eslintrc.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2022,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-function-return-type": "warn",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"no-console": "warn"
|
||||
},
|
||||
"ignorePatterns": ["dist", "node_modules", "*.js", "vitest.config.ts"]
|
||||
}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -35,3 +35,4 @@ coverage/
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
dummy-credentials.ini
|
||||
|
||||
47
CLAUDE.md
47
CLAUDE.md
@@ -2,38 +2,57 @@
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
npm install # Install dependencies
|
||||
npm run build # Build TypeScript to dist/
|
||||
npm test # Run tests with vitest
|
||||
npm run typecheck # Type check without emitting
|
||||
```
|
||||
|
||||
## Project Overview
|
||||
|
||||
Binect-JS is a JavaScript/TypeScript wrapper for the Binect REST API (https://app.binect.de/index.jsp?id=api) that enables sending PDF documents as physical mail. The project consists of two artifacts:
|
||||
|
||||
1. **Binect-JS SDK** (`@binect/js`) - A thin API wrapper
|
||||
2. **Binect Explorer** (`@binect/explorer`) - A browser-based interactive tool for learning and experimentation
|
||||
1. **Binect-JS SDK** (`@binect/js`) - A thin API wrapper in `src/`
|
||||
2. **Binect Explorer** - A browser-based interactive tool in `explorer/`
|
||||
|
||||
## Architecture Principles
|
||||
## Architecture
|
||||
|
||||
The SDK is organized around domain-aligned sub-clients that mirror the API vocabulary:
|
||||
- `documents` - PDF upload, status inspection, parameter modification
|
||||
- `attachments` - Attachment handling
|
||||
- `sendings` - Mail dispatch triggers, cancellation
|
||||
- `accounts` - Account management
|
||||
- `invoices` - Invoice access
|
||||
- `client.documents` - PDF upload, status inspection, parameter modification
|
||||
- `client.attachments` - Attachment handling
|
||||
- `client.sendings` - Mail dispatch triggers, cancellation
|
||||
- `client.accounts` - Account management
|
||||
- `client.invoices` - Invoice access
|
||||
|
||||
### SDK Layer Separation
|
||||
|
||||
**Core API Layer** (authoritative): 1:1 semantic mapping to REST endpoints. Methods like `documents.uploadPdf`, `documents.getStatus`, `sendings.announce`, `sendings.cancel`.
|
||||
**Core API Layer** (authoritative): 1:1 semantic mapping to REST endpoints. Methods like `documents.upload`, `documents.get`, `sendings.send`, `sendings.cancel`.
|
||||
|
||||
**Convenience Layer** (optional, non-authoritative): Additive helpers like status predicates (`isShippable`), error extraction, polling helpers. These must never be the only way to perform an action.
|
||||
**Convenience Layer** (optional, non-authoritative): Additive helpers in `src/helpers.ts` like status predicates (`isShippable`), error extraction, polling helpers. These must never be the only way to perform an action.
|
||||
|
||||
## Design Constraints
|
||||
|
||||
- **No backend dependency**: Must function entirely in browser/JS runtime
|
||||
- **No runtime dependencies**: Uses native `fetch` API only
|
||||
- **No semantic reinterpretation**: Wrapper must not alter business meaning or outcomes
|
||||
- **Transparency over abstraction**: Developers must reason about actual API calls
|
||||
- **No default retries**: Network behavior must be explicit and opt-in
|
||||
- **Authentication**: HTTP Basic Auth, credentials are ephemeral (not stored/cached)
|
||||
|
||||
## Key Files
|
||||
|
||||
- `src/client.ts` - Main BinectClient class
|
||||
- `src/clients/*.ts` - Domain sub-clients
|
||||
- `src/types.ts` - TypeScript type definitions
|
||||
- `src/errors.ts` - BinectApiError and BinectAuthError
|
||||
- `src/http.ts` - Low-level HTTP client
|
||||
- `src/helpers.ts` - Convenience helpers (predicates, polling)
|
||||
- `explorer/index.html` - Standalone Explorer UI
|
||||
|
||||
## API Integration Notes
|
||||
|
||||
- Uploads use base64-encoded PDFs
|
||||
- All non-success responses surface as structured errors preserving HTTP status, endpoint, and parsed response
|
||||
- The wrapper does not reinterpret business errors from the API
|
||||
- Uploads use base64-encoded PDFs (max 12 MB)
|
||||
- All non-success responses surface as structured `BinectApiError`
|
||||
- Document status codes: 1=Preparing, 2=Shippable, 3=Queue, 4=Printing, 5=Sent, 6=Canceled, 7=Error
|
||||
|
||||
207
README.md
207
README.md
@@ -1,11 +1,206 @@
|
||||
BinectJs
|
||||
# Binect-JS
|
||||
|
||||
*Javascript Binect API wrapper*
|
||||
A JavaScript/TypeScript wrapper for the [Binect API](https://app.binect.de/index.jsp?id=api) to send PDF documents as physical mail via Deutsche Post.
|
||||
|
||||
# Binect JS - Send Papermail from your Browser
|
||||
## Features
|
||||
|
||||
An easy to use javascript library to send pdf-documents as papermail using the app.binect.de API.
|
||||
- **SDK (`@binect/js`)**: Type-safe API wrapper with domain-aligned sub-clients
|
||||
- **Explorer**: Browser-based interactive tool for learning and testing the API
|
||||
- **Zero Dependencies**: Uses native `fetch` API, works in browsers and Node.js >= 18
|
||||
|
||||
For documentation of the API see: https://app.binect.de/index.jsp?id=api
|
||||
## Installation
|
||||
|
||||
xxx
|
||||
```bash
|
||||
npm install @binect/js
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { BinectClient, DocumentStatus, isShippable } from '@binect/js';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
// Create client with your Binect credentials
|
||||
const client = new BinectClient({
|
||||
username: 'your@email.com',
|
||||
password: 'your-password',
|
||||
});
|
||||
|
||||
// Upload a PDF document
|
||||
const pdfContent = readFileSync('letter.pdf').toString('base64');
|
||||
const document = await client.documents.upload({
|
||||
content: pdfContent,
|
||||
color: false,
|
||||
duplex: true,
|
||||
envelope: 'DINLANG',
|
||||
});
|
||||
|
||||
console.log('Document ID:', document.documentId);
|
||||
console.log('Status:', document.status);
|
||||
|
||||
// Check if ready to send
|
||||
if (isShippable(document)) {
|
||||
// Send the document as physical mail
|
||||
const sending = await client.sendings.send(document.documentId);
|
||||
console.log('Mail dispatched!');
|
||||
}
|
||||
```
|
||||
|
||||
## SDK API Reference
|
||||
|
||||
The SDK provides domain-aligned sub-clients:
|
||||
|
||||
### Documents (`client.documents`)
|
||||
|
||||
- `upload(options)` - Upload a PDF document
|
||||
- `list(pagination?)` - List shippable documents
|
||||
- `listErrors(pagination?)` - List documents with errors
|
||||
- `get(documentId)` - Get document details
|
||||
- `delete(documentId)` - Delete a document
|
||||
- `getPdf(documentId)` - Get PDF preview
|
||||
- `getPng(documentId)` - Get PNG preview
|
||||
- `getAttributes(documentId)` - Get document attributes
|
||||
- `setAttributes(documentId, attributes)` - Set attributes
|
||||
- `applyTransformation(documentId, transformation)` - Apply scaling/offset
|
||||
- `addCoverPage(documentId, options)` - Add cover page
|
||||
|
||||
### Sendings (`client.sendings`)
|
||||
|
||||
- `announce(documentIds)` - Announce documents for delivery
|
||||
- `list(pagination?)` - List all sendings
|
||||
- `send(documentId)` - Trigger single document send
|
||||
- `cancel(documentId)` - Cancel a sending
|
||||
- `getStatus(documentIds)` - Batch status check
|
||||
|
||||
### Attachments (`client.attachments`)
|
||||
|
||||
- `upload(options)` - Upload an attachment
|
||||
- `list(pagination?)` - List all attachments
|
||||
- `get(attachmentId)` - Get attachment details
|
||||
- `delete(attachmentId)` - Delete an attachment
|
||||
- `attachToDocuments(attachmentId, documentIds)` - Attach to documents
|
||||
|
||||
### Accounts (`client.accounts`)
|
||||
|
||||
- `get()` - Get account balance/credit info
|
||||
- `getPersonalData()` - Get personal data
|
||||
- `updatePersonalData(data)` - Update personal data
|
||||
- `getOptions()` - Get default print options
|
||||
- `updateOptions(options)` - Update print options
|
||||
- `getJournal(month)` - Get transaction journal
|
||||
|
||||
### Invoices (`client.invoices`)
|
||||
|
||||
- `list(pagination?)` - List all invoices
|
||||
- `get(invoiceNumber)` - Get invoice details
|
||||
- `getPdf(invoiceNumber)` - Download invoice PDF
|
||||
|
||||
## Convenience Helpers
|
||||
|
||||
```typescript
|
||||
import {
|
||||
isShippable,
|
||||
isErroneous,
|
||||
isSent,
|
||||
isTerminal,
|
||||
isCancelable,
|
||||
hasErrors,
|
||||
hasWarnings,
|
||||
getErrors,
|
||||
getStatusDescription,
|
||||
waitForShippable,
|
||||
fileToBase64,
|
||||
} from '@binect/js';
|
||||
|
||||
// Status predicates
|
||||
if (isShippable(document)) { /* ... */ }
|
||||
if (isErroneous(document)) { /* ... */ }
|
||||
|
||||
// Validation helpers
|
||||
const errors = getErrors(document);
|
||||
if (hasWarnings(document)) {
|
||||
console.log('Document has warnings');
|
||||
}
|
||||
|
||||
// Polling (opt-in)
|
||||
const readyDoc = await waitForShippable(
|
||||
() => client.documents.get(docId),
|
||||
{ intervalMs: 2000, maxAttempts: 30 }
|
||||
);
|
||||
|
||||
// Base64 encoding (browser)
|
||||
const file = document.querySelector('input[type="file"]').files[0];
|
||||
const base64 = await fileToBase64(file);
|
||||
```
|
||||
|
||||
## Explorer
|
||||
|
||||
The Binect Explorer is a browser-based interactive tool for:
|
||||
- Learning the Binect API
|
||||
- Testing document uploads and mail dispatch
|
||||
- Managing use case profiles
|
||||
|
||||
Open `explorer/index.html` in a browser to use it.
|
||||
|
||||
## Document Status Codes
|
||||
|
||||
| Code | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| 1 | IN_PREPARATION | Document is being validated |
|
||||
| 2 | SHIPPABLE | Ready to send |
|
||||
| 3 | PRODUCTION_QUEUE | In production queue |
|
||||
| 4 | PRINTING | Currently printing |
|
||||
| 5 | SENT | Mail has been sent |
|
||||
| 6 | CANCELED | Sending was canceled |
|
||||
| 7 | ERRONEOUS | Document has validation errors |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Run e2e tests (requires Binect credentials)
|
||||
BINECT_USERNAME=your@email.com BINECT_PASSWORD=yourpass npm run test:e2e
|
||||
|
||||
# Type check
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
binect-js/
|
||||
├── src/ # SDK source code
|
||||
│ ├── client.ts # Main BinectClient
|
||||
│ ├── clients/ # Domain sub-clients
|
||||
│ ├── types.ts # Type definitions
|
||||
│ ├── errors.ts # Error classes
|
||||
│ ├── http.ts # HTTP layer
|
||||
│ └── helpers.ts # Convenience helpers
|
||||
├── explorer/ # Browser-based Explorer UI
|
||||
├── tests/ # Test suite
|
||||
├── architecture/ # Architecture Decision Records
|
||||
└── research/ # API documentation research
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
See [architecture/](./architecture/) for Architecture Decision Records (ADRs):
|
||||
- ADR-001: SDK Architecture
|
||||
- ADR-002: No External Dependencies
|
||||
- ADR-003: Explorer Architecture
|
||||
|
||||
## API Documentation
|
||||
|
||||
Official Binect API documentation: https://app.binect.de/index.jsp?id=api
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
78
architecture/ADR-001-sdk-architecture.md
Normal file
78
architecture/ADR-001-sdk-architecture.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# ADR-001: SDK Architecture
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The Binect-JS SDK needs to provide a JavaScript/TypeScript wrapper for the Binect REST API. Per the PRD and TSD, the SDK must:
|
||||
- Be transparent and thin (no semantic reinterpretation)
|
||||
- Work in both browser and Node.js environments
|
||||
- Use domain-aligned sub-clients mirroring the API vocabulary
|
||||
- Separate core API layer from optional convenience helpers
|
||||
- Handle HTTP Basic Authentication without storing credentials
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Client Structure
|
||||
We adopt a **main client with domain-aligned sub-clients** pattern:
|
||||
|
||||
```typescript
|
||||
const client = new BinectClient({ username, password });
|
||||
client.documents.upload(...)
|
||||
client.sendings.announce(...)
|
||||
client.attachments.list(...)
|
||||
client.accounts.get(...)
|
||||
client.invoices.list(...)
|
||||
```
|
||||
|
||||
Each sub-client maps to an API domain and provides 1:1 method mapping to REST endpoints.
|
||||
|
||||
### 2. HTTP Layer
|
||||
We use the native `fetch` API for HTTP requests:
|
||||
- Works in both browser and Node.js (>=18)
|
||||
- No external dependencies
|
||||
- Predictable behavior without hidden retries or timeouts
|
||||
|
||||
### 3. Authentication
|
||||
- Credentials passed at client construction
|
||||
- Converted to Base64 Basic Auth header per request
|
||||
- Never stored beyond client instance lifetime
|
||||
- No automatic credential refresh
|
||||
|
||||
### 4. Error Handling
|
||||
- Non-2xx responses throw `BinectApiError` with:
|
||||
- HTTP status code
|
||||
- Endpoint path
|
||||
- Parsed response body (when available)
|
||||
- Network errors surface as-is (no wrapping)
|
||||
|
||||
### 5. Type Safety
|
||||
- Full TypeScript types for all API requests/responses
|
||||
- Enums for document status, envelope types, franking types
|
||||
- Generic response types preserving API structure
|
||||
|
||||
### 6. Convenience Layer (Optional)
|
||||
Additive helpers in separate modules:
|
||||
- Status predicates (`isShippable`, `isErroneous`)
|
||||
- Polling utilities (opt-in, no default behavior)
|
||||
- Response extractors
|
||||
|
||||
These never replace core methods.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Clear mental model mapping to API documentation
|
||||
- No hidden behavior or magic
|
||||
- Works in all JavaScript environments with fetch
|
||||
- Type-safe development experience
|
||||
|
||||
### Negative
|
||||
- Developers must understand API structure
|
||||
- No automatic retry on transient failures (by design)
|
||||
- More verbose than heavily abstracted SDKs
|
||||
|
||||
## References
|
||||
- PRD: Section 3.1 (Product Intent)
|
||||
- TSD: Section 3 (SDK Technical Orientation)
|
||||
- Binect API: https://app.binect.de/index.jsp?id=api
|
||||
49
architecture/ADR-002-no-external-dependencies.md
Normal file
49
architecture/ADR-002-no-external-dependencies.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# ADR-002: No External Runtime Dependencies
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The SDK needs to make HTTP requests and handle authentication. Common approaches include using libraries like axios, node-fetch, or got for HTTP, and various utilities for base64 encoding.
|
||||
|
||||
Per the TSD (Section 2, Design Guardrail #1): "No backend dependency - The product must function entirely in browser and JavaScript runtime environments."
|
||||
|
||||
## Decision
|
||||
|
||||
The SDK will have **zero runtime dependencies**:
|
||||
|
||||
1. **HTTP Requests**: Use native `fetch` API
|
||||
- Available in all modern browsers
|
||||
- Built into Node.js >= 18
|
||||
- No polyfills required for target environments
|
||||
|
||||
2. **Base64 Encoding**: Use native APIs
|
||||
- Browser: `btoa()` / `atob()`
|
||||
- Node.js: `Buffer.from().toString('base64')`
|
||||
- Provide isomorphic wrapper
|
||||
|
||||
3. **Type Checking**: TypeScript (dev dependency only)
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- No dependency vulnerabilities to manage
|
||||
- Smaller bundle size
|
||||
- Predictable behavior (no library-specific quirks)
|
||||
- Works identically in browser and Node.js
|
||||
- No version conflicts with consumer projects
|
||||
|
||||
### Negative
|
||||
- Must implement utility functions ourselves
|
||||
- Cannot leverage library conveniences (interceptors, etc.)
|
||||
- Requires Node.js >= 18 (has native fetch)
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **axios**: Popular but adds ~13KB and has had security vulnerabilities
|
||||
2. **node-fetch**: Would require different code paths for browser/Node
|
||||
3. **ky**: Modern but still an external dependency
|
||||
|
||||
## References
|
||||
- TSD: Section 2 (Design Guardrails)
|
||||
- Node.js fetch: https://nodejs.org/docs/latest-v18.x/api/globals.html#fetch
|
||||
72
architecture/ADR-003-explorer-architecture.md
Normal file
72
architecture/ADR-003-explorer-architecture.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# ADR-003: Explorer Architecture
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The Binect Explorer needs to be a browser-based interactive tool for:
|
||||
- Learning the Binect API
|
||||
- Experimentation and evaluation
|
||||
- Safe testing before production integration
|
||||
|
||||
Per the TSD (Section 4), the Explorer:
|
||||
- Must operate without server-side components
|
||||
- Must clearly distinguish between preview and send operations
|
||||
- Must require explicit confirmation for destructive actions
|
||||
- Is a learning tool, not an operations dashboard
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Technology Stack
|
||||
We use a **vanilla JavaScript/HTML/CSS** approach:
|
||||
- No framework dependencies (React, Vue, etc.)
|
||||
- Single HTML file with embedded CSS and JS
|
||||
- Can use the SDK directly via module import
|
||||
- Easy to host as a static file
|
||||
|
||||
Rationale: Per TSD Section 7, the product must remain independent of specific UI frameworks. A vanilla approach ensures maximum portability and simplicity.
|
||||
|
||||
### 2. Architecture Pattern
|
||||
**Component-based with vanilla JS**:
|
||||
- Modular JavaScript functions for each feature
|
||||
- Event-driven UI updates
|
||||
- State management via simple objects
|
||||
|
||||
### 3. Feature Organization
|
||||
The Explorer UI is organized around the API domains:
|
||||
- **Credentials Panel**: Input and manage API credentials
|
||||
- **Documents Panel**: Upload, view, manage documents
|
||||
- **Sendings Panel**: Announce and track mail dispatch
|
||||
- **Attachments Panel**: Manage attachments
|
||||
- **Account Panel**: View account info and options
|
||||
|
||||
### 4. Safety Features
|
||||
Per TSD requirements:
|
||||
- Credentials are ephemeral by default (cleared on page refresh)
|
||||
- Optional local storage for convenience (opt-in)
|
||||
- Send operations require explicit confirmation dialog
|
||||
- Preview available before sending
|
||||
- Clear visual distinction between safe (read) and destructive (send/delete) actions
|
||||
|
||||
### 5. Use Case Profiles
|
||||
- Stored in browser localStorage
|
||||
- Export/import as JSON files
|
||||
- Contain only parameter configurations, not workflows
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Zero external dependencies
|
||||
- Works as single HTML file
|
||||
- Easy to understand and modify
|
||||
- Can be hosted anywhere (CDN, local file, etc.)
|
||||
- Aligns with TSD requirement for framework independence
|
||||
|
||||
### Negative
|
||||
- Less sophisticated UI compared to framework-based apps
|
||||
- Manual DOM manipulation
|
||||
- No virtual DOM or reactive updates
|
||||
|
||||
## References
|
||||
- TSD: Section 4 (Explorer Technical Orientation)
|
||||
- PRD: Section 4.1 (Functional Expectations)
|
||||
BIN
examples/din5008/260114-brief-testbriefBinectJs.odt
Executable file
BIN
examples/din5008/260114-brief-testbriefBinectJs.odt
Executable file
Binary file not shown.
BIN
examples/din5008/260114-brief-testbriefBinectJs.pdf
Executable file
BIN
examples/din5008/260114-brief-testbriefBinectJs.pdf
Executable file
Binary file not shown.
BIN
examples/din5008/260114-brief-testbriefKeinePlz.odt
Executable file
BIN
examples/din5008/260114-brief-testbriefKeinePlz.odt
Executable file
Binary file not shown.
BIN
examples/din5008/260114-brief-testbriefKeinePlz.pdf
Executable file
BIN
examples/din5008/260114-brief-testbriefKeinePlz.pdf
Executable file
Binary file not shown.
1335
explorer/index.html
Normal file
1335
explorer/index.html
Normal file
File diff suppressed because it is too large
Load Diff
1849
package-lock.json
generated
Normal file
1849
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
package.json
Normal file
46
package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@binect/js",
|
||||
"version": "0.1.0",
|
||||
"description": "JavaScript/TypeScript wrapper for the Binect API - Send PDF documents as physical mail",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "node --experimental-vm-modules node_modules/vitest/vitest.mjs run",
|
||||
"test:watch": "node --experimental-vm-modules node_modules/vitest/vitest.mjs",
|
||||
"test:e2e": "node --experimental-vm-modules node_modules/vitest/vitest.mjs run tests/e2e.test.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rm -rf dist",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"binect",
|
||||
"mail",
|
||||
"letter",
|
||||
"post",
|
||||
"pdf",
|
||||
"api",
|
||||
"deutsche-post"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vitest": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
115
research/binect-api-specification.md
Normal file
115
research/binect-api-specification.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Binect API REST Specification
|
||||
|
||||
**Source:** https://app.binect.de/binectapi/v1_swagger_api_kernel.json
|
||||
**Retrieved:** 2026-01-14
|
||||
|
||||
## Overview
|
||||
The Binect API (v0.9.9) is a letter shipping service that handles document upload, validation, processing, and Deutsche Post delivery. The API uses HTTP Basic Authentication and HTTPS exclusively.
|
||||
|
||||
## Base Configuration
|
||||
- **Base Path:** `/binectapi/v1`
|
||||
- **Host:** `app.binect.de`
|
||||
- **Scheme:** HTTPS only
|
||||
- **Authentication:** HTTP Basic Auth (required for all endpoints)
|
||||
- **Contact:** kontakt@binect.de
|
||||
|
||||
## Core Endpoints
|
||||
|
||||
### Documents Management
|
||||
**Upload & Retrieve:**
|
||||
- `POST /documents` – Upload letters/serial letters with base64-encoded content
|
||||
- `GET /documents` – List shippable documents (status 2)
|
||||
- `GET /documents/errors` – List erroneous documents (status 7)
|
||||
- `GET /documents/{documentID}` – Fetch specific document
|
||||
- `DELETE /documents/{documentID}` – Remove document
|
||||
|
||||
**Document Attributes:**
|
||||
- `GET /documents/{documentID}/attributes` – Get all attributes
|
||||
- `POST /documents/{documentID}/attributes` – Set attributes
|
||||
- `GET /documents/{documentID}/attributes/{key}` – Get specific attribute
|
||||
- `PUT /documents/{documentID}/attributes/{key}` – Update specific attribute
|
||||
- `DELETE /documents/{documentID}/attributes/{key}` – Delete specific attribute
|
||||
|
||||
**Document Modifications:**
|
||||
- `PUT /documents/{documentID}/transformations` – Apply scaling/offset adjustments
|
||||
- `DELETE /documents/{documentID}/transformations` – Revert to original
|
||||
- `PUT /documents/{documentID}/coverpage` – Add cover page
|
||||
- `DELETE /documents/{documentID}/coverpage` – Remove cover page
|
||||
- `GET /documents/{documentID}/pdf` – PDF preview
|
||||
- `GET /documents/{documentID}/png` – PNG preview
|
||||
|
||||
**Attachments on Documents:**
|
||||
- `GET /documents/{documentID}/attachments` – Get document attachments
|
||||
- `POST /documents/{documentID}/attachments` – Add attachment to document
|
||||
- `PATCH /documents/{documentID}/attachments` – Update attachments
|
||||
- `DELETE /documents/{documentID}/attachments` – Remove attachments
|
||||
|
||||
### Sendings (Shipments)
|
||||
- `POST /sendings` – Announce documents for delivery
|
||||
- `GET /sendings` – List all shipments (statuses 3-7)
|
||||
- `PUT /sendings` – Cancel unshipped letters
|
||||
- `POST /sendings/{documentID}` – Trigger single shipment
|
||||
- `GET /sendings/{documentID}` – Get specific sending
|
||||
- `PUT /sendings/{documentID}` – Cancel specific sending
|
||||
- `DELETE /sendings/{documentID}` – Delete sending
|
||||
- `POST /sendings/document` – Upload and immediately send
|
||||
- `GET /sendings/status` – Batch status check
|
||||
|
||||
### Attachments (Standalone)
|
||||
- `POST /attachments` – Upload attachment
|
||||
- `GET /attachments` – List all attachments
|
||||
- `GET /attachments/{attachmentID}` – Get specific attachment
|
||||
- `DELETE /attachments/{attachmentID}` – Delete attachment
|
||||
- `GET /attachments/{attachmentID}/pdf` – Preview attachment PDF
|
||||
- `GET /attachments/{attachmentID}/documents` – Find associated documents
|
||||
- `PATCH /attachments/{attachmentID}/documents` – Attach to multiple documents
|
||||
|
||||
### Account Management
|
||||
- `GET /accounts` – Retrieve credit/financial data
|
||||
- `GET /accounts/personaldata` – Get user details
|
||||
- `PATCH /accounts/personaldata` – Update user details
|
||||
- `GET /accounts/options` – Get default print options
|
||||
- `PUT /accounts/options` – Update default print options
|
||||
- `GET /accounts/coworkers` – List team members
|
||||
- `GET /accounts/coworkers/{debitornumber}/journal/{month}` – Coworker transactions
|
||||
- `GET /accounts/journal/{month}` – Account transactions
|
||||
|
||||
### Invoicing
|
||||
- `GET /invoices` – List all invoices
|
||||
- `GET /invoices/{invoiceNumber}` – Fetch invoice transactions
|
||||
- `GET /invoices/{invoiceNumber}/pdf` – Download invoice PDF
|
||||
|
||||
## Key Data Models
|
||||
|
||||
### Document Status Codes
|
||||
- 1: In preparation
|
||||
- 2: Shippable
|
||||
- 3: Production queue
|
||||
- 4: Printing
|
||||
- 5: Sent
|
||||
- 6: Canceled
|
||||
- 7: Erroneous
|
||||
|
||||
### Envelope Options
|
||||
- DINLANG
|
||||
- C4
|
||||
|
||||
### Franking Types
|
||||
- UNSPECIFIED
|
||||
- STANDARD_FRANKING
|
||||
- DV_FRANKING
|
||||
|
||||
### Production Countries
|
||||
- UNSPECIFIED
|
||||
- DE
|
||||
- AT
|
||||
|
||||
### Response Format Options
|
||||
- FULL (default): Complete validation results
|
||||
- SHORT: Minimal response; validation runs asynchronously
|
||||
|
||||
## Constraints
|
||||
- Maximum file size: 12 MB
|
||||
- Pagination supported via `limit` and `offset` parameters
|
||||
- Serial letters support token-based or page-count splitting
|
||||
- Transformations and cover pages can only be applied to original documents
|
||||
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;
|
||||
}
|
||||
59
tests/client.test.ts
Normal file
59
tests/client.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { BinectClient } from '../src/client.js';
|
||||
import { DocumentsClient } from '../src/clients/documents.js';
|
||||
import { AttachmentsClient } from '../src/clients/attachments.js';
|
||||
import { SendingsClient } from '../src/clients/sendings.js';
|
||||
import { AccountsClient } from '../src/clients/accounts.js';
|
||||
import { InvoicesClient } from '../src/clients/invoices.js';
|
||||
|
||||
describe('BinectClient', () => {
|
||||
it('creates client with required config', () => {
|
||||
const client = new BinectClient({
|
||||
username: 'test@example.com',
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
expect(client).toBeInstanceOf(BinectClient);
|
||||
});
|
||||
|
||||
it('creates client with custom baseUrl', () => {
|
||||
const client = new BinectClient({
|
||||
username: 'test@example.com',
|
||||
password: 'password123',
|
||||
baseUrl: 'https://custom.api.com/v1',
|
||||
});
|
||||
|
||||
expect(client).toBeInstanceOf(BinectClient);
|
||||
});
|
||||
|
||||
describe('sub-clients', () => {
|
||||
let client: BinectClient;
|
||||
|
||||
beforeAll(() => {
|
||||
client = new BinectClient({
|
||||
username: 'test@example.com',
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
it('has documents client', () => {
|
||||
expect(client.documents).toBeInstanceOf(DocumentsClient);
|
||||
});
|
||||
|
||||
it('has attachments client', () => {
|
||||
expect(client.attachments).toBeInstanceOf(AttachmentsClient);
|
||||
});
|
||||
|
||||
it('has sendings client', () => {
|
||||
expect(client.sendings).toBeInstanceOf(SendingsClient);
|
||||
});
|
||||
|
||||
it('has accounts client', () => {
|
||||
expect(client.accounts).toBeInstanceOf(AccountsClient);
|
||||
});
|
||||
|
||||
it('has invoices client', () => {
|
||||
expect(client.invoices).toBeInstanceOf(InvoicesClient);
|
||||
});
|
||||
});
|
||||
});
|
||||
89
tests/errors.test.ts
Normal file
89
tests/errors.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { BinectApiError, BinectAuthError } from '../src/errors.js';
|
||||
|
||||
describe('BinectApiError', () => {
|
||||
it('creates error with all properties', () => {
|
||||
const response = { error: 'Not found', message: 'Document not found' };
|
||||
const error = new BinectApiError('Not found', 404, '/documents/123', 'GET', response);
|
||||
|
||||
expect(error.name).toBe('BinectApiError');
|
||||
expect(error.message).toBe('Not found');
|
||||
expect(error.status).toBe(404);
|
||||
expect(error.endpoint).toBe('/documents/123');
|
||||
expect(error.method).toBe('GET');
|
||||
expect(error.response).toEqual(response);
|
||||
});
|
||||
|
||||
it('creates error without response', () => {
|
||||
const error = new BinectApiError('Server error', 500, '/documents', 'POST');
|
||||
|
||||
expect(error.status).toBe(500);
|
||||
expect(error.response).toBeNull();
|
||||
});
|
||||
|
||||
it('has correct inheritance', () => {
|
||||
const error = new BinectApiError('Test', 400, '/test', 'GET');
|
||||
|
||||
expect(error instanceof Error).toBe(true);
|
||||
expect(error instanceof BinectApiError).toBe(true);
|
||||
});
|
||||
|
||||
describe('toDetailedString', () => {
|
||||
it('includes all error details', () => {
|
||||
const response = {
|
||||
error: 'Validation error',
|
||||
message: 'Invalid document format',
|
||||
details: ['Page size incorrect', 'Missing address'],
|
||||
};
|
||||
const error = new BinectApiError('Validation failed', 400, '/documents', 'POST', response);
|
||||
|
||||
const detailed = error.toDetailedString();
|
||||
|
||||
expect(detailed).toContain('BinectApiError: Validation failed');
|
||||
expect(detailed).toContain('Status: 400');
|
||||
expect(detailed).toContain('Endpoint: POST /documents');
|
||||
expect(detailed).toContain('Error: Validation error');
|
||||
expect(detailed).toContain('Message: Invalid document format');
|
||||
expect(detailed).toContain('Details: Page size incorrect, Missing address');
|
||||
});
|
||||
|
||||
it('handles minimal error response', () => {
|
||||
const error = new BinectApiError('Server error', 500, '/test', 'GET');
|
||||
|
||||
const detailed = error.toDetailedString();
|
||||
|
||||
expect(detailed).toContain('BinectApiError: Server error');
|
||||
expect(detailed).toContain('Status: 500');
|
||||
// Should not contain "Error:" as a separate field (only in class name)
|
||||
expect(detailed).not.toContain(' Error:');
|
||||
expect(detailed).not.toContain(' Message:');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('BinectAuthError', () => {
|
||||
it('creates authentication error', () => {
|
||||
const error = new BinectAuthError('/accounts', 'GET');
|
||||
|
||||
expect(error.name).toBe('BinectAuthError');
|
||||
expect(error.message).toBe('Authentication failed: Invalid credentials');
|
||||
expect(error.status).toBe(401);
|
||||
expect(error.endpoint).toBe('/accounts');
|
||||
expect(error.method).toBe('GET');
|
||||
});
|
||||
|
||||
it('includes response when provided', () => {
|
||||
const response = { error: 'Unauthorized', message: 'Invalid credentials' };
|
||||
const error = new BinectAuthError('/accounts', 'GET', response);
|
||||
|
||||
expect(error.response).toEqual(response);
|
||||
});
|
||||
|
||||
it('inherits from BinectApiError', () => {
|
||||
const error = new BinectAuthError('/test', 'GET');
|
||||
|
||||
expect(error instanceof Error).toBe(true);
|
||||
expect(error instanceof BinectApiError).toBe(true);
|
||||
expect(error instanceof BinectAuthError).toBe(true);
|
||||
});
|
||||
});
|
||||
281
tests/helpers.test.ts
Normal file
281
tests/helpers.test.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
isShippable,
|
||||
isErroneous,
|
||||
isInPreparation,
|
||||
isInProductionQueue,
|
||||
isPrinting,
|
||||
isSent,
|
||||
isCanceled,
|
||||
isTerminal,
|
||||
isCancelable,
|
||||
getErrors,
|
||||
getWarnings,
|
||||
getInfoMessages,
|
||||
hasErrors,
|
||||
hasWarnings,
|
||||
getStatusDescription,
|
||||
pollUntil,
|
||||
} from '../src/helpers.js';
|
||||
import { DocumentStatus } from '../src/types.js';
|
||||
import type { Document, ValidationMessage } from '../src/types.js';
|
||||
|
||||
// Helper to create mock documents with specific status
|
||||
function createMockDocument(status: DocumentStatus, validationMessages?: ValidationMessage[]): Document {
|
||||
return {
|
||||
id: 12345,
|
||||
filename: 'test-doc.pdf',
|
||||
numberOfPages: 1,
|
||||
status: {
|
||||
code: status,
|
||||
text: 'Test status',
|
||||
},
|
||||
documentType: 'LETTER',
|
||||
letter: {
|
||||
letterType: 'LETTERDATA',
|
||||
letterData: {
|
||||
options: {
|
||||
simplex: false,
|
||||
color: false,
|
||||
franking: 'STANDARD_FRANKING',
|
||||
productionCountry: 'DE',
|
||||
envelope: 'DINLANG',
|
||||
},
|
||||
},
|
||||
errors: validationMessages ?? [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('Status Predicates', () => {
|
||||
describe('isShippable', () => {
|
||||
it('returns true for status SHIPPABLE', () => {
|
||||
const doc = createMockDocument(DocumentStatus.SHIPPABLE);
|
||||
expect(isShippable(doc)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for other statuses', () => {
|
||||
expect(isShippable(createMockDocument(DocumentStatus.IN_PREPARATION))).toBe(false);
|
||||
expect(isShippable(createMockDocument(DocumentStatus.ERRONEOUS))).toBe(false);
|
||||
expect(isShippable(createMockDocument(DocumentStatus.SENT))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isErroneous', () => {
|
||||
it('returns true for status ERRONEOUS', () => {
|
||||
const doc = createMockDocument(DocumentStatus.ERRONEOUS);
|
||||
expect(isErroneous(doc)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for other statuses', () => {
|
||||
expect(isErroneous(createMockDocument(DocumentStatus.SHIPPABLE))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInPreparation', () => {
|
||||
it('returns true for status IN_PREPARATION', () => {
|
||||
const doc = createMockDocument(DocumentStatus.IN_PREPARATION);
|
||||
expect(isInPreparation(doc)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInProductionQueue', () => {
|
||||
it('returns true for status PRODUCTION_QUEUE', () => {
|
||||
const doc = createMockDocument(DocumentStatus.PRODUCTION_QUEUE);
|
||||
expect(isInProductionQueue(doc)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPrinting', () => {
|
||||
it('returns true for status PRINTING', () => {
|
||||
const doc = createMockDocument(DocumentStatus.PRINTING);
|
||||
expect(isPrinting(doc)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSent', () => {
|
||||
it('returns true for status SENT', () => {
|
||||
const doc = createMockDocument(DocumentStatus.SENT);
|
||||
expect(isSent(doc)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCanceled', () => {
|
||||
it('returns true for status CANCELED', () => {
|
||||
const doc = createMockDocument(DocumentStatus.CANCELED);
|
||||
expect(isCanceled(doc)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTerminal', () => {
|
||||
it('returns true for SENT, CANCELED, and ERRONEOUS', () => {
|
||||
expect(isTerminal(createMockDocument(DocumentStatus.SENT))).toBe(true);
|
||||
expect(isTerminal(createMockDocument(DocumentStatus.CANCELED))).toBe(true);
|
||||
expect(isTerminal(createMockDocument(DocumentStatus.ERRONEOUS))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for non-terminal statuses', () => {
|
||||
expect(isTerminal(createMockDocument(DocumentStatus.IN_PREPARATION))).toBe(false);
|
||||
expect(isTerminal(createMockDocument(DocumentStatus.SHIPPABLE))).toBe(false);
|
||||
expect(isTerminal(createMockDocument(DocumentStatus.PRODUCTION_QUEUE))).toBe(false);
|
||||
expect(isTerminal(createMockDocument(DocumentStatus.PRINTING))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCancelable', () => {
|
||||
it('returns true for PRODUCTION_QUEUE and PRINTING', () => {
|
||||
expect(isCancelable(createMockDocument(DocumentStatus.PRODUCTION_QUEUE))).toBe(true);
|
||||
expect(isCancelable(createMockDocument(DocumentStatus.PRINTING))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for non-cancelable statuses', () => {
|
||||
expect(isCancelable(createMockDocument(DocumentStatus.IN_PREPARATION))).toBe(false);
|
||||
expect(isCancelable(createMockDocument(DocumentStatus.SENT))).toBe(false);
|
||||
expect(isCancelable(createMockDocument(DocumentStatus.CANCELED))).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation Helpers', () => {
|
||||
const mockMessages: ValidationMessage[] = [
|
||||
{ type: 'ERROR', code: 'ERR001', message: 'Error 1' },
|
||||
{ type: 'ERROR', code: 'ERR002', message: 'Error 2' },
|
||||
{ type: 'WARNING', code: 'WARN001', message: 'Warning 1' },
|
||||
{ type: 'INFO', code: 'INFO001', message: 'Info 1' },
|
||||
];
|
||||
|
||||
describe('getErrors', () => {
|
||||
it('returns only error messages', () => {
|
||||
const doc = createMockDocument(DocumentStatus.ERRONEOUS, mockMessages);
|
||||
const errors = getErrors(doc);
|
||||
expect(errors).toHaveLength(2);
|
||||
expect(errors.every(m => m.type === 'ERROR')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns empty array when no validation messages', () => {
|
||||
const doc = createMockDocument(DocumentStatus.SHIPPABLE);
|
||||
expect(getErrors(doc)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWarnings', () => {
|
||||
it('returns only warning messages', () => {
|
||||
const doc = createMockDocument(DocumentStatus.SHIPPABLE, mockMessages);
|
||||
const warnings = getWarnings(doc);
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0]?.type).toBe('WARNING');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInfoMessages', () => {
|
||||
it('returns only info messages', () => {
|
||||
const doc = createMockDocument(DocumentStatus.SHIPPABLE, mockMessages);
|
||||
const info = getInfoMessages(doc);
|
||||
expect(info).toHaveLength(1);
|
||||
expect(info[0]?.type).toBe('INFO');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasErrors', () => {
|
||||
it('returns true when document has errors', () => {
|
||||
const doc = createMockDocument(DocumentStatus.ERRONEOUS, mockMessages);
|
||||
expect(hasErrors(doc)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when document has no errors', () => {
|
||||
const doc = createMockDocument(DocumentStatus.SHIPPABLE, [
|
||||
{ type: 'WARNING', code: 'WARN001', message: 'Warning' },
|
||||
]);
|
||||
expect(hasErrors(doc)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasWarnings', () => {
|
||||
it('returns true when document has warnings', () => {
|
||||
const doc = createMockDocument(DocumentStatus.SHIPPABLE, mockMessages);
|
||||
expect(hasWarnings(doc)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when document has no warnings', () => {
|
||||
const doc = createMockDocument(DocumentStatus.SHIPPABLE, [
|
||||
{ type: 'INFO', code: 'INFO001', message: 'Info' },
|
||||
]);
|
||||
expect(hasWarnings(doc)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatusDescription', () => {
|
||||
it('returns correct descriptions for all statuses', () => {
|
||||
expect(getStatusDescription(DocumentStatus.IN_PREPARATION)).toBe('In preparation');
|
||||
expect(getStatusDescription(DocumentStatus.SHIPPABLE)).toBe('Ready to ship');
|
||||
expect(getStatusDescription(DocumentStatus.PRODUCTION_QUEUE)).toBe('In production queue');
|
||||
expect(getStatusDescription(DocumentStatus.PRINTING)).toBe('Printing');
|
||||
expect(getStatusDescription(DocumentStatus.SENT)).toBe('Sent');
|
||||
expect(getStatusDescription(DocumentStatus.CANCELED)).toBe('Canceled');
|
||||
expect(getStatusDescription(DocumentStatus.ERRONEOUS)).toBe('Has errors');
|
||||
});
|
||||
|
||||
it('returns "Unknown status" for invalid status', () => {
|
||||
expect(getStatusDescription(999 as DocumentStatus)).toBe('Unknown status');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Polling Utilities', () => {
|
||||
describe('pollUntil', () => {
|
||||
it('returns immediately when condition is met', async () => {
|
||||
let callCount = 0;
|
||||
const result = await pollUntil(
|
||||
async () => {
|
||||
callCount++;
|
||||
return { value: 42 };
|
||||
},
|
||||
(r) => r.value === 42,
|
||||
{ intervalMs: 10 }
|
||||
);
|
||||
|
||||
expect(result.value).toBe(42);
|
||||
expect(callCount).toBe(1);
|
||||
});
|
||||
|
||||
it('polls until condition is met', async () => {
|
||||
let callCount = 0;
|
||||
const result = await pollUntil(
|
||||
async () => {
|
||||
callCount++;
|
||||
return { value: callCount };
|
||||
},
|
||||
(r) => r.value >= 3,
|
||||
{ intervalMs: 10 }
|
||||
);
|
||||
|
||||
expect(result.value).toBe(3);
|
||||
expect(callCount).toBe(3);
|
||||
});
|
||||
|
||||
it('throws when max attempts exceeded', async () => {
|
||||
await expect(
|
||||
pollUntil(
|
||||
async () => ({ value: 1 }),
|
||||
(r) => r.value > 100,
|
||||
{ intervalMs: 10, maxAttempts: 3 }
|
||||
)
|
||||
).rejects.toThrow('Polling exceeded maximum attempts (3)');
|
||||
});
|
||||
|
||||
it('respects abort signal', async () => {
|
||||
const controller = new AbortController();
|
||||
|
||||
// Abort immediately
|
||||
controller.abort();
|
||||
|
||||
await expect(
|
||||
pollUntil(
|
||||
async () => ({ value: 1 }),
|
||||
() => false,
|
||||
{ intervalMs: 10, signal: controller.signal }
|
||||
)
|
||||
).rejects.toThrow('Polling aborted');
|
||||
});
|
||||
});
|
||||
});
|
||||
221
tests/http.test.ts
Normal file
221
tests/http.test.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { HttpClient, encodeBasicAuth, DEFAULT_BASE_URL } from '../src/http.js';
|
||||
import { BinectApiError, BinectAuthError } from '../src/errors.js';
|
||||
|
||||
describe('encodeBasicAuth', () => {
|
||||
it('encodes credentials correctly', () => {
|
||||
const encoded = encodeBasicAuth('user@example.com', 'password123');
|
||||
// "user@example.com:password123" in base64
|
||||
expect(encoded).toBe('dXNlckBleGFtcGxlLmNvbTpwYXNzd29yZDEyMw==');
|
||||
});
|
||||
|
||||
it('handles special characters', () => {
|
||||
const encoded = encodeBasicAuth('user@example.com', 'p@ss:word!');
|
||||
const decoded = Buffer.from(encoded, 'base64').toString('utf-8');
|
||||
expect(decoded).toBe('user@example.com:p@ss:word!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DEFAULT_BASE_URL', () => {
|
||||
it('points to Binect API', () => {
|
||||
expect(DEFAULT_BASE_URL).toBe('https://app.binect.de/binectapi/v1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('HttpClient', () => {
|
||||
let client: HttpClient;
|
||||
let mockFetch: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockFetch = vi.fn();
|
||||
global.fetch = mockFetch;
|
||||
|
||||
client = new HttpClient({
|
||||
baseUrl: 'https://api.example.com',
|
||||
username: 'testuser',
|
||||
password: 'testpass',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('request', () => {
|
||||
it('makes GET request with auth header', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
text: async () => JSON.stringify({ id: '123' }),
|
||||
});
|
||||
|
||||
const result = await client.request<{ id: string }>({
|
||||
method: 'GET',
|
||||
path: '/test',
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/test',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
headers: expect.objectContaining({
|
||||
Authorization: expect.stringMatching(/^Basic /),
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(result.id).toBe('123');
|
||||
});
|
||||
|
||||
it('makes POST request with JSON body', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
text: async () => JSON.stringify({ success: true }),
|
||||
});
|
||||
|
||||
await client.request({
|
||||
method: 'POST',
|
||||
path: '/test',
|
||||
body: { data: 'test' },
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/test',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ data: 'test' }),
|
||||
headers: expect.objectContaining({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('adds query parameters to URL', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
text: async () => '{}',
|
||||
});
|
||||
|
||||
await client.request({
|
||||
method: 'GET',
|
||||
path: '/test',
|
||||
query: { limit: 10, offset: 20 },
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/test?limit=10&offset=20',
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('skips undefined query parameters', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
text: async () => '{}',
|
||||
});
|
||||
|
||||
await client.request({
|
||||
method: 'GET',
|
||||
path: '/test',
|
||||
query: { limit: 10, offset: undefined },
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/test?limit=10',
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('handles empty response', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'text/plain' }),
|
||||
text: async () => '',
|
||||
});
|
||||
|
||||
const result = await client.request({ method: 'DELETE', path: '/test' });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws BinectAuthError on 401', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 401,
|
||||
text: async () => JSON.stringify({ error: 'Unauthorized' }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.request({ method: 'GET', path: '/test' })
|
||||
).rejects.toThrow(BinectAuthError);
|
||||
});
|
||||
|
||||
it('throws BinectApiError on other errors', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
text: async () => JSON.stringify({ message: 'Not found' }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.request({ method: 'GET', path: '/test' })
|
||||
).rejects.toThrow(BinectApiError);
|
||||
|
||||
try {
|
||||
await client.request({ method: 'GET', path: '/test' });
|
||||
} catch (e) {
|
||||
if (e instanceof BinectApiError) {
|
||||
expect(e.status).toBe(404);
|
||||
expect(e.message).toBe('Not found');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('handles non-JSON error response', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
text: async () => 'Internal Server Error',
|
||||
});
|
||||
|
||||
// Now captures the raw text response as the error message
|
||||
await expect(
|
||||
client.request({ method: 'GET', path: '/test' })
|
||||
).rejects.toThrow('Internal Server Error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestRaw', () => {
|
||||
it('returns raw response for binary data', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/pdf' }),
|
||||
blob: async () => new Blob(['pdf content']),
|
||||
};
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const response = await client.requestRaw({
|
||||
method: 'GET',
|
||||
path: '/documents/123/pdf',
|
||||
});
|
||||
|
||||
expect(response).toBe(mockResponse);
|
||||
});
|
||||
|
||||
it('throws on error response', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
text: async () => JSON.stringify({ message: 'Document not found' }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.requestRaw({ method: 'GET', path: '/documents/123/pdf' })
|
||||
).rejects.toThrow(BinectApiError);
|
||||
});
|
||||
});
|
||||
});
|
||||
52
tests/types.test.ts
Normal file
52
tests/types.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
FrankingType,
|
||||
ProductionCountry,
|
||||
ResponseFormat,
|
||||
} from '../src/types.js';
|
||||
|
||||
describe('Enums', () => {
|
||||
describe('DocumentStatus', () => {
|
||||
it('has correct values', () => {
|
||||
expect(DocumentStatus.IN_PREPARATION).toBe(1);
|
||||
expect(DocumentStatus.SHIPPABLE).toBe(2);
|
||||
expect(DocumentStatus.PRODUCTION_QUEUE).toBe(3);
|
||||
expect(DocumentStatus.PRINTING).toBe(4);
|
||||
expect(DocumentStatus.SENT).toBe(5);
|
||||
expect(DocumentStatus.CANCELED).toBe(6);
|
||||
expect(DocumentStatus.ERRONEOUS).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EnvelopeType', () => {
|
||||
it('has correct values', () => {
|
||||
expect(EnvelopeType.DINLANG).toBe('DINLANG');
|
||||
expect(EnvelopeType.C4).toBe('C4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FrankingType', () => {
|
||||
it('has correct values', () => {
|
||||
expect(FrankingType.UNSPECIFIED).toBe('UNSPECIFIED');
|
||||
expect(FrankingType.STANDARD_FRANKING).toBe('STANDARD_FRANKING');
|
||||
expect(FrankingType.DV_FRANKING).toBe('DV_FRANKING');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProductionCountry', () => {
|
||||
it('has correct values', () => {
|
||||
expect(ProductionCountry.UNSPECIFIED).toBe('UNSPECIFIED');
|
||||
expect(ProductionCountry.DE).toBe('DE');
|
||||
expect(ProductionCountry.AT).toBe('AT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ResponseFormat', () => {
|
||||
it('has correct values', () => {
|
||||
expect(ResponseFormat.FULL).toBe('FULL');
|
||||
expect(ResponseFormat.SHORT).toBe('SHORT');
|
||||
});
|
||||
});
|
||||
});
|
||||
35
tsconfig.json
Normal file
35
tsconfig.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "tests"]
|
||||
}
|
||||
15
vitest.config.ts
Normal file
15
vitest.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['tests/**/*.test.ts'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
include: ['src/**/*.ts'],
|
||||
exclude: ['src/**/*.d.ts'],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user