telebirr-receipt 0.0.1

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.
Files changed (4) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +107 -0
  3. package/index.js +152 -0
  4. package/package.json +33 -0
package/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+ Copyright 2023 Yonathan Tesfaye
2
+ Email: yt040448@gmail.com
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # telebirr-receipt
2
+
3
+ ### What is telebirr-receipt
4
+
5
+ `telebirr-receipt` Is a small nodejs package to parse and verify
6
+ telebirr transaction receipt. Any telebirr transaction
7
+ receipt can be found through the url
8
+ `https://transactioninfo.ethiotelecom.et/receipt/{receipt_no}`
9
+ by replacing `{receipt_no}` with receipt number or id which is sent from telebirr
10
+ transaction SMS or the full receipt url is also sent via SMS.
11
+
12
+ ### How to install
13
+
14
+ ``
15
+ npm install telebirr-receipt
16
+ ``
17
+
18
+ ### Basic usage
19
+
20
+ #### Loading receipt from url
21
+
22
+ ```javascript
23
+ const {receipt, utils: {loadReceipt, parseFromHTML}} = require('telebirr-receipt')
24
+
25
+ // use it with caution / security warning
26
+ process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0
27
+
28
+ // TODO: replace `receiptNo`
29
+ loadReceipt({receiptNo: 'ADQ...'}).then(htmlAsString => {
30
+ const {verify, verifyOnly, verifyAll, equals} = receipt(parseFromHTML(htmlAsString), {to: 'Someone'})
31
+ verify((parsedFields, preDefinedFields) => {
32
+ // do anything with receipt
33
+ })
34
+ }).catch(reason => {
35
+ console.error(reason)
36
+ }).finally(() => {
37
+ process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 1
38
+ })
39
+ ```
40
+
41
+ #### Using custom receipt parser with custom string source
42
+
43
+ ```javascript
44
+ const {receipt} = require('telebirr-receipt');
45
+
46
+ function parse() {
47
+ const source = 'payer_name=Abebe;amount=5000;to=Kebede'
48
+ const result = {};
49
+
50
+ source.split(';').forEach(pair => {
51
+ const field = pair.split('=');
52
+ result[field[0]] = field[1];
53
+ });
54
+
55
+ return result;
56
+ }
57
+
58
+ const {verifyAll, verify, equals} = receipt(parse(), {to: 'Kebede', amount: '5000'})
59
+
60
+ // match expected receiver's name with the name found on receipt
61
+ console.log(verify((parsedInfo, myInfo) => equals(parsedInfo?.to, myInfo?.to))) // match any field
62
+
63
+ console.log(verifyAll(['payer_name'])) // match every field but `payer_name`
64
+ ```
65
+
66
+ ### Use case
67
+
68
+ The main use case of this project is to overcome payment api unavailability and requirements in Ethiopia. Based on this
69
+ project you can get paid and verify your payment transactions.
70
+
71
+ 1. Your customers may fill their personal or payment information
72
+ 2. Your customers pay you via telebirr
73
+ 3. You let your customers know how to verify their payment by entering their transaction number or link which is
74
+ received from 127 / telebirr SMS.
75
+ 4. So you can verify that who send the money to you.
76
+
77
+ ### IMPORTANT
78
+
79
+ The telebirr receipt website may be modified or changed at anytime, so it is important to test HTML parser every time
80
+ before letting users pay and before verifying.
81
+ You could test the parser simply by hard coding a sample transaction information manually and matching it with parsed
82
+ from url.
83
+
84
+ ### Known issues
85
+
86
+ 1. `loadReceipt` fails with error: `unable to verify the first certificate`, code: `UNABLE_TO_VERIFY_LEAF_SIGNATURE`.
87
+ Disabling certificate verification could fix the problem, but it is not recommended.
88
+ Let me know if there is good solution to this problem.
89
+ 2. `parseFromHTML` may return different receipt field values or even empty and omit field values because of
90
+ the variability of the receipt website based on transaction such as transferring to bank and direct telebirr
91
+ transferring have different receipt format and soon. Thus, you should take a look at the return data of the
92
+ function `parseFromHTML` for different transaction destinations.
93
+
94
+ ### DISCLAIMER
95
+
96
+ The open-source code provided is offered "as is" without warranty of any kind, either express or implied, including, but
97
+ not limited to, the implied warranties of merchantability and fitness for a particular purpose. The author of this code
98
+ shall not be liable for any damages arising from the use of this code, including, but not limited to, direct, indirect,
99
+ incidental, special, or consequential damages.
100
+
101
+ Users of this open-source code are solely responsible for testing and verifying the code's suitability for their
102
+ specific purposes. The author of this code makes no representations or warranties about the code's accuracy,
103
+ reliability, completeness, or timeliness. The user assumes all risk associated with the use of this code and agrees to
104
+ indemnify and hold harmless the author of this code from any and all claims, damages, or liabilities arising from the
105
+ use of this code.
106
+
107
+ By using this open-source code, the user acknowledges and agrees to these terms and conditions.
package/index.js ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Initialize receipt
3
+ * @param {Object} parsedFields Parsed receipt information
4
+ * @param {Object} preDefinedFields Expected receipt information, used for verification
5
+ * @return {Object}
6
+ */
7
+ function receipt(parsedFields = {}, preDefinedFields = {}) {
8
+ /**
9
+ * Compare two strings or numbers
10
+ * @param {string|number} a
11
+ * @param {string|number} b
12
+ * @return {boolean} `true` if a and b are both string or number and equal otherwise `false`
13
+ */
14
+ function equals(a, b) {
15
+ // considering `undefined === undefined`
16
+ return (typeof a === 'string' && typeof b === 'string' || typeof a === 'number' && typeof b === 'number') && a === b
17
+ }
18
+
19
+ /**
20
+ * Verify any parsed field with its corresponding predefined field
21
+ * @param {function(Object, Object) : boolean} callback Callback function with two parameters `parsedFields` and `preDefinedFields` respectively
22
+ * @return {boolean} the return value from the callback function, should return `boolean`
23
+ */
24
+ function verify(callback) {
25
+ return callback(parsedFields, preDefinedFields)
26
+ }
27
+
28
+ /**
29
+ * Match or verify all parsed fields with their corresponding predefined fields
30
+ * `parsedFields` and `preDefinedFields` must have the same name
31
+ * @param {[string]} doNotCompare List of field names to ignore from checking
32
+ * @return {boolean} `true` if all fields match otherwise `false`, if `parsedFields` are empty returns `false`
33
+ */
34
+ function verifyAll(doNotCompare = []) {
35
+ if (Object.keys(parsedFields).length <= 0) {
36
+ return false
37
+ }
38
+
39
+ for (const key in parsedFields) {
40
+ if (Object.hasOwnProperty.call(parsedFields, key)) {
41
+ if (!doNotCompare.includes(key)) {
42
+ if (!equals(parsedFields[key], preDefinedFields[key])) {
43
+ return false
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ return true
50
+ }
51
+
52
+ /**
53
+ * Match or verify specified list of fields with their corresponding predefined fields
54
+ * `parsedFields` and `preDefinedFields` must have the same name
55
+ * @param {[string]} fieldNames List of field names to check
56
+ * @return {boolean} `true` if all fields match otherwise `false`, if empty `fieldNames` passed returns `false`
57
+ */
58
+ function verifyOnly(fieldNames = []) {
59
+ if (fieldNames.length <= 0) {
60
+ return false
61
+ }
62
+
63
+ fieldNames.forEach(key => {
64
+ if (!equals(fieldNames[key], preDefinedFields[key])) {
65
+ return false
66
+ }
67
+ })
68
+
69
+ return true
70
+ }
71
+
72
+ return {equals, verify, verifyOnly, verifyAll}
73
+ }
74
+
75
+ /**
76
+ * Parse fields from HTML string
77
+ * @param {string} str HTML string
78
+ * @return {{payer_name:string?, payer_phone:string?, payer_acc_type:string?, credited_party_name:string?, credited_party_acc_no:string?, transaction_status:string?, bank_acc_no:string?, receiptNo:string?, date:string?, settled_amount:number?, discount_amount:number?, vat_amount:number?, total_amount:number?, amount_in_word:string?, payment_mode:string?, payment_reason:string?, payment_channel:string?}} `parsedFields` as an object with field names and values
79
+ */
80
+ function parseFromHTML(str) {
81
+ const htmlparser = require('htmlparser2')
82
+ const document = htmlparser.parseDocument(str)
83
+ const td = htmlparser.DomUtils.getElementsByTagName('td', document)
84
+ const fields = {}
85
+
86
+ function getNextValue(index) {
87
+ if (index + 1 < td.length) {
88
+ return htmlparser.DomUtils.textContent(td[index + 1]).replace(/[\n\r\t]/ig, '').replace(/\s+/ig, ' ').trim();
89
+ }
90
+ return ''
91
+ }
92
+
93
+ const names = ['የከፋይስም/payername', 'የከፋይቴሌብርቁ./payertelebirrno.', 'የከፋይአካውንትአይነት/payeraccounttype', 'የገንዘብተቀባይስም/creditedpartyname', 'የገንዘብተቀባይቴሌብርቁ./creditedpartyaccountno', 'የክፍያውሁኔታ/transactionstatus', 'የባንክአካውንትቁጥር/bankaccountnumber', 'የክፍያቁጥር/receiptno.', 'የክፍያቀን/paymentdate', 'የተከፈለውመጠን/settledamount', 'ቅናሽ/discountamount', '15%ቫት/vat', 'ጠቅላላየተክፈለ/totalamountpaid', 'የገንዘቡልክበፊደል/totalamountinword', 'የክፍያዘዴ/paymentmode', 'የክፍያምክንያት/paymentreason', 'የክፍያመንገድ/paymentchannel']
94
+ const keys = ['payer_name', 'payer_phone', 'payer_acc_type', 'credited_party_name', 'credited_party_acc_no', 'transaction_status', 'bank_acc_no', 'receiptNo', 'date', 'settled_amount', 'discount_amount', 'vat_amount', 'total_amount', 'amount_in_word', 'payment_mode', 'payment_reason', 'payment_channel']
95
+
96
+ td.forEach((value, index) => {
97
+ let s = htmlparser.DomUtils.textContent(value).replace(/[\n\r\s\t]/ig, '').trim().toLowerCase();
98
+
99
+ const indexOf = names.indexOf(s)
100
+ if (indexOf >= 0) {
101
+ let nextValue = getNextValue(index + (indexOf > 6 && indexOf <= 9 ? 2 : 0));
102
+ let key = keys[indexOf];
103
+
104
+ if (key === 'bank_acc_no') {
105
+ fields['to'] = nextValue.replace(/^[0-9]+/g, '').trim()
106
+ fields[key] = nextValue.replace(/[A-Z]/ig, '').trim()
107
+ } else if (key.endsWith('amount')) {
108
+ fields[key] = parseFloat(nextValue.replace(/birr/ig, '').trim())
109
+ } else {
110
+ fields[key] = nextValue
111
+ }
112
+ }
113
+ })
114
+
115
+ return fields
116
+ }
117
+
118
+ /**
119
+ * Load receipt HTML string from receipt number or any url
120
+ * @param {string} [receiptNo] Receipt number received from telebirr SMS, high priority
121
+ * @param {string} [fullUrl] Or full receipt url, less prior
122
+ * @return {Promise<String>} HTML string
123
+ */
124
+ function loadReceipt({receiptNo, fullUrl}) {
125
+ return new Promise((resolve, reject) => {
126
+ const url = receiptNo ? `https://transactioninfo.ethiotelecom.et/receipt/${receiptNo}` : fullUrl
127
+
128
+ const https = require('https');
129
+ const options = new URL(url)
130
+
131
+ let data = ''
132
+ const req = https.request(options, (res) => {
133
+ res.on('data', (chunk) => {
134
+ data += chunk
135
+ })
136
+
137
+ res.on('error', (error) => {
138
+ reject(error)
139
+ })
140
+
141
+ res.on('end', () => {
142
+ resolve(data)
143
+ })
144
+ })
145
+ req.on('error', (error) => {
146
+ reject(error)
147
+ })
148
+ req.end()
149
+ })
150
+ }
151
+
152
+ module.exports = {receipt, utils: {parseFromHTML, loadReceipt}}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "telebirr-receipt",
3
+ "description": "Telebirr transaction receipt parser and verifier",
4
+ "version": "0.0.1",
5
+ "author": "Yonathan Tesfaye <yt040448@gmail.com>",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "start": "node index.js",
9
+ "test": "echo \"Error: no test specified\" && exit 1"
10
+ },
11
+ "keywords": [
12
+ "telebirr",
13
+ "payment",
14
+ "gateway",
15
+ "api",
16
+ "parse",
17
+ "transaction"
18
+ ],
19
+ "license": "Apache-2.0",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git://github.com/TheRealYT/telebirr-receipt.git"
23
+ },
24
+ "files": [
25
+ "README.md",
26
+ "LICENSE",
27
+ "package.json",
28
+ "index.js"
29
+ ],
30
+ "dependencies": {
31
+ "htmlparser2": "^9.0.0"
32
+ }
33
+ }