@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/README.md ADDED
@@ -0,0 +1,304 @@
1
+ # mplusqapi-node
2
+
3
+ [![CI](https://github.com/izak0s/mplusqapi-node/actions/workflows/ci.yml/badge.svg)](https://github.com/izak0s/mplusqapi-node/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/%40izak0s%2Fmplusqapi-node)](https://www.npmjs.com/package/@izak0s/mplusqapi-node)
5
+ [![license](https://img.shields.io/badge/license-GPL--3.0--only-blue)](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)