diff --git a/dist/example.d.ts b/dist/example.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/example.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/example.js b/dist/example.js new file mode 100644 index 0000000..601f55a --- /dev/null +++ b/dist/example.js @@ -0,0 +1,13 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var _1 = __importDefault(require(".")); +console.log((0, _1.default)({ + name: 'Red Cross of Belgium', + iban: 'BE72000000001616', + amount: 123.45, + unstructuredReference: 'Urgency fund', + information: 'Sample QR code' +})); diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..41d483c --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,12 @@ +type QrCodeData = { + name: string; + iban: string; + bic?: string; + amount?: number; + purposeCode?: string; + structuredReference?: string; + unstructuredReference?: string; + information?: string; +}; +declare const generateQrCode: (data: QrCodeData) => string; +export = generateQrCode; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..312cbdd --- /dev/null +++ b/dist/index.js @@ -0,0 +1,86 @@ +"use strict"; +var iban_1 = require("iban"); +var SERVICE_TAG = 'BCD'; +var VERSION = '002'; +var CHARACTER_SET = 1; +var IDENTIFICATION_CODE = 'SCT'; +var assertNonEmptyString = function (val, name) { + if (typeof val !== 'string' || !val) { + throw new Error(name + ' must be a non-empty string.'); + } +}; +var generateQrCode = function (data) { + if (!data) + throw new Error('data must be an object.'); + // > AT-21 Name of the Beneficiary + assertNonEmptyString(data.name, 'data.name'); + if (data.name.length > 70) + throw new Error('data.name must have <=70 characters'); + // > AT-23 BIC of the Beneficiary Bank + if (data.bic) { + assertNonEmptyString(data.bic, 'data.bic'); + if (data.bic.length > 11) + throw new Error('data.bic must have <=11 characters'); + // todo: validate more? + } + // > AT-20 Account number of the Beneficiary + // > Only IBAN is allowed. + assertNonEmptyString(data.iban, 'data.iban'); + if (!(0, iban_1.isValid)(data.iban)) { + throw new Error('data.iban must be a valid iban code.'); + } + // > AT-04 Amount of the Credit Transfer in Euro + // > Amount must be 0.01 or more and 999999999.99 or less + if (data.amount !== null) { + if (typeof data.amount !== 'number') + throw new Error('data.amount must be a number or null.'); + if (data.amount < 0.01 || data.amount > 999999999.99) { + throw new Error('data.amount must be >=0.01 and <=999999999.99.'); + } + } + // > AT-44 Purpose of the Credit Transfer + if (data.purposeCode) { + assertNonEmptyString(data.purposeCode, 'data.purposeCode'); + if (data.purposeCode.length > 4) + throw new Error('data.purposeCode must have <=4 characters'); + // todo: validate against AT-44 + } + // > AT-05 Remittance Information (Structured) + // > Creditor Reference (ISO 11649 RF Creditor Reference may be used) + if (data.structuredReference) { + assertNonEmptyString(data.structuredReference, 'data.structuredReference'); + if (data.structuredReference.length > 35) + throw new Error('data.structuredReference must have <=35 characters'); + // todo: validate against AT-05 + } + // > AT-05 Remittance Information (Unstructured) + if (data.unstructuredReference) { + assertNonEmptyString(data.unstructuredReference, 'data.unstructuredReference'); + if (data.unstructuredReference.length > 140) + throw new Error('data.unstructuredReference must have <=140 characters'); + } + if ('structuredReference' in data && 'unstructuredReference' in data) { + throw new Error('Use either data.structuredReference or data.unstructuredReference.'); + } + // > Beneficiary to originator information + if (data.information) { + assertNonEmptyString(data.information, 'data.information'); + if (data.information.length > 70) + throw new Error('data.information must have <=70 characters'); + } + return [ + SERVICE_TAG, + VERSION, + CHARACTER_SET, + IDENTIFICATION_CODE, + data.bic, + data.name, + (0, iban_1.electronicFormat)(data.iban), + data.amount === null ? '' : 'EUR' + data.amount.toFixed(2), + data.purposeCode || '', + data.structuredReference || '', + data.unstructuredReference || '', + data.information || '', + ].join('\n'); +}; +module.exports = generateQrCode; diff --git a/dist/test.d.ts b/dist/test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/test.js b/dist/test.js new file mode 100644 index 0000000..a6e155f --- /dev/null +++ b/dist/test.js @@ -0,0 +1,46 @@ +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var assert_1 = require("assert"); +var _1 = __importDefault(require(".")); +var ex1 = { + name: 'Red Cross of Belgium', + iban: 'BE72000000001616', + bic: 'BPOTBEB1', + amount: 123.45, + reference: 'Urgency fund', + purposeCode: 'abc', + structuredReference: '123', + information: 'foo bar' +}; +(0, assert_1.strictEqual)((0, _1.default)(ex1), "BCD\n002\n1\nSCT\nBPOTBEB1\nRed Cross of Belgium\nBE72000000001616\nEUR123.45\nabc\n123\n\nfoo bar"); +// missing data.iban +{ + var ex2_1 = __assign({}, ex1); + delete ex2_1.iban; + (0, assert_1.throws)(function () { + (0, _1.default)(ex2_1); + }, 'throws with missing data.iban'); +} +// invalid data.iban +{ + (0, assert_1.throws)(function () { + (0, _1.default)(__assign(__assign({}, ex1), { iban: 'BE00000000000000' })); + }, 'throws with invalid data.iban'); +} +// omitted amount +(0, assert_1.strictEqual)((0, _1.default)(__assign(__assign({}, ex1), { amount: null })), "BCD\n002\n1\nSCT\nBPOTBEB1\nRed Cross of Belgium\nBE72000000001616\n\nabc\n123\n\nfoo bar"); +console.info('seems to work ✓'); diff --git a/example.js b/example.js index afa10d8..1d1f225 100644 --- a/example.js +++ b/example.js @@ -1,6 +1,6 @@ 'use strict' -const generateQrCode = require('.') +const generateQrCode = require('sepa-payment-qr-code') console.log(generateQrCode({ name: 'Red Cross of Belgium', diff --git a/example.ts b/example.ts new file mode 100644 index 0000000..b141331 --- /dev/null +++ b/example.ts @@ -0,0 +1,9 @@ +import generateQrCode from '.' + +console.log(generateQrCode({ + name: 'Red Cross of Belgium', + iban: 'BE72000000001616', + amount: 123.45, + unstructuredReference: 'Urgency fund', + information: 'Sample QR code' +})) diff --git a/index.js b/index.js deleted file mode 100644 index 727e025..0000000 --- a/index.js +++ /dev/null @@ -1,91 +0,0 @@ -'use strict' - -const {isValid: isValidIBAN, electronicFormat: serializeIBAN} = require('iban') - -const SERVICE_TAG = 'BCD' -const VERSION = '002' -const CHARACTER_SET = 1 -const IDENTIFICATION_CODE = 'SCT' - -const assertNonEmptyString = (val, name) => { - if ('string' !== typeof val || !val) { - throw new Error(name + ' must be a non-empty string.') - } -} - -const generateQrCode = data => { - if (!data) throw new Error('data must be an object.') - - // > AT-21 Name of the Beneficiary - assertNonEmptyString(data.name, 'data.name') - if (data.name.length > 70) throw new Error('data.name must have <=70 characters') - - // > AT-23 BIC of the Beneficiary Bank - if ('bic' in data) { - assertNonEmptyString(data.bic, 'data.bic') - if (data.bic.length > 11) throw new Error('data.bic must have <=11 characters') - // todo: validate more? - } - - // > AT-20 Account number of the Beneficiary - // > Only IBAN is allowed. - assertNonEmptyString(data.iban, 'data.iban') - if (!isValidIBAN(data.iban)) { - throw new Error('data.iban must be a valid iban code.') - } - - // > AT-04 Amount of the Credit Transfer in Euro - // > Amount must be 0.01 or more and 999999999.99 or less - if (data.amount !== null) { - if ('number' !== typeof data.amount) throw new Error('data.amount must be a number or null.') - if (data.amount < 0.01 || data.amount > 999999999.99) { - throw new Error('data.amount must be >=0.01 and <=999999999.99.') - } - } - - // > AT-44 Purpose of the Credit Transfer - if ('purposeCode' in data) { - assertNonEmptyString(data.purposeCode, 'data.purposeCode') - if (data.purposeCode.length > 4) throw new Error('data.purposeCode must have <=4 characters') - // todo: validate against AT-44 - } - - // > AT-05 Remittance Information (Structured) - // > Creditor Reference (ISO 11649 RF Creditor Reference may be used) - if ('structuredReference' in data) { - assertNonEmptyString(data.structuredReference, 'data.structuredReference') - if (data.structuredReference.length > 35) throw new Error('data.structuredReference must have <=35 characters') - // todo: validate against AT-05 - } - // > AT-05 Remittance Information (Unstructured) - if ('unstructuredReference' in data) { - assertNonEmptyString(data.unstructuredReference, 'data.unstructuredReference') - if (data.unstructuredReference.length > 140) throw new Error('data.unstructuredReference must have <=140 characters') - } - if (('structuredReference' in data) && ('unstructuredReference' in data)) { - throw new Error('Use either data.structuredReference or data.unstructuredReference.') - } - - // > Beneficiary to originator information - if ('information' in data) { - assertNonEmptyString(data.information, 'data.information') - if (data.information.length > 70) throw new Error('data.information must have <=70 characters') - } - - return [ - SERVICE_TAG, - VERSION, - CHARACTER_SET, - IDENTIFICATION_CODE, - data.bic, - data.name, - serializeIBAN(data.iban), - data.amount === null ? '' : 'EUR' + data.amount.toFixed(2), - data.purposeCode || '', - data.structuredReference || '', - data.unstructuredReference || '', - data.information || '' - ].join('\n') -} - -module.exports = generateQrCode diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..8d99539 --- /dev/null +++ b/index.ts @@ -0,0 +1,102 @@ +import { isValid, electronicFormat } from 'iban' + +const SERVICE_TAG = 'BCD' +const VERSION = '002' +const CHARACTER_SET = 1 +const IDENTIFICATION_CODE = 'SCT' + +const assertNonEmptyString = (val: unknown, name: string) => { + if (typeof val !== 'string' || !val) { + throw new Error(name + ' must be a non-empty string.') + } +} + +type QrCodeData = { + name: string + iban: string + bic?: string + amount?: number + purposeCode?: string + structuredReference?: string + unstructuredReference?: string + information?: string +} + +const generateQrCode = (data: QrCodeData) => { + if (!data) throw new Error('data must be an object.') + + // > AT-21 Name of the Beneficiary + assertNonEmptyString(data.name, 'data.name') + if (data.name.length > 70) throw new Error('data.name must have <=70 characters') + + // > AT-23 BIC of the Beneficiary Bank + if (data.bic) { + assertNonEmptyString(data.bic, 'data.bic') + if (data.bic.length > 11) throw new Error('data.bic must have <=11 characters') + // todo: validate more? + } + + // > AT-20 Account number of the Beneficiary + // > Only IBAN is allowed. + assertNonEmptyString(data.iban, 'data.iban') + if (!isValid(data.iban)) { + throw new Error('data.iban must be a valid iban code.') + } + + // > AT-04 Amount of the Credit Transfer in Euro + // > Amount must be 0.01 or more and 999999999.99 or less + if (data.amount !== null) { + if (typeof data.amount !== 'number') throw new Error('data.amount must be a number or null.') + if (data.amount < 0.01 || data.amount > 999999999.99) { + throw new Error('data.amount must be >=0.01 and <=999999999.99.') + } + } + + // > AT-44 Purpose of the Credit Transfer + if (data.purposeCode) { + assertNonEmptyString(data.purposeCode, 'data.purposeCode') + if (data.purposeCode.length > 4) throw new Error('data.purposeCode must have <=4 characters') + // todo: validate against AT-44 + } + + // > AT-05 Remittance Information (Structured) + // > Creditor Reference (ISO 11649 RF Creditor Reference may be used) + if (data.structuredReference) { + assertNonEmptyString(data.structuredReference, 'data.structuredReference') + if (data.structuredReference.length > 35) + throw new Error('data.structuredReference must have <=35 characters') + // todo: validate against AT-05 + } + // > AT-05 Remittance Information (Unstructured) + if (data.unstructuredReference) { + assertNonEmptyString(data.unstructuredReference, 'data.unstructuredReference') + if (data.unstructuredReference.length > 140) + throw new Error('data.unstructuredReference must have <=140 characters') + } + if ('structuredReference' in data && 'unstructuredReference' in data) { + throw new Error('Use either data.structuredReference or data.unstructuredReference.') + } + + // > Beneficiary to originator information + if (data.information) { + assertNonEmptyString(data.information, 'data.information') + if (data.information.length > 70) throw new Error('data.information must have <=70 characters') + } + + return [ + SERVICE_TAG, + VERSION, + CHARACTER_SET, + IDENTIFICATION_CODE, + data.bic, + data.name, + electronicFormat(data.iban), + data.amount === null ? '' : 'EUR' + data.amount.toFixed(2), + data.purposeCode || '', + data.structuredReference || '', + data.unstructuredReference || '', + data.information || '', + ].join('\n') +} + +export = generateQrCode diff --git a/package.json b/package.json index 987af4f..6a8b96f 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,11 @@ "name": "sepa-payment-qr-code", "description": "Generate a QR code to initiate a SEPA bank transfer.", "version": "2.0.2", - "main": "index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", "files": [ - "index.js", - "example.js" + "dist/index.js", + "dist/index.d.ts" ], "keywords": [ "payment", @@ -20,10 +21,14 @@ ], "author": "Jannis R ", "contributors": [ - "@moltam89 (https://github.com/moltam89)" + "@moltam89 (https://github.com/moltam89)", + "@roschaefer (https://github.com/roschaefer)" ], "homepage": "https://github.com/derhuerst/sepa-payment-qr-code", - "repository": "derhuerst/sepa-payment-qr-code", + "repository": { + "type": "git", + "url": "git+https://github.com/derhuerst/sepa-payment-qr-code.git" + }, "bugs": "https://github.com/derhuerst/sepa-payment-qr-code/issues", "license": "ISC", "engines": { @@ -33,11 +38,17 @@ "iban": "0.0.14" }, "devDependencies": { - "eslint": "^8.24.0" + "@types/assert": "^1.5.10", + "@types/iban": "^0.0.35", + "assert": "^2.1.0", + "eslint": "^8.24.0", + "typescript": "^5.6.2" }, "scripts": { + "build": "tsc", + "example": "node dist/example.js", "lint": "eslint .", - "test": "env NODE_ENV=dev node test.js", - "prepublishOnly": "npm run lint && npm test" + "test": "node dist/test.js", + "prepublishOnly": "npm run lint && npm run test && npm run build" } } diff --git a/test.js b/test.ts similarity index 79% rename from test.js rename to test.ts index 7840bf9..903033a 100644 --- a/test.js +++ b/test.ts @@ -1,7 +1,5 @@ -'use strict' - -const a = require('assert') -const generateQrCode = require('.') +import { strictEqual, throws } from 'assert' +import generateQrCode from '.' const ex1 = { name: 'Red Cross of Belgium', @@ -14,7 +12,7 @@ const ex1 = { information: 'foo bar' } -a.strictEqual(generateQrCode(ex1), `\ +strictEqual(generateQrCode(ex1), `\ BCD 002 1 @@ -32,14 +30,14 @@ foo bar`) { const ex2 = {...ex1} delete ex2.iban - a.throws(() => { + throws(() => { generateQrCode(ex2) }, 'throws with missing data.iban') } // invalid data.iban { - a.throws(() => { + throws(() => { generateQrCode({ ...ex1, iban: 'BE00000000000000', @@ -48,7 +46,7 @@ foo bar`) } // omitted amount -a.strictEqual(generateQrCode({ +strictEqual(generateQrCode({ ...ex1, amount: null, }), `\ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..16e572e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "declaration": true, + "esModuleInterop": true, + "outDir": "dist" + }, + "files": [ + "index.ts", + "example.ts", + "test.ts" + ] +}