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.
- package/LICENSE +14 -0
- package/README.md +107 -0
- package/index.js +152 -0
- 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
|
+
}
|