bysquare 2.13.2 → 3.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 +108 -87
- package/lib/base32hex.d.ts +37 -0
- package/lib/base32hex.js +37 -0
- package/lib/classifier.d.ts +26 -0
- package/lib/classifier.js +26 -0
- package/lib/decode.d.ts +50 -13
- package/lib/decode.js +83 -52
- package/lib/encode.d.ts +108 -8
- package/lib/encode.js +123 -19
- package/lib/index.d.ts +3 -4
- package/lib/index.js +3 -4
- package/lib/types.d.ts +69 -85
- package/lib/types.js +21 -22
- package/lib/validations.d.ts +0 -6
- package/lib/validations.js +5 -5
- package/package.json +12 -8
- package/src/base32hex.ts +110 -0
- package/src/classifier.ts +81 -0
- package/src/cli.ts +137 -0
- package/src/crc32.ts +15 -0
- package/src/deburr.ts +91 -0
- package/src/decode.ts +346 -0
- package/src/encode.ts +443 -0
- package/src/index.ts +12 -0
- package/src/types.ts +604 -0
- package/src/validations.ts +97 -0
- package/lib/helpers.d.ts +0 -34
- package/lib/helpers.js +0 -70
package/src/decode.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { decompress } from "lzma1";
|
|
2
|
+
|
|
3
|
+
import * as base32hex from "./base32hex.js";
|
|
4
|
+
import {
|
|
5
|
+
BankAccount,
|
|
6
|
+
Beneficiary,
|
|
7
|
+
CurrencyCode,
|
|
8
|
+
DataModel,
|
|
9
|
+
type DirectDebit,
|
|
10
|
+
Payment,
|
|
11
|
+
PaymentOptions,
|
|
12
|
+
type Periodicity,
|
|
13
|
+
type StandingOrder,
|
|
14
|
+
Version,
|
|
15
|
+
} from "./index.js";
|
|
16
|
+
|
|
17
|
+
export const DecodeErrorMessage = {
|
|
18
|
+
MissingIBAN: "IBAN is missing",
|
|
19
|
+
/**
|
|
20
|
+
* @description - find original LZMA error in extensions
|
|
21
|
+
*/
|
|
22
|
+
LZMADecompressionFailed: "LZMA decompression failed",
|
|
23
|
+
/**
|
|
24
|
+
* @description - find found version in extensions
|
|
25
|
+
* @see {@link ./types#Version} for valid ranges
|
|
26
|
+
*/
|
|
27
|
+
UnsupportedVersion: "Unsupported version",
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export class DecodeError extends Error {
|
|
31
|
+
public extensions?: { [name: string]: any; };
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
message: string,
|
|
35
|
+
extensions?: { [name: string]: any; },
|
|
36
|
+
) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = this.constructor.name;
|
|
39
|
+
|
|
40
|
+
if (extensions) {
|
|
41
|
+
this.extensions = extensions;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function decodeNumber(value: string | undefined): number | undefined {
|
|
47
|
+
return value?.length ? Number(value) : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function decodeString(value: string | undefined): string | undefined {
|
|
51
|
+
return value?.length ? value : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Converts date from YYYYMMDD format to ISO 8601 format (YYYY-MM-DD)
|
|
56
|
+
* per Pay by Square specification section 3.7.
|
|
57
|
+
*
|
|
58
|
+
* Note: This conversion is only used for paymentDueDate per specification.
|
|
59
|
+
* lastDate remains in YYYYMMDD format.
|
|
60
|
+
*
|
|
61
|
+
* @param input - Date in YYYYMMDD format
|
|
62
|
+
* @returns Date in ISO 8601 format (YYYY-MM-DD) | undefined
|
|
63
|
+
*/
|
|
64
|
+
function deserializeDate(input?: string): string | undefined {
|
|
65
|
+
if (!input || input.length !== 8) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const year = input.slice(0, 4);
|
|
70
|
+
const month = input.slice(4, 6);
|
|
71
|
+
const day = input.slice(6, 8);
|
|
72
|
+
|
|
73
|
+
return year + "-" + month + "-" + day;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parse a tab-separated intermediate format into DataModel.
|
|
78
|
+
*
|
|
79
|
+
* Base fields
|
|
80
|
+
* - Field 0: invoiceId
|
|
81
|
+
* - Field 1: paymentsCount
|
|
82
|
+
*
|
|
83
|
+
* Payment block (repeated `paymentsCount` times)
|
|
84
|
+
* - Field +0: type
|
|
85
|
+
* - Field +1: amount
|
|
86
|
+
* - Field +2: currencyCode
|
|
87
|
+
* - Field +3: paymentDueDate (YYYYMMDD)
|
|
88
|
+
* - Field +4: variableSymbol
|
|
89
|
+
* - Field +5: constantSymbol
|
|
90
|
+
* - Field +6: specificSymbol
|
|
91
|
+
* - Field +7: originatorsReferenceInformation
|
|
92
|
+
* - Field +8: paymentNote
|
|
93
|
+
* - Field +9: bankAccountsCount
|
|
94
|
+
*
|
|
95
|
+
* Bank account block (nested, repeated `bankAccountsCount` times)
|
|
96
|
+
* - Field +0: iban
|
|
97
|
+
* - Field +1: bic
|
|
98
|
+
*
|
|
99
|
+
* Standing order extension
|
|
100
|
+
* - Field +X: standingOrderExt ("0" | "1")
|
|
101
|
+
* - if "1":
|
|
102
|
+
* - Field +1: day
|
|
103
|
+
* - Field +2: month (classifier sum)
|
|
104
|
+
* - Field +3: periodicity
|
|
105
|
+
* - Field +4: lastDate (YYYYMMDD)
|
|
106
|
+
*
|
|
107
|
+
* Direct debit extension
|
|
108
|
+
* - Field +Y: directDebitExt ("0" | "1")
|
|
109
|
+
* - if "1":
|
|
110
|
+
* - Field +1: directDebitScheme
|
|
111
|
+
* - Field +2: directDebitType
|
|
112
|
+
* - Field +3: variableSymbol
|
|
113
|
+
* - Field +4: specificSymbol
|
|
114
|
+
* - Field +5: originatorsReferenceInformation
|
|
115
|
+
* - Field +6: mandateId
|
|
116
|
+
* - Field +7: creditorId
|
|
117
|
+
* - Field +8: contractId
|
|
118
|
+
* - Field +9: maxAmount
|
|
119
|
+
* - Field +10: validTillDate
|
|
120
|
+
*
|
|
121
|
+
* Beneficiary block (repeated per payment)
|
|
122
|
+
* - Field +0: beneficiaryName
|
|
123
|
+
* - Field +1: beneficiaryStreet
|
|
124
|
+
* - Field +2: beneficiaryCity
|
|
125
|
+
*
|
|
126
|
+
* @see 3.14
|
|
127
|
+
* @see Table 15
|
|
128
|
+
*/
|
|
129
|
+
export function deserialize(qr: string): DataModel {
|
|
130
|
+
const data = qr.split("\t");
|
|
131
|
+
const invoiceId = data.shift();
|
|
132
|
+
|
|
133
|
+
const output = {
|
|
134
|
+
invoiceId: invoiceId?.length ? invoiceId : undefined,
|
|
135
|
+
payments: new Array<Payment>(),
|
|
136
|
+
} satisfies DataModel;
|
|
137
|
+
|
|
138
|
+
const paymentslen = Number(data.shift());
|
|
139
|
+
for (let i = 0; i < paymentslen; i++) {
|
|
140
|
+
const paymentOptions = data.shift();
|
|
141
|
+
const ammount = data.shift();
|
|
142
|
+
const currency = data.shift();
|
|
143
|
+
const dueDate = data.shift();
|
|
144
|
+
const variableSymbol = data.shift();
|
|
145
|
+
const constantSymbol = data.shift();
|
|
146
|
+
const specificSymbol = data.shift();
|
|
147
|
+
const originatorRefInfo = data.shift();
|
|
148
|
+
const paymentNote = data.shift();
|
|
149
|
+
|
|
150
|
+
let payment = {
|
|
151
|
+
type: Number(paymentOptions),
|
|
152
|
+
currencyCode: currency ?? CurrencyCode.EUR,
|
|
153
|
+
amount: Number(ammount),
|
|
154
|
+
paymentDueDate: deserializeDate(dueDate),
|
|
155
|
+
variableSymbol: variableSymbol || undefined,
|
|
156
|
+
constantSymbol: constantSymbol || undefined,
|
|
157
|
+
specificSymbol: specificSymbol || undefined,
|
|
158
|
+
originatorsReferenceInformation: originatorRefInfo || undefined,
|
|
159
|
+
paymentNote: paymentNote || undefined,
|
|
160
|
+
bankAccounts: [],
|
|
161
|
+
} as Payment;
|
|
162
|
+
|
|
163
|
+
const numberOfAccounts = Number(data.shift());
|
|
164
|
+
|
|
165
|
+
for (let j = 0; j < numberOfAccounts; j++) {
|
|
166
|
+
const iban = data.shift();
|
|
167
|
+
if (iban === undefined || iban.length === 0) {
|
|
168
|
+
throw new DecodeError(DecodeErrorMessage.MissingIBAN);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const bic = data.shift();
|
|
172
|
+
const account = {
|
|
173
|
+
iban: iban,
|
|
174
|
+
bic: bic || undefined,
|
|
175
|
+
} satisfies BankAccount;
|
|
176
|
+
|
|
177
|
+
payment.bankAccounts.push(account);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const standingOrderExt = data.shift();
|
|
181
|
+
if (standingOrderExt === "1" && payment.type === PaymentOptions.StandingOrder) {
|
|
182
|
+
payment = {
|
|
183
|
+
...payment,
|
|
184
|
+
day: decodeNumber(data.shift()),
|
|
185
|
+
month: decodeNumber(data.shift()),
|
|
186
|
+
periodicity: decodeString(data.shift()) as Periodicity,
|
|
187
|
+
// lastDate stays in YYYYMMDD format (not converted per specification)
|
|
188
|
+
lastDate: decodeString(data.shift()),
|
|
189
|
+
} satisfies StandingOrder;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const directDebitExt = data.shift();
|
|
193
|
+
if (directDebitExt === "1" && payment.type === PaymentOptions.DirectDebit) {
|
|
194
|
+
payment = {
|
|
195
|
+
...payment,
|
|
196
|
+
directDebitScheme: decodeNumber(data.shift()),
|
|
197
|
+
directDebitType: decodeNumber(data.shift()),
|
|
198
|
+
variableSymbol: decodeString(data.shift()),
|
|
199
|
+
specificSymbol: decodeString(data.shift()),
|
|
200
|
+
originatorsReferenceInformation: decodeString(data.shift()),
|
|
201
|
+
mandateId: decodeString(data.shift()),
|
|
202
|
+
creditorId: decodeString(data.shift()),
|
|
203
|
+
contractId: decodeString(data.shift()),
|
|
204
|
+
maxAmount: decodeNumber(data.shift()),
|
|
205
|
+
validTillDate: decodeString(data.shift()),
|
|
206
|
+
} satisfies DirectDebit;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
output.payments.push(payment);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (let i = 0; i < paymentslen; i++) {
|
|
213
|
+
const name = data.shift();
|
|
214
|
+
const addressLine1 = data.shift();
|
|
215
|
+
const addressLine2 = data.shift();
|
|
216
|
+
|
|
217
|
+
if (Boolean(name) || Boolean(addressLine1) || Boolean(addressLine2)) {
|
|
218
|
+
const beneficiary = {
|
|
219
|
+
name: name || undefined,
|
|
220
|
+
street: addressLine1 || undefined,
|
|
221
|
+
city: addressLine2 || undefined,
|
|
222
|
+
} satisfies Beneficiary;
|
|
223
|
+
|
|
224
|
+
output.payments[i].beneficiary = beneficiary;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return output;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
interface Header {
|
|
232
|
+
bysquareType: number;
|
|
233
|
+
version: number;
|
|
234
|
+
documentType: number;
|
|
235
|
+
reserved: number;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Extracts the 4 nibbles from a 2-byte bysquare header using bit-shifting and
|
|
240
|
+
* masking.
|
|
241
|
+
*
|
|
242
|
+
* ```
|
|
243
|
+
* | Attribute | Number of bits | Possible values | Note
|
|
244
|
+
* --------------------------------------------------------------------------------------------
|
|
245
|
+
* | BySquareType | 4 | 0-15 | by square type
|
|
246
|
+
* | Version | 4 | 0-15 | version of the by square type
|
|
247
|
+
* | DocumentType | 4 | 0-15 | document type within given by square type
|
|
248
|
+
* | Reserved | 4 | 0-15 | bits reserved for future needs
|
|
249
|
+
* ```
|
|
250
|
+
*
|
|
251
|
+
* @param header 2-bytes size
|
|
252
|
+
* @see 3.5.
|
|
253
|
+
*/
|
|
254
|
+
function bysquareHeaderDecoder(header: Uint8Array): Header {
|
|
255
|
+
const bytes = (header[0] << 8) | header[1];
|
|
256
|
+
const bysquareType = bytes >> 12;
|
|
257
|
+
const version = (bytes >> 8) & 0b0000_1111;
|
|
258
|
+
const documentType = (bytes >> 4) & 0b0000_1111;
|
|
259
|
+
const reserved = bytes & 0b0000_1111;
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
bysquareType,
|
|
263
|
+
version,
|
|
264
|
+
documentType,
|
|
265
|
+
reserved,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Decoding client data from QR Code 2005 symbol
|
|
271
|
+
*
|
|
272
|
+
* @see 3.16.
|
|
273
|
+
* @param qr base32hex encoded bysqaure binary data
|
|
274
|
+
*/
|
|
275
|
+
export function decode(qr: string): DataModel {
|
|
276
|
+
const bytes = base32hex.decode(qr);
|
|
277
|
+
const headerBytes = bytes.slice(0, 2);
|
|
278
|
+
const headerData = bysquareHeaderDecoder(headerBytes);
|
|
279
|
+
|
|
280
|
+
if ((headerData.version > Version["1.1.0"])) {
|
|
281
|
+
throw new DecodeError(DecodeErrorMessage.UnsupportedVersion, {
|
|
282
|
+
version: headerData.version,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The process of decompressing data requires the addition of an LZMA header
|
|
288
|
+
* to the compressed data. This header is necessary for the decompression
|
|
289
|
+
* algorithm to properly interpret and extract the original uncompressed
|
|
290
|
+
* data. Bysquare only store properties
|
|
291
|
+
*
|
|
292
|
+
* @see https://docs.fileformat.com/compression/lzma/
|
|
293
|
+
*
|
|
294
|
+
* +---------------+---------------------------+-------------------+
|
|
295
|
+
* | 1B | 4B | 8B |
|
|
296
|
+
* +---------------+---------------------------+-------------------+
|
|
297
|
+
* | Properties | Dictionary Size | Uncompressed Size |
|
|
298
|
+
* +---------------+---------------------------+-------------------+
|
|
299
|
+
*/
|
|
300
|
+
const defaultProperties = [0x5D]; // lc=3, lp=0, pb=2
|
|
301
|
+
const defaultDictionarySize = [0x00, 0x00, 0x20, 0x00]; // 2^21 = 2097152
|
|
302
|
+
|
|
303
|
+
// Parse the payload length from bytes 2-3 and properly expand to 8-byte uncompressed size
|
|
304
|
+
const payloadLengthBytes = bytes.slice(2, 4);
|
|
305
|
+
const payloadLength = payloadLengthBytes[0] | (payloadLengthBytes[1] << 8);
|
|
306
|
+
|
|
307
|
+
const uncompressedSize = new Uint8Array(8);
|
|
308
|
+
// Set the full 32-bit value in little-endian format
|
|
309
|
+
uncompressedSize[0] = payloadLength & 0xFF;
|
|
310
|
+
uncompressedSize[1] = (payloadLength >> 8) & 0xFF;
|
|
311
|
+
uncompressedSize[2] = (payloadLength >> 16) & 0xFF;
|
|
312
|
+
uncompressedSize[3] = (payloadLength >> 24) & 0xFF;
|
|
313
|
+
// Bytes 4-7 remain 0 for sizes < 2^32
|
|
314
|
+
|
|
315
|
+
const header = [
|
|
316
|
+
...defaultProperties,
|
|
317
|
+
...defaultDictionarySize,
|
|
318
|
+
...uncompressedSize,
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
const payload = bytes.slice(4);
|
|
322
|
+
const body = new Uint8Array([
|
|
323
|
+
...header,
|
|
324
|
+
...payload,
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
let decompressed: Uint8Array | undefined;
|
|
328
|
+
try {
|
|
329
|
+
decompressed = decompress(body);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
throw new DecodeError(DecodeErrorMessage.LZMADecompressionFailed, { error });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!decompressed) {
|
|
335
|
+
throw new DecodeError(DecodeErrorMessage.LZMADecompressionFailed, {
|
|
336
|
+
error: "Decompression returned undefined",
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Extract checksum and body
|
|
341
|
+
const _checksum = decompressed.slice(0, 4);
|
|
342
|
+
const decompressedBody = decompressed.slice(4);
|
|
343
|
+
const decoded = new TextDecoder("utf-8").decode(decompressedBody.buffer);
|
|
344
|
+
|
|
345
|
+
return deserialize(decoded);
|
|
346
|
+
}
|