bysquare 3.1.0 → 3.2.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 +8 -8
- package/lib/cli.js +179 -106
- package/lib/decode.js +1 -20
- package/lib/encode.d.ts +2 -3
- package/lib/encode.js +1 -17
- package/lib/index.d.ts +1 -1
- package/lib/types.d.ts +3 -4
- package/lib/validations.d.ts +3 -3
- package/lib/validations.js +39 -3
- package/package.json +2 -2
- package/src/cli.ts +189 -109
- package/src/decode.ts +1 -23
- package/src/encode.ts +3 -21
- package/src/index.ts +1 -1
- package/src/types.ts +3 -4
- package/src/validations.ts +52 -5
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
<h1 align="center">bysquare</h1>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
"PAY by square" is a national standard for QR code payments that was adopted by
|
|
5
|
-
the Slovak Banking Association in 2013. It is incorporated into a variety of
|
|
6
|
-
invoices, reminders and other payment regulations.
|
|
4
|
+
"PAY by square" is a national standard for QR code payments that was adopted by
|
|
5
|
+
the Slovak Banking Association in 2013. It is incorporated into a variety of
|
|
6
|
+
invoices, reminders and other payment regulations.
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
<a href="#features">Features</a> •
|
|
11
|
+
<a href="#installation">Installation</a> •
|
|
12
|
+
<a href="#usage">Usage</a> •
|
|
13
|
+
<a href="#cli">CLI</a> •
|
|
14
|
+
<a href="#validation">Validation</a>
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
## Features
|
package/lib/cli.js
CHANGED
|
@@ -1,119 +1,192 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, statSync, } from "node:fs";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
import { decode } from "./decode.js";
|
|
6
|
-
import { encode } from "./encode.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
6
|
+
import { encode, } from "./encode.js";
|
|
7
|
+
import { Version } from "./types.js";
|
|
8
|
+
const version = "3.1.0";
|
|
9
|
+
const usage = `bysquare - Slovak PAY by square QR payment standard
|
|
10
|
+
|
|
11
|
+
USAGE:
|
|
12
|
+
bysquare encode [OPTIONS] <input.json>
|
|
13
|
+
bysquare decode <qr-string>
|
|
14
|
+
bysquare version
|
|
15
|
+
|
|
16
|
+
COMMANDS:
|
|
17
|
+
encode Encode JSON payment data to BySquare QR string
|
|
18
|
+
decode Decode BySquare QR string to JSON payment data
|
|
19
|
+
version Print version information
|
|
20
|
+
|
|
21
|
+
ENCODE OPTIONS:
|
|
22
|
+
-D, --no-deburr Keep diacritics (deburr enabled by default)
|
|
23
|
+
-V, --no-validate Skip validation (validation enabled by default)
|
|
24
|
+
-s, --spec-version VER Specification version: 1.0.0, 1.1.0, 1.2.0 (default: 1.2.0)
|
|
25
|
+
|
|
26
|
+
EXAMPLES:
|
|
27
|
+
# Encode with defaults (deburr=true, validate=true, version=1.2.0)
|
|
28
|
+
$ bysquare encode payment.json
|
|
29
|
+
|
|
30
|
+
# Encode with specific options
|
|
31
|
+
$ bysquare encode --no-deburr payment.json
|
|
32
|
+
$ bysquare encode --spec-version 1.1.0 payment.json
|
|
33
|
+
$ bysquare encode -s 1.0.0 --no-validate payment.json
|
|
34
|
+
|
|
35
|
+
# Encode from stdin
|
|
36
|
+
$ echo '{"payments":[...]}' | bysquare encode -
|
|
37
|
+
|
|
38
|
+
# Encode multiple files (including JSONL)
|
|
39
|
+
$ bysquare encode file1.json file2.jsonl
|
|
40
|
+
|
|
41
|
+
# Decode QR string
|
|
42
|
+
$ bysquare decode "00D80..."
|
|
43
|
+
|
|
44
|
+
# Decode from file
|
|
45
|
+
$ bysquare decode qr.txt
|
|
46
|
+
|
|
47
|
+
For more information, visit: https://github.com/xseman/bysquare
|
|
48
|
+
`;
|
|
49
|
+
function errorMessage(error) {
|
|
50
|
+
return error instanceof Error
|
|
51
|
+
? error.message
|
|
52
|
+
: String(error);
|
|
53
|
+
}
|
|
54
|
+
async function readStdin() {
|
|
55
|
+
const chunks = [];
|
|
56
|
+
for await (const chunk of process.stdin) {
|
|
57
|
+
chunks.push(chunk);
|
|
58
|
+
}
|
|
59
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
60
|
+
}
|
|
61
|
+
async function readInput(path) {
|
|
62
|
+
if (path === "-") {
|
|
63
|
+
return readStdin();
|
|
64
|
+
}
|
|
65
|
+
if (!existsSync(path)) {
|
|
66
|
+
throw new Error(`file ${path} doesn't exist`);
|
|
67
|
+
}
|
|
68
|
+
return readFileSync(path, "utf8");
|
|
69
|
+
}
|
|
70
|
+
async function cmdEncode(args) {
|
|
71
|
+
const parsed = parseArgs({
|
|
72
|
+
args,
|
|
73
|
+
allowPositionals: true,
|
|
74
|
+
options: {
|
|
75
|
+
"no-deburr": {
|
|
76
|
+
type: "boolean",
|
|
77
|
+
short: "D",
|
|
78
|
+
},
|
|
79
|
+
"no-validate": {
|
|
80
|
+
type: "boolean",
|
|
81
|
+
short: "V",
|
|
82
|
+
},
|
|
83
|
+
"spec-version": {
|
|
84
|
+
type: "string",
|
|
85
|
+
short: "s",
|
|
86
|
+
default: "1.2.0",
|
|
87
|
+
},
|
|
29
88
|
},
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
89
|
+
});
|
|
90
|
+
if (parsed.positionals.length === 0) {
|
|
91
|
+
console.error("Error: missing input file argument");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
const versionStr = parsed.values["spec-version"];
|
|
95
|
+
if (!(versionStr in Version)) {
|
|
96
|
+
console.error("Error: unsupported spec version:", parsed.values["spec-version"]);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const encodeOpts = {
|
|
100
|
+
validate: !parsed.values["no-validate"],
|
|
101
|
+
deburr: !parsed.values["no-deburr"],
|
|
102
|
+
version: Version[versionStr],
|
|
103
|
+
};
|
|
104
|
+
for (const inputFile of parsed.positionals) {
|
|
105
|
+
let input;
|
|
106
|
+
try {
|
|
107
|
+
input = await readInput(inputFile);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.error("Error:", errorMessage(error));
|
|
36
111
|
process.exit(1);
|
|
37
112
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (file.endsWith(".json") === false
|
|
44
|
-
&& file.endsWith(".jsonl") === false) {
|
|
45
|
-
console.error(`Unsupported file format for ${file}`);
|
|
46
|
-
process.exit(1);
|
|
113
|
+
if (inputFile.endsWith(".jsonl")) {
|
|
114
|
+
for (const line of input.split("\n")) {
|
|
115
|
+
if (!line.trim())
|
|
116
|
+
continue;
|
|
117
|
+
encodeAndPrint(line, encodeOpts);
|
|
47
118
|
}
|
|
48
|
-
|
|
49
|
-
if (file.endsWith(".jsonl")) {
|
|
50
|
-
const lines = data.split("\n");
|
|
51
|
-
for (const line of lines) {
|
|
52
|
-
if (!line)
|
|
53
|
-
continue;
|
|
54
|
-
const json = JSON.parse(line);
|
|
55
|
-
console.log(encode(json));
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
if (file.endsWith(".json")) {
|
|
59
|
-
console.log(encode(JSON.parse(data), {
|
|
60
|
-
validate: Boolean(args.values.validate),
|
|
61
|
-
deburr: Boolean(args.values.deburr),
|
|
62
|
-
}));
|
|
63
|
-
}
|
|
64
|
-
process.exit(0);
|
|
119
|
+
continue;
|
|
65
120
|
}
|
|
121
|
+
encodeAndPrint(input.trim(), encodeOpts);
|
|
66
122
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
123
|
+
}
|
|
124
|
+
function encodeAndPrint(jsonStr, opts) {
|
|
125
|
+
try {
|
|
126
|
+
const data = JSON.parse(jsonStr);
|
|
127
|
+
const result = encode(data, opts);
|
|
128
|
+
console.log(result);
|
|
71
129
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"NAME",
|
|
76
|
-
" bysquare - Simple Node.js library to generate and parse PAY bysquare standard",
|
|
77
|
-
"",
|
|
78
|
-
"SYNOPSIS",
|
|
79
|
-
" bysquare [OPTIONS] [FILES...]",
|
|
80
|
-
"",
|
|
81
|
-
"DESCRIPTION",
|
|
82
|
-
" bysquare is a command-line tool that provides a simple Node.js library to generate ",
|
|
83
|
-
" and parse PAY bysquare standard. It offers functionality to encode JSON data into a ",
|
|
84
|
-
" corresponding QR code and decode a QR code string to obtain the associated JSON data.",
|
|
85
|
-
"",
|
|
86
|
-
"OPTIONS",
|
|
87
|
-
" -d, --decode <qrstring>",
|
|
88
|
-
" Decode the specified QR code string and print the corresponding JSON data.",
|
|
89
|
-
" The qrstring argument should be a valid QR code string.",
|
|
90
|
-
"",
|
|
91
|
-
" -e, --encode",
|
|
92
|
-
" Encode JSON data from one or more files and print the corresponding QR code.",
|
|
93
|
-
"",
|
|
94
|
-
" -v, --validate",
|
|
95
|
-
" Validate JSON data from one or more files before encoding.",
|
|
96
|
-
"",
|
|
97
|
-
" -b, --deburr",
|
|
98
|
-
" Deburr JSON data from one or more files before encoding.",
|
|
99
|
-
"",
|
|
100
|
-
" -h, --help",
|
|
101
|
-
" Display the help message and exit.",
|
|
102
|
-
"",
|
|
103
|
-
"USAGE",
|
|
104
|
-
" Encoding JSON data from one or more files",
|
|
105
|
-
"",
|
|
106
|
-
` ${process.argv[1]} --encode file1.json file2.json ...`,
|
|
107
|
-
" The file1.json, file2.json, ... arguments should be the paths to the JSON or JSONL",
|
|
108
|
-
" files you want to encode. The tool will read each file, generate a QR code representing",
|
|
109
|
-
" the JSON data, and print them.",
|
|
110
|
-
"",
|
|
111
|
-
" Decoding a QR code string",
|
|
112
|
-
"",
|
|
113
|
-
` ${process.argv[1]} --decode <qrstring>`,
|
|
114
|
-
" Replace qrstring with the QR code string you want to decode.",
|
|
115
|
-
" The program will parse the QR code string and print the resulting JSON data.",
|
|
116
|
-
"",
|
|
117
|
-
].join("\n"));
|
|
130
|
+
catch (error) {
|
|
131
|
+
console.error("Error:", errorMessage(error));
|
|
132
|
+
process.exit(1);
|
|
118
133
|
}
|
|
119
134
|
}
|
|
135
|
+
async function cmdDecode(args) {
|
|
136
|
+
if (args.length === 0) {
|
|
137
|
+
console.error("Error: missing QR string argument");
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
const qrInput = args[0];
|
|
141
|
+
try {
|
|
142
|
+
let qr;
|
|
143
|
+
if (qrInput === "-") {
|
|
144
|
+
qr = await readStdin();
|
|
145
|
+
}
|
|
146
|
+
else if (existsSync(qrInput) && statSync(qrInput).isFile()) {
|
|
147
|
+
qr = readFileSync(qrInput, "utf8");
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
qr = qrInput;
|
|
151
|
+
}
|
|
152
|
+
const model = decode(qr.trim());
|
|
153
|
+
console.log(JSON.stringify(model, null, 2));
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error("Error:", errorMessage(error));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function main() {
|
|
161
|
+
if (process.argv.length < 3) {
|
|
162
|
+
console.error(usage);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
const command = process.argv[2];
|
|
166
|
+
switch (command) {
|
|
167
|
+
case "encode":
|
|
168
|
+
await cmdEncode(process.argv.slice(3));
|
|
169
|
+
break;
|
|
170
|
+
case "decode":
|
|
171
|
+
await cmdDecode(process.argv.slice(3));
|
|
172
|
+
break;
|
|
173
|
+
case "version":
|
|
174
|
+
case "-v":
|
|
175
|
+
case "--version":
|
|
176
|
+
console.log(`bysquare version ${version}`);
|
|
177
|
+
break;
|
|
178
|
+
case "help":
|
|
179
|
+
case "-h":
|
|
180
|
+
case "--help":
|
|
181
|
+
console.log(usage);
|
|
182
|
+
break;
|
|
183
|
+
default:
|
|
184
|
+
console.error("Unknown command:", command);
|
|
185
|
+
console.error(usage);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
main().catch((error) => {
|
|
190
|
+
console.error("Fatal error:", error);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
});
|
package/lib/decode.js
CHANGED
|
@@ -29,25 +29,6 @@ function decodeNumber(value) {
|
|
|
29
29
|
function decodeString(value) {
|
|
30
30
|
return value?.length ? value : undefined;
|
|
31
31
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Converts date from YYYYMMDD format to ISO 8601 format (YYYY-MM-DD)
|
|
34
|
-
* per Pay by Square specification section 3.7.
|
|
35
|
-
*
|
|
36
|
-
* Note: This conversion is only used for paymentDueDate per specification.
|
|
37
|
-
* lastDate remains in YYYYMMDD format.
|
|
38
|
-
*
|
|
39
|
-
* @param input - Date in YYYYMMDD format
|
|
40
|
-
* @returns Date in ISO 8601 format (YYYY-MM-DD) | undefined
|
|
41
|
-
*/
|
|
42
|
-
function deserializeDate(input) {
|
|
43
|
-
if (!input || input.length !== 8) {
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
const year = input.slice(0, 4);
|
|
47
|
-
const month = input.slice(4, 6);
|
|
48
|
-
const day = input.slice(6, 8);
|
|
49
|
-
return year + "-" + month + "-" + day;
|
|
50
|
-
}
|
|
51
32
|
/**
|
|
52
33
|
* Parse a tab-separated intermediate format into DataModel.
|
|
53
34
|
*
|
|
@@ -123,7 +104,7 @@ export function deserialize(qr) {
|
|
|
123
104
|
type: Number(paymentOptions),
|
|
124
105
|
currencyCode: currency ?? CurrencyCode.EUR,
|
|
125
106
|
amount: Number(ammount),
|
|
126
|
-
paymentDueDate:
|
|
107
|
+
paymentDueDate: dueDate || undefined,
|
|
127
108
|
variableSymbol: variableSymbol || undefined,
|
|
128
109
|
constantSymbol: constantSymbol || undefined,
|
|
129
110
|
specificSymbol: specificSymbol || undefined,
|
package/lib/encode.d.ts
CHANGED
|
@@ -141,7 +141,7 @@ export declare function addChecksum(tabbedPayload: string): Uint8Array;
|
|
|
141
141
|
*/
|
|
142
142
|
export declare function serialize(data: DataModel): string;
|
|
143
143
|
export declare function removeDiacritics(model: DataModel): void;
|
|
144
|
-
type
|
|
144
|
+
export type EncodeOptions = {
|
|
145
145
|
/**
|
|
146
146
|
* Many banking apps do not support diacritics, which results in errors when
|
|
147
147
|
* serializing data from QR codes.
|
|
@@ -199,5 +199,4 @@ type Options = {
|
|
|
199
199
|
*
|
|
200
200
|
* @see 3.16.
|
|
201
201
|
*/
|
|
202
|
-
export declare function encode(model: DataModel, options?:
|
|
203
|
-
export {};
|
|
202
|
+
export declare function encode(model: DataModel, options?: EncodeOptions): string;
|
package/lib/encode.js
CHANGED
|
@@ -4,22 +4,6 @@ import { crc32 } from "./crc32.js";
|
|
|
4
4
|
import { deburr } from "./deburr.js";
|
|
5
5
|
import { Month, PaymentOptions, Version, } from "./types.js";
|
|
6
6
|
import { validateDataModel } from "./validations.js";
|
|
7
|
-
/**
|
|
8
|
-
* Converts date from ISO 8601 format (YYYY-MM-DD) to YYYYMMDD format
|
|
9
|
-
* per Pay by Square specification section 3.7.
|
|
10
|
-
*
|
|
11
|
-
* Note: This conversion is only used for paymentDueDate per specification.
|
|
12
|
-
* lastDate expects YYYYMMDD format directly.
|
|
13
|
-
*
|
|
14
|
-
* @param input - Date in ISO 8601 format (YYYY-MM-DD)
|
|
15
|
-
* @returns Date in YYYYMMDD format | undefined
|
|
16
|
-
*/
|
|
17
|
-
function serializeDate(input) {
|
|
18
|
-
if (!input) {
|
|
19
|
-
return undefined;
|
|
20
|
-
}
|
|
21
|
-
return input.split("-").join("");
|
|
22
|
-
}
|
|
23
7
|
export const EncodeErrorMessage = {
|
|
24
8
|
/**
|
|
25
9
|
* @description - find invalid value in extensions
|
|
@@ -206,7 +190,7 @@ export function serialize(data) {
|
|
|
206
190
|
serialized.push(p.type.toString());
|
|
207
191
|
serialized.push(p.amount?.toString());
|
|
208
192
|
serialized.push(p.currencyCode);
|
|
209
|
-
serialized.push(
|
|
193
|
+
serialized.push(p.paymentDueDate);
|
|
210
194
|
serialized.push(p.variableSymbol);
|
|
211
195
|
serialized.push(p.constantSymbol);
|
|
212
196
|
serialized.push(p.specificSymbol);
|
package/lib/index.d.ts
CHANGED
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { decodeOptions, encodeOptions } from "./classifier.js";
|
|
7
7
|
export { decode } from "./decode.js";
|
|
8
|
-
export { encode } from "./encode.js";
|
|
8
|
+
export { encode, type EncodeOptions } from "./encode.js";
|
|
9
9
|
export { validateDataModel } from "./validations.js";
|
|
10
10
|
export * from "./types.js";
|
package/lib/types.d.ts
CHANGED
|
@@ -196,14 +196,13 @@ export type SimplePayment = {
|
|
|
196
196
|
*/
|
|
197
197
|
currencyCode: string | keyof typeof CurrencyCode;
|
|
198
198
|
/**
|
|
199
|
-
* Payment due date.
|
|
199
|
+
* Payment due date in YYYYMMDD format per v1.2 specification section 3.7.
|
|
200
200
|
*
|
|
201
201
|
* For standing orders, this indicates the first payment date.
|
|
202
|
-
* The date will be converted to YYYYMMDD format during encoding per specification section 3.7.
|
|
203
202
|
*
|
|
204
203
|
* @format date
|
|
205
|
-
* @example "
|
|
206
|
-
* @pattern \d{
|
|
204
|
+
* @example "20241231"
|
|
205
|
+
* @pattern \d{8}
|
|
207
206
|
*/
|
|
208
207
|
paymentDueDate?: string;
|
|
209
208
|
/**
|
package/lib/validations.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BankAccount, DataModel,
|
|
1
|
+
import { BankAccount, DataModel, type Payment } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* This error will be thrown in case of a validation issue. It provides message with error description and specific path to issue in dataModel object.
|
|
4
4
|
*/
|
|
@@ -19,12 +19,12 @@ export declare function validateBankAccount(bankAccount: BankAccount, path: stri
|
|
|
19
19
|
/**
|
|
20
20
|
* validate simple payment fields:
|
|
21
21
|
* - currencyCode (ISO 4217)
|
|
22
|
-
* - paymentDueDate (
|
|
22
|
+
* - paymentDueDate (YYYYMMDD format per v1.2 specification)
|
|
23
23
|
* - bankAccounts
|
|
24
24
|
*
|
|
25
25
|
* @see validateBankAccount
|
|
26
26
|
*/
|
|
27
|
-
export declare function validateSimplePayment(simplePayment:
|
|
27
|
+
export declare function validateSimplePayment(simplePayment: Payment, path: string): void;
|
|
28
28
|
/**
|
|
29
29
|
* Validate `payments` field of dataModel.
|
|
30
30
|
*
|
package/lib/validations.js
CHANGED
|
@@ -1,11 +1,41 @@
|
|
|
1
1
|
import validator from "validator";
|
|
2
|
+
import { PaymentOptions, } from "./types.js";
|
|
2
3
|
const ErrorMessages = {
|
|
3
4
|
IBAN: "Invalid IBAN. Make sure ISO 13616 format is used.",
|
|
4
5
|
BIC: "Invalid BIC. Make sure ISO 9362 format is used.",
|
|
5
6
|
CurrencyCode: "Invalid currency code. Make sure ISO 4217 format is used.",
|
|
6
|
-
Date: "Invalid date. Make sure
|
|
7
|
+
Date: "Invalid date. Make sure YYYYMMDD format is used.",
|
|
7
8
|
BeneficiaryName: "Beneficiary name is required.",
|
|
8
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* TODO: remove after release https://github.com/validatorjs/validator.js/pull/2659
|
|
12
|
+
*
|
|
13
|
+
* Validates date string in YYYYMMDD format.
|
|
14
|
+
*
|
|
15
|
+
* Uses validator.js library for semantic date validation by converting
|
|
16
|
+
* YYYYMMDD to YYYY-MM-DD format (ISO 8601) which validator.isDate supports.
|
|
17
|
+
*
|
|
18
|
+
* @param date - Date string to validate in YYYYMMDD format
|
|
19
|
+
* @returns true if valid YYYYMMDD date, false otherwise
|
|
20
|
+
*/
|
|
21
|
+
function isValidYYYYMMDD(date) {
|
|
22
|
+
// Check format: exactly 8 digits
|
|
23
|
+
if (!/^\d{8}$/.test(date)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
// Convert YYYYMMDD to YYYY-MM-DD for validator.js
|
|
27
|
+
const year = date.substring(0, 4);
|
|
28
|
+
const month = date.substring(4, 6);
|
|
29
|
+
const day = date.substring(6, 8);
|
|
30
|
+
const isoFormat = `${year}-${month}-${day}`;
|
|
31
|
+
// Use validator.js to check if it's a valid calendar date
|
|
32
|
+
// This handles leap years, month boundaries, and all edge cases
|
|
33
|
+
return validator.isDate(isoFormat, {
|
|
34
|
+
format: "YYYY-MM-DD",
|
|
35
|
+
strictMode: true,
|
|
36
|
+
delimiters: ["-"],
|
|
37
|
+
});
|
|
38
|
+
}
|
|
9
39
|
/**
|
|
10
40
|
* This error will be thrown in case of a validation issue. It provides message with error description and specific path to issue in dataModel object.
|
|
11
41
|
*/
|
|
@@ -37,7 +67,7 @@ export function validateBankAccount(bankAccount, path) {
|
|
|
37
67
|
/**
|
|
38
68
|
* validate simple payment fields:
|
|
39
69
|
* - currencyCode (ISO 4217)
|
|
40
|
-
* - paymentDueDate (
|
|
70
|
+
* - paymentDueDate (YYYYMMDD format per v1.2 specification)
|
|
41
71
|
* - bankAccounts
|
|
42
72
|
*
|
|
43
73
|
* @see validateBankAccount
|
|
@@ -49,9 +79,15 @@ export function validateSimplePayment(simplePayment, path) {
|
|
|
49
79
|
if (simplePayment.currencyCode && !validator.isISO4217(simplePayment.currencyCode)) {
|
|
50
80
|
throw new ValidationError(ErrorMessages.CurrencyCode, `${path}.currencyCode`);
|
|
51
81
|
}
|
|
52
|
-
if (simplePayment.paymentDueDate
|
|
82
|
+
if (simplePayment.paymentDueDate
|
|
83
|
+
&& !isValidYYYYMMDD(simplePayment.paymentDueDate)) {
|
|
53
84
|
throw new ValidationError(ErrorMessages.Date, `${path}.paymentDueDate`);
|
|
54
85
|
}
|
|
86
|
+
if (simplePayment.type === PaymentOptions.StandingOrder
|
|
87
|
+
&& simplePayment.lastDate
|
|
88
|
+
&& !isValidYYYYMMDD(simplePayment.lastDate)) {
|
|
89
|
+
throw new ValidationError(ErrorMessages.Date, `${path}.lastDate`);
|
|
90
|
+
}
|
|
55
91
|
if (!simplePayment.beneficiary?.name) {
|
|
56
92
|
throw new ValidationError(ErrorMessages.BeneficiaryName, `${path}.beneficiary.name`);
|
|
57
93
|
}
|
package/package.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"name": "bysquare",
|
|
3
3
|
"description": "It's a national standard for payment QR codes adopted by Slovak Banking Association (SBA)",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.2.0",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"funding": "https://github.com/sponsors/xseman",
|
|
8
|
-
"homepage": "https://github.com/xseman/bysquare#readme",
|
|
8
|
+
"homepage": "https://github.com/xseman/bysquare/tree/master/typescript#readme",
|
|
9
9
|
"author": "Filip Seman <filip.seman@pm.me>",
|
|
10
10
|
"keywords": [
|
|
11
11
|
"pay by square",
|
package/src/cli.ts
CHANGED
|
@@ -3,135 +3,215 @@
|
|
|
3
3
|
import {
|
|
4
4
|
existsSync,
|
|
5
5
|
readFileSync,
|
|
6
|
+
statSync,
|
|
6
7
|
} from "node:fs";
|
|
7
8
|
import process from "node:process";
|
|
8
9
|
import { parseArgs } from "node:util";
|
|
9
10
|
|
|
10
11
|
import { decode } from "./decode.js";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
12
|
+
import {
|
|
13
|
+
encode,
|
|
14
|
+
type EncodeOptions,
|
|
15
|
+
} from "./encode.js";
|
|
16
|
+
import { Version } from "./types.js";
|
|
17
|
+
|
|
18
|
+
const version = "3.1.0";
|
|
19
|
+
|
|
20
|
+
const usage = `bysquare - Slovak PAY by square QR payment standard
|
|
21
|
+
|
|
22
|
+
USAGE:
|
|
23
|
+
bysquare encode [OPTIONS] <input.json>
|
|
24
|
+
bysquare decode <qr-string>
|
|
25
|
+
bysquare version
|
|
26
|
+
|
|
27
|
+
COMMANDS:
|
|
28
|
+
encode Encode JSON payment data to BySquare QR string
|
|
29
|
+
decode Decode BySquare QR string to JSON payment data
|
|
30
|
+
version Print version information
|
|
31
|
+
|
|
32
|
+
ENCODE OPTIONS:
|
|
33
|
+
-D, --no-deburr Keep diacritics (deburr enabled by default)
|
|
34
|
+
-V, --no-validate Skip validation (validation enabled by default)
|
|
35
|
+
-s, --spec-version VER Specification version: 1.0.0, 1.1.0, 1.2.0 (default: 1.2.0)
|
|
36
|
+
|
|
37
|
+
EXAMPLES:
|
|
38
|
+
# Encode with defaults (deburr=true, validate=true, version=1.2.0)
|
|
39
|
+
$ bysquare encode payment.json
|
|
40
|
+
|
|
41
|
+
# Encode with specific options
|
|
42
|
+
$ bysquare encode --no-deburr payment.json
|
|
43
|
+
$ bysquare encode --spec-version 1.1.0 payment.json
|
|
44
|
+
$ bysquare encode -s 1.0.0 --no-validate payment.json
|
|
45
|
+
|
|
46
|
+
# Encode from stdin
|
|
47
|
+
$ echo '{"payments":[...]}' | bysquare encode -
|
|
48
|
+
|
|
49
|
+
# Encode multiple files (including JSONL)
|
|
50
|
+
$ bysquare encode file1.json file2.jsonl
|
|
51
|
+
|
|
52
|
+
# Decode QR string
|
|
53
|
+
$ bysquare decode "00D80..."
|
|
54
|
+
|
|
55
|
+
# Decode from file
|
|
56
|
+
$ bysquare decode qr.txt
|
|
57
|
+
|
|
58
|
+
For more information, visit: https://github.com/xseman/bysquare
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
function errorMessage(error: unknown): string {
|
|
62
|
+
return error instanceof Error
|
|
63
|
+
? error.message
|
|
64
|
+
: String(error);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function readStdin(): Promise<string> {
|
|
68
|
+
const chunks: Buffer[] = [];
|
|
69
|
+
for await (const chunk of process.stdin) {
|
|
70
|
+
chunks.push(chunk);
|
|
71
|
+
}
|
|
72
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function readInput(path: string): Promise<string> {
|
|
76
|
+
if (path === "-") {
|
|
77
|
+
return readStdin();
|
|
78
|
+
}
|
|
79
|
+
if (!existsSync(path)) {
|
|
80
|
+
throw new Error(`file ${path} doesn't exist`);
|
|
81
|
+
}
|
|
82
|
+
return readFileSync(path, "utf8");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function cmdEncode(args: string[]): Promise<void> {
|
|
86
|
+
const parsed = parseArgs({
|
|
87
|
+
args,
|
|
88
|
+
allowPositionals: true,
|
|
89
|
+
options: {
|
|
90
|
+
"no-deburr": {
|
|
91
|
+
type: "boolean",
|
|
92
|
+
short: "D",
|
|
93
|
+
},
|
|
94
|
+
"no-validate": {
|
|
95
|
+
type: "boolean",
|
|
96
|
+
short: "V",
|
|
97
|
+
},
|
|
98
|
+
"spec-version": {
|
|
99
|
+
type: "string",
|
|
100
|
+
short: "s",
|
|
101
|
+
default: "1.2.0",
|
|
102
|
+
},
|
|
35
103
|
},
|
|
36
|
-
}
|
|
37
|
-
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (parsed.positionals.length === 0) {
|
|
107
|
+
console.error("Error: missing input file argument");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const versionStr = parsed.values["spec-version"] as keyof typeof Version;
|
|
112
|
+
if (!(versionStr in Version)) {
|
|
113
|
+
console.error("Error: unsupported spec version:", parsed.values["spec-version"]);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const encodeOpts = {
|
|
118
|
+
validate: !parsed.values["no-validate"],
|
|
119
|
+
deburr: !parsed.values["no-deburr"],
|
|
120
|
+
version: Version[versionStr],
|
|
121
|
+
} satisfies EncodeOptions;
|
|
38
122
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
123
|
+
for (const inputFile of parsed.positionals) {
|
|
124
|
+
let input: string;
|
|
125
|
+
try {
|
|
126
|
+
input = await readInput(inputFile);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error("Error:", errorMessage(error));
|
|
43
129
|
process.exit(1);
|
|
44
130
|
}
|
|
45
131
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
132
|
+
if (inputFile.endsWith(".jsonl")) {
|
|
133
|
+
for (const line of input.split("\n")) {
|
|
134
|
+
if (!line.trim()) continue;
|
|
135
|
+
encodeAndPrint(line, encodeOpts);
|
|
50
136
|
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
51
139
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
) {
|
|
56
|
-
console.error(`Unsupported file format for ${file}`);
|
|
57
|
-
process.exit(1);
|
|
58
|
-
}
|
|
140
|
+
encodeAndPrint(input.trim(), encodeOpts);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
59
143
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
144
|
+
function encodeAndPrint(jsonStr: string, opts: EncodeOptions): void {
|
|
145
|
+
try {
|
|
146
|
+
const data = JSON.parse(jsonStr);
|
|
147
|
+
const result = encode(data, opts);
|
|
148
|
+
console.log(result);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error("Error:", errorMessage(error));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
65
154
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
155
|
+
async function cmdDecode(args: string[]): Promise<void> {
|
|
156
|
+
if (args.length === 0) {
|
|
157
|
+
console.error("Error: missing QR string argument");
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
70
160
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}));
|
|
76
|
-
}
|
|
161
|
+
const qrInput = args[0];
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
let qr: string;
|
|
77
165
|
|
|
78
|
-
|
|
166
|
+
if (qrInput === "-") {
|
|
167
|
+
qr = await readStdin();
|
|
168
|
+
} else if (existsSync(qrInput) && statSync(qrInput).isFile()) {
|
|
169
|
+
qr = readFileSync(qrInput, "utf8");
|
|
170
|
+
} else {
|
|
171
|
+
qr = qrInput;
|
|
79
172
|
}
|
|
173
|
+
|
|
174
|
+
const model = decode(qr.trim());
|
|
175
|
+
console.log(JSON.stringify(model, null, 2));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error("Error:", errorMessage(error));
|
|
178
|
+
process.exit(1);
|
|
80
179
|
}
|
|
180
|
+
}
|
|
81
181
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
console.
|
|
85
|
-
process.exit(
|
|
182
|
+
async function main(): Promise<void> {
|
|
183
|
+
if (process.argv.length < 3) {
|
|
184
|
+
console.error(usage);
|
|
185
|
+
process.exit(1);
|
|
86
186
|
}
|
|
87
187
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
"",
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"",
|
|
112
|
-
" -v, --validate",
|
|
113
|
-
" Validate JSON data from one or more files before encoding.",
|
|
114
|
-
"",
|
|
115
|
-
" -b, --deburr",
|
|
116
|
-
" Deburr JSON data from one or more files before encoding.",
|
|
117
|
-
"",
|
|
118
|
-
" -h, --help",
|
|
119
|
-
" Display the help message and exit.",
|
|
120
|
-
"",
|
|
121
|
-
"USAGE",
|
|
122
|
-
" Encoding JSON data from one or more files",
|
|
123
|
-
"",
|
|
124
|
-
` ${process.argv[1]} --encode file1.json file2.json ...`,
|
|
125
|
-
" The file1.json, file2.json, ... arguments should be the paths to the JSON or JSONL",
|
|
126
|
-
" files you want to encode. The tool will read each file, generate a QR code representing",
|
|
127
|
-
" the JSON data, and print them.",
|
|
128
|
-
"",
|
|
129
|
-
" Decoding a QR code string",
|
|
130
|
-
"",
|
|
131
|
-
` ${process.argv[1]} --decode <qrstring>`,
|
|
132
|
-
" Replace qrstring with the QR code string you want to decode.",
|
|
133
|
-
" The program will parse the QR code string and print the resulting JSON data.",
|
|
134
|
-
"",
|
|
135
|
-
].join("\n"));
|
|
188
|
+
const command = process.argv[2];
|
|
189
|
+
|
|
190
|
+
switch (command) {
|
|
191
|
+
case "encode":
|
|
192
|
+
await cmdEncode(process.argv.slice(3));
|
|
193
|
+
break;
|
|
194
|
+
case "decode":
|
|
195
|
+
await cmdDecode(process.argv.slice(3));
|
|
196
|
+
break;
|
|
197
|
+
case "version":
|
|
198
|
+
case "-v":
|
|
199
|
+
case "--version":
|
|
200
|
+
console.log(`bysquare version ${version}`);
|
|
201
|
+
break;
|
|
202
|
+
case "help":
|
|
203
|
+
case "-h":
|
|
204
|
+
case "--help":
|
|
205
|
+
console.log(usage);
|
|
206
|
+
break;
|
|
207
|
+
default:
|
|
208
|
+
console.error("Unknown command:", command);
|
|
209
|
+
console.error(usage);
|
|
210
|
+
process.exit(1);
|
|
136
211
|
}
|
|
137
212
|
}
|
|
213
|
+
|
|
214
|
+
main().catch((error) => {
|
|
215
|
+
console.error("Fatal error:", error);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
});
|
package/src/decode.ts
CHANGED
|
@@ -51,28 +51,6 @@ function decodeString(value: string | undefined): string | undefined {
|
|
|
51
51
|
return value?.length ? value : undefined;
|
|
52
52
|
}
|
|
53
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
54
|
/**
|
|
77
55
|
* Parse a tab-separated intermediate format into DataModel.
|
|
78
56
|
*
|
|
@@ -151,7 +129,7 @@ export function deserialize(qr: string): DataModel {
|
|
|
151
129
|
type: Number(paymentOptions),
|
|
152
130
|
currencyCode: currency ?? CurrencyCode.EUR,
|
|
153
131
|
amount: Number(ammount),
|
|
154
|
-
paymentDueDate:
|
|
132
|
+
paymentDueDate: dueDate || undefined,
|
|
155
133
|
variableSymbol: variableSymbol || undefined,
|
|
156
134
|
constantSymbol: constantSymbol || undefined,
|
|
157
135
|
specificSymbol: specificSymbol || undefined,
|
package/src/encode.ts
CHANGED
|
@@ -11,24 +11,6 @@ import {
|
|
|
11
11
|
} from "./types.js";
|
|
12
12
|
import { validateDataModel } from "./validations.js";
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* Converts date from ISO 8601 format (YYYY-MM-DD) to YYYYMMDD format
|
|
16
|
-
* per Pay by Square specification section 3.7.
|
|
17
|
-
*
|
|
18
|
-
* Note: This conversion is only used for paymentDueDate per specification.
|
|
19
|
-
* lastDate expects YYYYMMDD format directly.
|
|
20
|
-
*
|
|
21
|
-
* @param input - Date in ISO 8601 format (YYYY-MM-DD)
|
|
22
|
-
* @returns Date in YYYYMMDD format | undefined
|
|
23
|
-
*/
|
|
24
|
-
function serializeDate(input?: string): string | undefined {
|
|
25
|
-
if (!input) {
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return input.split("-").join("");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
14
|
export const EncodeErrorMessage = {
|
|
33
15
|
/**
|
|
34
16
|
* @description - find invalid value in extensions
|
|
@@ -248,7 +230,7 @@ export function serialize(data: DataModel): string {
|
|
|
248
230
|
serialized.push(p.type.toString());
|
|
249
231
|
serialized.push(p.amount?.toString());
|
|
250
232
|
serialized.push(p.currencyCode);
|
|
251
|
-
serialized.push(
|
|
233
|
+
serialized.push(p.paymentDueDate);
|
|
252
234
|
serialized.push(p.variableSymbol);
|
|
253
235
|
serialized.push(p.constantSymbol);
|
|
254
236
|
serialized.push(p.specificSymbol);
|
|
@@ -332,7 +314,7 @@ export function removeDiacritics(model: DataModel): void {
|
|
|
332
314
|
}
|
|
333
315
|
}
|
|
334
316
|
|
|
335
|
-
type
|
|
317
|
+
export type EncodeOptions = {
|
|
336
318
|
/**
|
|
337
319
|
* Many banking apps do not support diacritics, which results in errors when
|
|
338
320
|
* serializing data from QR codes.
|
|
@@ -395,7 +377,7 @@ type Options = {
|
|
|
395
377
|
*/
|
|
396
378
|
export function encode(
|
|
397
379
|
model: DataModel,
|
|
398
|
-
options:
|
|
380
|
+
options: EncodeOptions = { deburr: true, validate: true },
|
|
399
381
|
): string {
|
|
400
382
|
if (options.deburr) {
|
|
401
383
|
removeDiacritics(model);
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
export { decodeOptions, encodeOptions } from "./classifier.js";
|
|
8
8
|
export { decode } from "./decode.js";
|
|
9
|
-
export { encode } from "./encode.js";
|
|
9
|
+
export { encode, type EncodeOptions } from "./encode.js";
|
|
10
10
|
export { validateDataModel } from "./validations.js";
|
|
11
11
|
|
|
12
12
|
export * from "./types.js";
|
package/src/types.ts
CHANGED
|
@@ -259,14 +259,13 @@ export type SimplePayment = {
|
|
|
259
259
|
currencyCode: string | keyof typeof CurrencyCode;
|
|
260
260
|
|
|
261
261
|
/**
|
|
262
|
-
* Payment due date.
|
|
262
|
+
* Payment due date in YYYYMMDD format per v1.2 specification section 3.7.
|
|
263
263
|
*
|
|
264
264
|
* For standing orders, this indicates the first payment date.
|
|
265
|
-
* The date will be converted to YYYYMMDD format during encoding per specification section 3.7.
|
|
266
265
|
*
|
|
267
266
|
* @format date
|
|
268
|
-
* @example "
|
|
269
|
-
* @pattern \d{
|
|
267
|
+
* @example "20241231"
|
|
268
|
+
* @pattern \d{8}
|
|
270
269
|
*/
|
|
271
270
|
paymentDueDate?: string;
|
|
272
271
|
|
package/src/validations.ts
CHANGED
|
@@ -3,17 +3,50 @@ import validator from "validator";
|
|
|
3
3
|
import {
|
|
4
4
|
BankAccount,
|
|
5
5
|
DataModel,
|
|
6
|
-
|
|
6
|
+
type Payment,
|
|
7
|
+
PaymentOptions,
|
|
7
8
|
} from "./types.js";
|
|
8
9
|
|
|
9
10
|
const ErrorMessages = {
|
|
10
11
|
IBAN: "Invalid IBAN. Make sure ISO 13616 format is used.",
|
|
11
12
|
BIC: "Invalid BIC. Make sure ISO 9362 format is used.",
|
|
12
13
|
CurrencyCode: "Invalid currency code. Make sure ISO 4217 format is used.",
|
|
13
|
-
Date: "Invalid date. Make sure
|
|
14
|
+
Date: "Invalid date. Make sure YYYYMMDD format is used.",
|
|
14
15
|
BeneficiaryName: "Beneficiary name is required.",
|
|
15
16
|
} as const;
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* TODO: remove after release https://github.com/validatorjs/validator.js/pull/2659
|
|
20
|
+
*
|
|
21
|
+
* Validates date string in YYYYMMDD format.
|
|
22
|
+
*
|
|
23
|
+
* Uses validator.js library for semantic date validation by converting
|
|
24
|
+
* YYYYMMDD to YYYY-MM-DD format (ISO 8601) which validator.isDate supports.
|
|
25
|
+
*
|
|
26
|
+
* @param date - Date string to validate in YYYYMMDD format
|
|
27
|
+
* @returns true if valid YYYYMMDD date, false otherwise
|
|
28
|
+
*/
|
|
29
|
+
function isValidYYYYMMDD(date: string): boolean {
|
|
30
|
+
// Check format: exactly 8 digits
|
|
31
|
+
if (!/^\d{8}$/.test(date)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Convert YYYYMMDD to YYYY-MM-DD for validator.js
|
|
36
|
+
const year = date.substring(0, 4);
|
|
37
|
+
const month = date.substring(4, 6);
|
|
38
|
+
const day = date.substring(6, 8);
|
|
39
|
+
const isoFormat = `${year}-${month}-${day}`;
|
|
40
|
+
|
|
41
|
+
// Use validator.js to check if it's a valid calendar date
|
|
42
|
+
// This handles leap years, month boundaries, and all edge cases
|
|
43
|
+
return validator.isDate(isoFormat, {
|
|
44
|
+
format: "YYYY-MM-DD",
|
|
45
|
+
strictMode: true,
|
|
46
|
+
delimiters: ["-"],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
17
50
|
/**
|
|
18
51
|
* This error will be thrown in case of a validation issue. It provides message with error description and specific path to issue in dataModel object.
|
|
19
52
|
*/
|
|
@@ -55,13 +88,13 @@ export function validateBankAccount(
|
|
|
55
88
|
/**
|
|
56
89
|
* validate simple payment fields:
|
|
57
90
|
* - currencyCode (ISO 4217)
|
|
58
|
-
* - paymentDueDate (
|
|
91
|
+
* - paymentDueDate (YYYYMMDD format per v1.2 specification)
|
|
59
92
|
* - bankAccounts
|
|
60
93
|
*
|
|
61
94
|
* @see validateBankAccount
|
|
62
95
|
*/
|
|
63
96
|
export function validateSimplePayment(
|
|
64
|
-
simplePayment:
|
|
97
|
+
simplePayment: Payment,
|
|
65
98
|
path: string,
|
|
66
99
|
): void {
|
|
67
100
|
for (const [index, bankAccount] of simplePayment.bankAccounts.entries()) {
|
|
@@ -75,13 +108,27 @@ export function validateSimplePayment(
|
|
|
75
108
|
);
|
|
76
109
|
}
|
|
77
110
|
|
|
78
|
-
if (
|
|
111
|
+
if (
|
|
112
|
+
simplePayment.paymentDueDate
|
|
113
|
+
&& !isValidYYYYMMDD(simplePayment.paymentDueDate)
|
|
114
|
+
) {
|
|
79
115
|
throw new ValidationError(
|
|
80
116
|
ErrorMessages.Date,
|
|
81
117
|
`${path}.paymentDueDate`,
|
|
82
118
|
);
|
|
83
119
|
}
|
|
84
120
|
|
|
121
|
+
if (
|
|
122
|
+
simplePayment.type === PaymentOptions.StandingOrder
|
|
123
|
+
&& simplePayment.lastDate
|
|
124
|
+
&& !isValidYYYYMMDD(simplePayment.lastDate)
|
|
125
|
+
) {
|
|
126
|
+
throw new ValidationError(
|
|
127
|
+
ErrorMessages.Date,
|
|
128
|
+
`${path}.lastDate`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
85
132
|
if (!simplePayment.beneficiary?.name) {
|
|
86
133
|
throw new ValidationError(
|
|
87
134
|
ErrorMessages.BeneficiaryName,
|