@izak0s/mplusqapi-node 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +304 -0
- package/dist/index.d.mts +9343 -0
- package/dist/index.d.ts +9343 -0
- package/dist/index.js +27795 -0
- package/dist/index.mjs +27751 -0
- package/package.json +74 -0
package/README.md
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# mplusqapi-node
|
|
2
|
+
|
|
3
|
+
[](https://github.com/izak0s/mplusqapi-node/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@izak0s/mplusqapi-node)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
A fully-typed TypeScript client for the [MplusKASSA](https://www.mpluskassa.nl) SOAP API (`urn:mplusqapi`), auto-generated from the official WSDL.
|
|
8
|
+
|
|
9
|
+
> **Community package** — This is not an official MplusKASSA package. It is independently developed and maintained by the community. Use at your own risk. For the official PHP client, see [MplusKASSA/mplusqapi-php](https://github.com/MplusKASSA/mplusqapi-php).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **351 typed async methods** covering the full MplusKASSA API surface
|
|
16
|
+
- **Auto-generated** from the official WSDL URL — regenerate anytime the WSDL changes
|
|
17
|
+
- **Fully typed** — 261 enum types and 1235 interfaces, all derived from the WSDL
|
|
18
|
+
- **List flattening** — `*List` wrapper types (e.g. `OrderList`) are transparently unwrapped to plain arrays (`Order[]`)
|
|
19
|
+
- **Decimal-safe** — `xsd:decimal` fields typed as `string` to avoid floating-point precision loss
|
|
20
|
+
- **Date handling** — `SoapMplusDateTime` structs and ISO date fields both map to `Date`
|
|
21
|
+
- **Rich error types** — errors carry the raw `xmlRequest` / `xmlResponse` (where available) for easy debugging
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @izak0s/mplusqapi-node
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Runtime dependencies:**
|
|
32
|
+
- [`axios`](https://github.com/axios/axios) — HTTP transport
|
|
33
|
+
- [`fast-xml-parser`](https://github.com/NaturalIntelligence/fast-xml-parser) — XML parsing
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import {
|
|
41
|
+
MplusKassaClient,
|
|
42
|
+
MplusApiClientError,
|
|
43
|
+
MplusApiServerError,
|
|
44
|
+
MplusApiCommunicationError,
|
|
45
|
+
} from '@izak0s/mplusqapi-node';
|
|
46
|
+
|
|
47
|
+
const client = new MplusKassaClient({
|
|
48
|
+
host: process.env.MPLUS_HOST!,
|
|
49
|
+
port: Number(process.env.MPLUS_PORT!),
|
|
50
|
+
ident: process.env.MPLUS_IDENT!,
|
|
51
|
+
secret: process.env.MPLUS_SECRET!,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Fetch API version
|
|
55
|
+
const version = await client.getApiVersion();
|
|
56
|
+
console.log(`API: ${version.majorNumber}.${version.minorNumber}.${version.revisionNumber}`);
|
|
57
|
+
|
|
58
|
+
// Fetch orders (returns Order[] directly — list wrappers are unwrapped)
|
|
59
|
+
const orders = await client.getOrders({ syncMarker: 0, syncMarkerLimit: 10 });
|
|
60
|
+
for (const order of orders) {
|
|
61
|
+
console.log(order.orderId, order.financialDate);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fetch a single relation — `relation` is undefined when result is NOT-FOUND
|
|
65
|
+
const { result, relation } = await client.getRelation(42);
|
|
66
|
+
if (result === 'GET-RELATION-RESULT-OK') {
|
|
67
|
+
console.log(relation?.name, relation?.email);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Authentication
|
|
74
|
+
|
|
75
|
+
Credentials are passed as **URL query parameters** (`?ident=X&secret=Y`), not in SOAP headers. Pass them to the constructor:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const client = new MplusKassaClient({
|
|
79
|
+
host: process.env.MPLUS_HOST!, // hostname only, no protocol
|
|
80
|
+
port: Number(process.env.MPLUS_PORT!),
|
|
81
|
+
ident: process.env.MPLUS_IDENT!,
|
|
82
|
+
secret: process.env.MPLUS_SECRET!,
|
|
83
|
+
rejectUnauthorized: false, // set to false for self-signed certs (e.g. local test servers)
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Transport options
|
|
88
|
+
|
|
89
|
+
| Option | Default | Description |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `timeout` | `30` | Request timeout in seconds |
|
|
92
|
+
| `maxRetries` | `3` | Retry attempts on retryable transport errors (see below; SOAP faults are never retried) |
|
|
93
|
+
| `retryDelay` | `500` | Base retry delay in ms, doubled per attempt (exponential backoff with jitter) |
|
|
94
|
+
| `rejectUnauthorized` | `true` | Set `false` to accept self-signed TLS certificates |
|
|
95
|
+
| `signal` | — | `AbortSignal` to cancel all in-flight requests from this client (e.g. on shutdown) |
|
|
96
|
+
|
|
97
|
+
### Retries and idempotency
|
|
98
|
+
|
|
99
|
+
Retrying a mutation after an ambiguous network failure (e.g. a connection reset after the request was sent) could execute it twice — duplicate orders, double payments. The client guards against this:
|
|
100
|
+
|
|
101
|
+
- **Idempotent operations** (requests carrying an `idempotencyKey`, e.g. `createOrderV3`, `payOrderV2`): the key is **auto-generated** (UUID) when you don't provide one, and any transport error is retried freely — the server deduplicates by key. Provide your own key to make retries safe across process restarts.
|
|
102
|
+
- **All other operations**: only errors where the request provably never reached the server are retried (`ECONNREFUSED`, `ENOTFOUND`, DNS failures, unreachable host/network). Timeouts, connection resets, and HTTP 5xx responses are **not** retried — inspect `MplusApiCommunicationError.code` / `.httpStatus` and decide yourself.
|
|
103
|
+
|
|
104
|
+
All attempts of one call share the same `X-Request-Id` header.
|
|
105
|
+
|
|
106
|
+
### Response guarantees
|
|
107
|
+
|
|
108
|
+
- List fields are always present on responses (`[]` when the server omits them).
|
|
109
|
+
- Complex response fields (e.g. `GetRelationResponse.relation`) are `undefined` when the server omits them — check the accompanying `result` field.
|
|
110
|
+
- A required scalar field missing from a response throws `MplusApiDeserializationError` instead of silently producing `''`/`NaN`/`false`.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Error Handling
|
|
115
|
+
|
|
116
|
+
All errors extend `MplusApiError` and carry the raw XML for debugging:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import {
|
|
120
|
+
MplusApiClientError, // SOAP fault with Client.* faultcode
|
|
121
|
+
MplusApiServerError, // SOAP fault with Server.* faultcode
|
|
122
|
+
MplusApiCommunicationError, // network / HTTP error
|
|
123
|
+
MplusApiSerializationError, // failed to build request XML
|
|
124
|
+
MplusApiDeserializationError, // failed to parse response XML
|
|
125
|
+
} from '@izak0s/mplusqapi-node';
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await client.getOrder('invalid-id');
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (err instanceof MplusApiClientError) {
|
|
131
|
+
console.error(`Client fault [${err.faultCode}]: ${err.message}`);
|
|
132
|
+
console.error('Request XML:', err.xmlRequest);
|
|
133
|
+
console.error('Response XML:', err.xmlResponse);
|
|
134
|
+
} else if (err instanceof MplusApiServerError) {
|
|
135
|
+
console.error(`Server fault [${err.faultCode}]: ${err.message}`);
|
|
136
|
+
} else if (err instanceof MplusApiCommunicationError) {
|
|
137
|
+
console.error(`Network error: ${err.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Type System
|
|
145
|
+
|
|
146
|
+
### Enums
|
|
147
|
+
|
|
148
|
+
Enum fields are typed as TypeScript string unions:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import type { OrderType, ArticleType } from '@izak0s/mplusqapi-node';
|
|
152
|
+
|
|
153
|
+
const type: OrderType = 'ORDER-TYPE-SALES-ORDER';
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Money and decimal fields
|
|
157
|
+
|
|
158
|
+
The WSDL uses two conventions for money/quantity values, and the generated types mirror them faithfully:
|
|
159
|
+
|
|
160
|
+
- **`xsd:decimal` → `string`** — fractional values like `"12.50"`. Kept as strings end-to-end to preserve precision (the XML parser is configured to never coerce them to floats).
|
|
161
|
+
- **`xsd:long` → `number`** — scaled integers, typically cents (look for sibling fields like `minimumAmountDecimalPlaces`). Safe as JS numbers.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// xsd:decimal — string, e.g. SalesLineContractLine
|
|
165
|
+
const price: string = contractLine.priceIncl; // "12.50", not 12.5
|
|
166
|
+
|
|
167
|
+
// xsd:long — integer cents, e.g. Payment
|
|
168
|
+
const payment = { method: 'CASH', amount: 1000 }; // €10.00
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The same field name (e.g. `priceIncl`) can be a `string` on one type and a `number` on another — trust the TypeScript type, it reflects what the API sends.
|
|
172
|
+
|
|
173
|
+
### Dates
|
|
174
|
+
|
|
175
|
+
`SoapMplusDateTime` response fields are deserialized to `Date` objects. Pass `Date` objects for request fields that accept dates.
|
|
176
|
+
|
|
177
|
+
### List fields
|
|
178
|
+
|
|
179
|
+
`*List` wrapper types are flattened to plain arrays in both requests and responses:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// Response: getOrders returns Order[] directly, not { orderList: ... }
|
|
183
|
+
const orders = await client.getOrders({});
|
|
184
|
+
const first: Order = orders[0];
|
|
185
|
+
|
|
186
|
+
// Request: pass an array directly
|
|
187
|
+
await client.payTableOrder({
|
|
188
|
+
terminal: { branchNumber: 1, terminalNumber: 40 },
|
|
189
|
+
paymentList: [{ method: 'CASH', amount: 1000 }], // amount in cents
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Request input types
|
|
194
|
+
|
|
195
|
+
Request parameters use `Input<T>` — a deep-partial variant of the response types. All fields are optional when building requests; fields the WSDL marks required describe what *responses* are guaranteed to contain (e.g. `Order.orderId` is assigned by the server on create). Omitted fields are simply not serialized. The server validates true requirements at runtime and responds with a SOAP fault.
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// Order without orderId — server assigns it
|
|
199
|
+
await client.createOrderV3({
|
|
200
|
+
order: { lineList: [{ articleNumber: 123, data: { quantity: 1 } }] },
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Request tracing
|
|
207
|
+
|
|
208
|
+
Pass an optional `requestId` as the last argument to any method for correlation logging:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const result = await client.getRelations({ syncMarker: 0 }, 'req-abc-123');
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The ID is sent as the `X-Request-Id` header.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
### Regenerate from WSDL
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# Regenerate src/generated/ from a WSDL URL
|
|
224
|
+
npm run generate -- 'https://api.mpluskassa.nl:PORT/?wsdl'
|
|
225
|
+
|
|
226
|
+
# Or keep the URL out of shell history by using an environment variable
|
|
227
|
+
MPLUS_WSDL_URL='https://api.mpluskassa.nl:PORT/?wsdl' npm run generate
|
|
228
|
+
|
|
229
|
+
# Or regenerate from the cached local WSDL
|
|
230
|
+
npm run generate:local
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Build
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
npm run build # tsc → dist/
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Output (CommonJS + declarations):
|
|
240
|
+
```
|
|
241
|
+
dist/
|
|
242
|
+
index.js / index.d.ts entry point
|
|
243
|
+
errors.js, soap.js, transport.js (+ .d.ts)
|
|
244
|
+
generated/ client, types, serializer, deserializer
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Test
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
npm test # node:test suite (test/)
|
|
251
|
+
npm run test:types # tsc --noEmit over src + test
|
|
252
|
+
npm run check # build + both of the above (what CI runs)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Release
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
npm version patch # or minor / major — bumps package.json, commits, tags
|
|
259
|
+
git push --follow-tags
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The tag triggers `.github/workflows/publish.yml`: gate on `npm run check`, publish to npm via [trusted publishing](https://docs.npmjs.com/trusted-publishers) (OIDC, no token), and create a GitHub release with generated notes. The workflow refuses to publish if the tag doesn't match `package.json`.
|
|
263
|
+
|
|
264
|
+
### Run the example
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
cp .env.example .env # fill in your credentials
|
|
268
|
+
source .env && npx ts-node --project scripts/tsconfig.json example.ts
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Architecture
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
src/
|
|
277
|
+
index.ts Public exports
|
|
278
|
+
errors.ts Error hierarchy
|
|
279
|
+
soap.ts Envelope builder, response parser, serializers
|
|
280
|
+
transport.ts HTTP client (axios)
|
|
281
|
+
generated/ Auto-generated — do not edit manually
|
|
282
|
+
types.ts TypeScript interfaces and string union enums
|
|
283
|
+
serializer.ts TS objects → SOAP XML
|
|
284
|
+
deserializer.ts Response XML → typed TS objects
|
|
285
|
+
client.ts MplusKassaClient with all 351 methods
|
|
286
|
+
|
|
287
|
+
scripts/
|
|
288
|
+
generate.ts WSDL parser + code generator
|
|
289
|
+
wsdl.xml Cached WSDL for offline/local regeneration
|
|
290
|
+
(gitignored — fetch once via `npm run generate -- <url>`
|
|
291
|
+
before `npm run generate:local` works)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Contributing & security
|
|
297
|
+
|
|
298
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Vulnerabilities: report privately via [SECURITY.md](SECURITY.md), not public issues.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## License
|
|
303
|
+
|
|
304
|
+
[GPL-3.0-only](LICENSE)
|