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 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
- <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>
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
- const args = parseArgs({
8
- allowPositionals: true,
9
- options: {
10
- decode: {
11
- type: "string",
12
- short: "d",
13
- },
14
- encode: {
15
- type: "boolean",
16
- short: "e",
17
- },
18
- validate: {
19
- type: "boolean",
20
- short: "v",
21
- },
22
- deburr: {
23
- type: "boolean",
24
- short: "b",
25
- },
26
- help: {
27
- type: "boolean",
28
- short: "h",
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
- if (process.stdin.isTTY) {
33
- if (args.values.encode) {
34
- if (args.positionals.length === 0) {
35
- console.error("No files provided for encoding.");
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
- for (const file of args.positionals) {
39
- if (existsSync(file) === false) {
40
- console.error(`File ${file} doesn't exist`);
41
- process.exit(1);
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
- const data = readFileSync(file, "utf8");
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
- if (args.values.decode) {
68
- const qrstring = args.values.decode;
69
- console.log(JSON.stringify(decode(qrstring), null, 4));
70
- process.exit(0);
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
- if (args.values.help
73
- || Object.keys(args.values).length === 0) {
74
- console.log([
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: deserializeDate(dueDate),
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 Options = {
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?: Options): string;
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(serializeDate(p.paymentDueDate));
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 "2024-12-31"
206
- * @pattern \d{4}-\d{2}-\d{2}
204
+ * @example "20241231"
205
+ * @pattern \d{8}
207
206
  */
208
207
  paymentDueDate?: string;
209
208
  /**
@@ -1,4 +1,4 @@
1
- import { BankAccount, DataModel, SimplePayment } from "./types.js";
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 (ISO 8601)
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: SimplePayment, path: string): void;
27
+ export declare function validateSimplePayment(simplePayment: Payment, path: string): void;
28
28
  /**
29
29
  * Validate `payments` field of dataModel.
30
30
  *
@@ -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 ISO 8601 format is used.",
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 (ISO 8601)
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 && !validator.isDate(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.1.0",
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 { encode } from "./encode.js";
12
-
13
- const args = parseArgs({
14
- allowPositionals: true,
15
- options: {
16
- decode: {
17
- type: "string",
18
- short: "d",
19
- },
20
- encode: {
21
- type: "boolean",
22
- short: "e",
23
- },
24
- validate: {
25
- type: "boolean",
26
- short: "v",
27
- },
28
- deburr: {
29
- type: "boolean",
30
- short: "b",
31
- },
32
- help: {
33
- type: "boolean",
34
- short: "h",
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
- if (process.stdin.isTTY) {
40
- if (args.values.encode) {
41
- if (args.positionals.length === 0) {
42
- console.error("No files provided for encoding.");
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
- for (const file of args.positionals) {
47
- if (existsSync(file) === false) {
48
- console.error(`File ${file} doesn't exist`);
49
- process.exit(1);
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
- if (
53
- file.endsWith(".json") === false
54
- && file.endsWith(".jsonl") === false
55
- ) {
56
- console.error(`Unsupported file format for ${file}`);
57
- process.exit(1);
58
- }
140
+ encodeAndPrint(input.trim(), encodeOpts);
141
+ }
142
+ }
59
143
 
60
- const data = readFileSync(file, "utf8");
61
- if (file.endsWith(".jsonl")) {
62
- const lines = data.split("\n");
63
- for (const line of lines) {
64
- if (!line) continue;
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
- const json = JSON.parse(line);
67
- console.log(encode(json));
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
- if (file.endsWith(".json")) {
72
- console.log(encode(JSON.parse(data), {
73
- validate: Boolean(args.values.validate),
74
- deburr: Boolean(args.values.deburr),
75
- }));
76
- }
161
+ const qrInput = args[0];
162
+
163
+ try {
164
+ let qr: string;
77
165
 
78
- process.exit(0);
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
- if (args.values.decode) {
83
- const qrstring = args.values.decode;
84
- console.log(JSON.stringify(decode(qrstring), null, 4));
85
- process.exit(0);
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
- if (
89
- args.values.help
90
- || Object.keys(args.values).length === 0
91
- ) {
92
- console.log([
93
- "NAME",
94
- " bysquare - Simple Node.js library to generate and parse PAY bysquare standard",
95
- "",
96
- "SYNOPSIS",
97
- " bysquare [OPTIONS] [FILES...]",
98
- "",
99
- "DESCRIPTION",
100
- " bysquare is a command-line tool that provides a simple Node.js library to generate ",
101
- " and parse PAY bysquare standard. It offers functionality to encode JSON data into a ",
102
- " corresponding QR code and decode a QR code string to obtain the associated JSON data.",
103
- "",
104
- "OPTIONS",
105
- " -d, --decode <qrstring>",
106
- " Decode the specified QR code string and print the corresponding JSON data.",
107
- " The qrstring argument should be a valid QR code string.",
108
- "",
109
- " -e, --encode",
110
- " Encode JSON data from one or more files and print the corresponding QR code.",
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: deserializeDate(dueDate),
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(serializeDate(p.paymentDueDate));
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 Options = {
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: Options = { deburr: true, validate: true },
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 "2024-12-31"
269
- * @pattern \d{4}-\d{2}-\d{2}
267
+ * @example "20241231"
268
+ * @pattern \d{8}
270
269
  */
271
270
  paymentDueDate?: string;
272
271
 
@@ -3,17 +3,50 @@ import validator from "validator";
3
3
  import {
4
4
  BankAccount,
5
5
  DataModel,
6
- SimplePayment,
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 ISO 8601 format is used.",
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 (ISO 8601)
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: 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 (simplePayment.paymentDueDate && !validator.isDate(simplePayment.paymentDueDate)) {
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,