wahdx-api 1.0.3 → 1.0.4
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 +216 -188
- package/dist/index.cjs +9 -1
- package/dist/index.mjs +9 -1
- package/dist/qr-generator.cjs +207 -207
- package/dist/qr-generator.mjs +210 -210
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,189 +1,217 @@
|
|
|
1
|
-
# wahdx-api
|
|
2
|
-
|
|
3
|
-
Package untuk generate QRIS dan cek payment status secara realtime dengan API OrderKuota dari [https://api.wahdx.co](https://api.wahdx.co).
|
|
4
|
-
|
|
5
|
-
## Fitur
|
|
6
|
-
|
|
7
|
-
- Membaca dan ekstrak kode QR dari gambar
|
|
8
|
-
- Generate kode QR dengan nominal tertentu
|
|
9
|
-
- Cek status pembayaran secara realtime
|
|
10
|
-
- Generate bukti transaksi (receipt)
|
|
11
|
-
- Kompatibel di semua platform (Windows, Linux, Mac)
|
|
12
|
-
|
|
13
|
-
## Instalasi
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm install wahdx-api
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Konfigurasi
|
|
20
|
-
|
|
21
|
-
Buat file `.env` di root project Anda:
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
WAHDX_TOKENKEY=your_wahdx_token_key
|
|
25
|
-
ORKUT_TOKEN_AUTH=your_orkut_token_auth
|
|
26
|
-
ORKUT_USERNAME=your_orkut_username
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
> **Catatan:** Untuk mendapatkan tokenKey, Anda bisa membelinya di halaman [https://api.wahdx.co](https://api.wahdx.co)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
console.log('
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
####
|
|
133
|
-
|
|
134
|
-
```javascript
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
1
|
+
# wahdx-api
|
|
2
|
+
|
|
3
|
+
Package untuk generate QRIS dan cek payment status secara realtime dengan API OrderKuota dari [https://api.wahdx.co](https://api.wahdx.co).
|
|
4
|
+
|
|
5
|
+
## Fitur
|
|
6
|
+
|
|
7
|
+
- Membaca dan ekstrak kode QR dari gambar
|
|
8
|
+
- Generate kode QR dengan nominal tertentu
|
|
9
|
+
- Cek status pembayaran secara realtime
|
|
10
|
+
- Generate bukti transaksi (receipt)
|
|
11
|
+
- Kompatibel di semua platform (Windows, Linux, Mac)
|
|
12
|
+
|
|
13
|
+
## Instalasi
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install wahdx-api
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Konfigurasi
|
|
20
|
+
|
|
21
|
+
Buat file `.env` di root project Anda:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
WAHDX_TOKENKEY=your_wahdx_token_key
|
|
25
|
+
ORKUT_TOKEN_AUTH=your_orkut_token_auth
|
|
26
|
+
ORKUT_USERNAME=your_orkut_username
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> **Catatan:** Untuk mendapatkan tokenKey, Anda bisa membelinya di halaman [https://api.wahdx.co](https://api.wahdx.co)
|
|
30
|
+
|
|
31
|
+
### Opsi Konfigurasi
|
|
32
|
+
|
|
33
|
+
| Opsi | Deskripsi | Default |
|
|
34
|
+
|---------------------|------------------------------------------------------------------|-------------|
|
|
35
|
+
| storeName | Nama toko yang akan ditampilkan pada receipt | - |
|
|
36
|
+
| defaultQrPath | Path ke QRIS static yang didownload di merchant orderkuota | - |
|
|
37
|
+
| tokenKey | Token key dari WAHDX | - |
|
|
38
|
+
| auth_token | Token autentikasi OrderKuota | - |
|
|
39
|
+
| auth_username | Username OrderKuota | - |
|
|
40
|
+
| autoGenerateReceipt | Mengaktifkan/menonaktifkan pembuatan receipt otomatis | true |
|
|
41
|
+
|
|
42
|
+
## Penggunaan
|
|
43
|
+
|
|
44
|
+
### Contoh Lengkap
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
import QRISPayment from 'wahdx-api';
|
|
48
|
+
import '@dotenvx/dotenvx/config';
|
|
49
|
+
import fs from 'fs';
|
|
50
|
+
|
|
51
|
+
// Konfigurasi
|
|
52
|
+
const config = {
|
|
53
|
+
storeName: 'AHDX STORE',
|
|
54
|
+
defaultQrPath: 'QRIS.png', // Path ke QRIS static yang didownload di merchant orderkuota
|
|
55
|
+
tokenKey: process.env.WAHDX_TOKENKEY,
|
|
56
|
+
auth_token: process.env.ORKUT_TOKEN_AUTH,
|
|
57
|
+
auth_username: process.env.ORKUT_USERNAME,
|
|
58
|
+
autoGenerateReceipt: true // Atur false jika tidak ingin otomatis membuat receipt
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Membuat instance QRISPayment
|
|
62
|
+
const qrisPayment = new QRISPayment(config);
|
|
63
|
+
|
|
64
|
+
async function main() {
|
|
65
|
+
try {
|
|
66
|
+
console.log('=== TEST REALTIME QRIS PAYMENT ===\n');
|
|
67
|
+
const randomAmount = Math.floor(Math.random() * 99) + 1; // Random 1-99
|
|
68
|
+
const amount = 100 + randomAmount; // Base 100 + random amount
|
|
69
|
+
const reference = 'REF' + Date.now();
|
|
70
|
+
|
|
71
|
+
// Generate QR code
|
|
72
|
+
const { qrBuffer } = await qrisPayment.generateQRFromImage(amount);
|
|
73
|
+
|
|
74
|
+
// Save QR code image
|
|
75
|
+
fs.writeFileSync(`qr-${amount}.png`, qrBuffer);
|
|
76
|
+
|
|
77
|
+
console.log('=== TRANSACTION DETAILS ===');
|
|
78
|
+
console.log('Reference:', reference);
|
|
79
|
+
console.log('Amount:', amount);
|
|
80
|
+
console.log('QR Image:', `qr-${amount}.png`);
|
|
81
|
+
console.log('\nSilakan scan QR code dan lakukan pembayaran');
|
|
82
|
+
console.log('\nMenunggu pembayaran...\n');
|
|
83
|
+
|
|
84
|
+
// Check payment status with 5 minutes timeout
|
|
85
|
+
const startTime = Date.now();
|
|
86
|
+
const timeout = 5 * 60 * 1000;
|
|
87
|
+
|
|
88
|
+
while (Date.now() - startTime < timeout) {
|
|
89
|
+
const result = await qrisPayment.checkPayment(reference, amount);
|
|
90
|
+
|
|
91
|
+
if (result.success && result.data.status === 'PAID') {
|
|
92
|
+
console.log('✓ Pembayaran berhasil!');
|
|
93
|
+
if (result.receipt) {
|
|
94
|
+
console.log('✓ Bukti transaksi:', result.receipt.filePath);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await new Promise(resolve => setTimeout(resolve, 10000)); // delay 10 detik
|
|
100
|
+
console.log('Menunggu pembayaran...');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
throw new Error('Timeout: Pembayaran tidak diterima dalam 5 menit');
|
|
104
|
+
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Error:', error.message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### API Reference
|
|
114
|
+
|
|
115
|
+
#### Inisialisasi
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
import QRISPayment from 'wahdx-api';
|
|
119
|
+
|
|
120
|
+
const config = {
|
|
121
|
+
storeName: 'NAMA TOKO',
|
|
122
|
+
defaultQrPath: 'path/to/qris/template.png',
|
|
123
|
+
tokenKey: 'your_wahdx_token_key',
|
|
124
|
+
auth_token: 'your_orkut_token_auth',
|
|
125
|
+
auth_username: 'your_orkut_username',
|
|
126
|
+
autoGenerateReceipt: true // Atur false jika tidak ingin otomatis membuat receipt
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const qrisPayment = new QRISPayment(config);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### Generate QR dari Template
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
const amount = 10000; // Rp 10.000
|
|
136
|
+
// Menggunakan default QR yang sudah diatur
|
|
137
|
+
const { qrString, qrBuffer } = await qrisPayment.generateQRFromImage(amount);
|
|
138
|
+
// Atau dengan path QR spesifik
|
|
139
|
+
// const { qrString, qrBuffer } = await qrisPayment.generateQRFromImage(amount, 'path/to/qris/template.png');
|
|
140
|
+
|
|
141
|
+
// Simpan QR ke file
|
|
142
|
+
fs.writeFileSync('qr-output.png', qrBuffer);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### Cek Status Pembayaran
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
const reference = 'REF' + Date.now();
|
|
149
|
+
const amount = 10000;
|
|
150
|
+
|
|
151
|
+
const result = await qrisPayment.checkPayment(reference, amount);
|
|
152
|
+
console.log(result);
|
|
153
|
+
/*
|
|
154
|
+
Output jika berhasil:
|
|
155
|
+
{
|
|
156
|
+
success: true,
|
|
157
|
+
data: {
|
|
158
|
+
status: 'PAID',
|
|
159
|
+
amount: 10000,
|
|
160
|
+
reference: 'REF1234567890',
|
|
161
|
+
...
|
|
162
|
+
},
|
|
163
|
+
receipt: {
|
|
164
|
+
filePath: 'path/to/receipt.pdf',
|
|
165
|
+
...
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
*/
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### Generate Receipt Secara Manual
|
|
172
|
+
|
|
173
|
+
Jika Anda telah menonaktifkan `autoGenerateReceipt` dalam konfigurasi, Anda dapat menghasilkan receipt secara manual dengan metode berikut:
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
// Ketika pembayaran sudah diterima
|
|
177
|
+
const result = await qrisPayment.checkPayment(reference, amount);
|
|
178
|
+
|
|
179
|
+
if (result.success && result.data.status === 'PAID') {
|
|
180
|
+
// Generate receipt secara manual
|
|
181
|
+
const receipt = await qrisPayment.generateReceipt(result.data);
|
|
182
|
+
console.log('Receipt berhasil dibuat:', receipt.filePath);
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## FAQ
|
|
187
|
+
|
|
188
|
+
### Q: Bagaimana cara mendapatkan tokenKey agar bisa menggunakan module ini?
|
|
189
|
+
A: Anda bisa membeli tokenKey di halaman utama [https://api.wahdx.co](https://api.wahdx.co)
|
|
190
|
+
|
|
191
|
+
### Q: Bagaimana cara mendapatkan kredensial API OrderKuota?
|
|
192
|
+
A: Silahkan kunjungi dokumentasi api [https://api.wahdx.co/api-docs](https://api.wahdx.co/api-docs) untuk mendapatkan token pada akun orderkuota anda.
|
|
193
|
+
|
|
194
|
+
### Q: Apakah module ini bisa digunakan di project CommonJS?
|
|
195
|
+
A: Ya! Package ini mendukung dual module system (ESM dan CommonJS). Anda bisa menggunakan dengan dua cara:
|
|
196
|
+
|
|
197
|
+
1. Dengan ES Modules (dalam file .js dengan `type: "module"` di package.json):
|
|
198
|
+
```javascript
|
|
199
|
+
import QRISPayment from 'wahdx-api';
|
|
200
|
+
|
|
201
|
+
const qrisPayment = new QRISPayment(config);
|
|
202
|
+
// gunakan qrisPayment
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
2. Dengan CommonJS (dalam file .cjs atau project tanpa `type: "module"`):
|
|
206
|
+
```javascript
|
|
207
|
+
const QRISPayment = require('wahdx-api');
|
|
208
|
+
|
|
209
|
+
const qrisPayment = new QRISPayment(config);
|
|
210
|
+
// gunakan qrisPayment
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Package secara otomatis mendeteksi format yang digunakan dan menyediakan versi yang sesuai.
|
|
214
|
+
|
|
215
|
+
## Lisensi
|
|
216
|
+
|
|
189
217
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -4,6 +4,12 @@ const ReceiptGenerator = require("./receipt-generator.cjs");
|
|
|
4
4
|
|
|
5
5
|
class QRISPayment {
|
|
6
6
|
constructor(config = {}) {
|
|
7
|
+
// Default config
|
|
8
|
+
this.config = {
|
|
9
|
+
autoGenerateReceipt: true, // Default-nya adalah true untuk mempertahankan kompatibilitas
|
|
10
|
+
...config
|
|
11
|
+
};
|
|
12
|
+
|
|
7
13
|
this.qrGenerator = new QRISGenerator(config);
|
|
8
14
|
this.paymentChecker = new PaymentChecker(config);
|
|
9
15
|
this.receiptGenerator = new ReceiptGenerator(config);
|
|
@@ -39,7 +45,9 @@ class QRISPayment {
|
|
|
39
45
|
|
|
40
46
|
async checkPayment(reference, amount) {
|
|
41
47
|
const result = await this.paymentChecker.checkPaymentStatus(reference, amount);
|
|
42
|
-
|
|
48
|
+
|
|
49
|
+
// Cek apakah pembayaran sukses dan statusnya PAID, serta autoGenerateReceipt diaktifkan
|
|
50
|
+
if (result.success && result.data.status === 'PAID' && this.config.autoGenerateReceipt) {
|
|
43
51
|
const receipt = await this.receiptGenerator.generateReceipt(result.data);
|
|
44
52
|
result.receipt = receipt;
|
|
45
53
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,12 @@ import ReceiptGenerator from './receipt-generator.mjs';
|
|
|
4
4
|
|
|
5
5
|
class QRISPayment {
|
|
6
6
|
constructor(config = {}) {
|
|
7
|
+
// Default config
|
|
8
|
+
this.config = {
|
|
9
|
+
autoGenerateReceipt: true, // Default-nya adalah true untuk mempertahankan kompatibilitas
|
|
10
|
+
...config
|
|
11
|
+
};
|
|
12
|
+
|
|
7
13
|
this.qrGenerator = new QRISGenerator(config);
|
|
8
14
|
this.paymentChecker = new PaymentChecker(config);
|
|
9
15
|
this.receiptGenerator = new ReceiptGenerator(config);
|
|
@@ -39,7 +45,9 @@ class QRISPayment {
|
|
|
39
45
|
|
|
40
46
|
async checkPayment(reference, amount) {
|
|
41
47
|
const result = await this.paymentChecker.checkPaymentStatus(reference, amount);
|
|
42
|
-
|
|
48
|
+
|
|
49
|
+
// Cek apakah pembayaran sukses dan statusnya PAID, serta autoGenerateReceipt diaktifkan
|
|
50
|
+
if (result.success && result.data.status === 'PAID' && this.config.autoGenerateReceipt) {
|
|
43
51
|
const receipt = await this.receiptGenerator.generateReceipt(result.data);
|
|
44
52
|
result.receipt = receipt;
|
|
45
53
|
}
|
package/dist/qr-generator.cjs
CHANGED
|
@@ -1,208 +1,208 @@
|
|
|
1
|
-
const QRCode = require("qrcode");
|
|
2
|
-
const fs = require("fs");
|
|
3
|
-
const sharp = require("sharp");
|
|
4
|
-
const jsQR = require("jsqr");
|
|
5
|
-
const path = require("path");
|
|
6
|
-
// CJS sudah menyediakan __filename dan __dirname secara otomatis
|
|
7
|
-
|
|
8
|
-
class QRISGenerator {
|
|
9
|
-
constructor(config = {}) {
|
|
10
|
-
this.config = {
|
|
11
|
-
baseQrString: config.baseQrString || '',
|
|
12
|
-
defaultQrPath: config.defaultQrPath || 'QRIS.png'
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Membaca QR code dari gambar dan mengekstrak baseQrString
|
|
18
|
-
* @param {string} imagePath - Path relatif atau absolut ke file gambar QR
|
|
19
|
-
* @returns {Promise<string>} - Promise yang menghasilkan baseQrString
|
|
20
|
-
*/
|
|
21
|
-
async readQRFromImage(imagePath) {
|
|
22
|
-
try {
|
|
23
|
-
// Konversi path relatif ke absolut jika diperlukan
|
|
24
|
-
const absolutePath = path.isAbsolute(imagePath)
|
|
25
|
-
? imagePath
|
|
26
|
-
: path.resolve(process.cwd(), imagePath);
|
|
27
|
-
|
|
28
|
-
// Periksa apakah file ada
|
|
29
|
-
if (!fs.existsSync(absolutePath)) {
|
|
30
|
-
throw new Error(`File tidak ditemukan: ${absolutePath}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Baca gambar menggunakan Sharp
|
|
34
|
-
const image = sharp(absolutePath);
|
|
35
|
-
const metadata = await image.metadata();
|
|
36
|
-
const { width, height } = metadata;
|
|
37
|
-
|
|
38
|
-
// Konversi ke raw pixel data
|
|
39
|
-
const rawData = await image
|
|
40
|
-
.raw()
|
|
41
|
-
.toBuffer();
|
|
42
|
-
|
|
43
|
-
// Format data untuk jsQR
|
|
44
|
-
const imageData = new Uint8ClampedArray(width * height * 4);
|
|
45
|
-
|
|
46
|
-
for (let i = 0; i < rawData.length; i += 3) { // RGB format
|
|
47
|
-
const pixelIndex = (i / 3) * 4; // Convert RGB index to RGBA index
|
|
48
|
-
imageData[pixelIndex] = rawData[i]; // R
|
|
49
|
-
imageData[pixelIndex + 1] = rawData[i + 1]; // G
|
|
50
|
-
imageData[pixelIndex + 2] = rawData[i + 2]; // B
|
|
51
|
-
imageData[pixelIndex + 3] = 255; // A (full opacity)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Dekode QR code menggunakan jsQR
|
|
55
|
-
const code = jsQR(imageData, width, height);
|
|
56
|
-
|
|
57
|
-
if (code) {
|
|
58
|
-
// Simpan baseQrString yang dibaca ke config
|
|
59
|
-
this.config.baseQrString = code.data;
|
|
60
|
-
return code.data;
|
|
61
|
-
} else {
|
|
62
|
-
throw new Error('QR code tidak terdeteksi dalam gambar');
|
|
63
|
-
}
|
|
64
|
-
} catch (error) {
|
|
65
|
-
console.error('Error saat membaca QR code:', error);
|
|
66
|
-
throw error;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Membaca QR code dari file default (biasanya QRIS.png)
|
|
72
|
-
* @returns {Promise<string>} - Promise yang menghasilkan baseQrString
|
|
73
|
-
*/
|
|
74
|
-
async readDefaultQR() {
|
|
75
|
-
try {
|
|
76
|
-
// Path default ke file QRIS.png
|
|
77
|
-
const defaultPath = path.resolve(process.cwd(), this.config.defaultQrPath);
|
|
78
|
-
return await this.readQRFromImage(defaultPath);
|
|
79
|
-
} catch (error) {
|
|
80
|
-
console.error(`Error saat membaca gambar default (${this.config.defaultQrPath}):`, error);
|
|
81
|
-
throw error;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Generate QR code sebagai PNG buffer
|
|
87
|
-
* @param {string} qrString - String QR code yang akan digenerate
|
|
88
|
-
* @returns {Promise<Buffer>} - Buffer PNG dari QR code
|
|
89
|
-
*/
|
|
90
|
-
async generateQRImage(qrString) {
|
|
91
|
-
try {
|
|
92
|
-
if (!qrString) {
|
|
93
|
-
throw new Error('qrString tidak boleh kosong');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Gunakan QRCode.toBuffer langsung tanpa canvas
|
|
97
|
-
const qrBuffer = await QRCode.toBuffer(qrString, {
|
|
98
|
-
errorCorrectionLevel: 'H',
|
|
99
|
-
margin: 2,
|
|
100
|
-
width: 500,
|
|
101
|
-
color: {
|
|
102
|
-
dark: '#000000',
|
|
103
|
-
light: '#ffffff'
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
return qrBuffer;
|
|
108
|
-
} catch (error) {
|
|
109
|
-
throw new Error('Gagal generate QR: ' + error.message);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Membaca QR dari file kemudian generate QR baru dengan nominal
|
|
115
|
-
* @param {number} amount - Nominal pembayaran
|
|
116
|
-
* @param {string} qrImagePath - Path ke gambar QR (opsional)
|
|
117
|
-
* @returns {Promise<{qrString: string, qrBuffer: Buffer}>}
|
|
118
|
-
*/
|
|
119
|
-
async generateQRFromImage(amount, qrImagePath = null) {
|
|
120
|
-
try {
|
|
121
|
-
// Jika path gambar disediakan, baca dari path tersebut
|
|
122
|
-
if (qrImagePath) {
|
|
123
|
-
await this.readQRFromImage(qrImagePath);
|
|
124
|
-
}
|
|
125
|
-
// Jika tidak ada path dan baseQrString kosong, gunakan default
|
|
126
|
-
else if (!this.config.baseQrString) {
|
|
127
|
-
await this.readDefaultQR();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Generate QR string dengan nominal
|
|
131
|
-
const qrString = this.generateQrString(amount);
|
|
132
|
-
|
|
133
|
-
// Generate QR image
|
|
134
|
-
const qrBuffer = await this.generateQRImage(qrString);
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
qrString,
|
|
138
|
-
qrBuffer
|
|
139
|
-
};
|
|
140
|
-
} catch (error) {
|
|
141
|
-
throw new Error('Gagal generate QR dari gambar: ' + error.message);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Generate QR dengan nominal tertentu
|
|
147
|
-
* @param {number} amount - Nominal pembayaran
|
|
148
|
-
* @returns {Promise<{qrString: string, qrBuffer: Buffer}>}
|
|
149
|
-
*/
|
|
150
|
-
async generateQR(amount) {
|
|
151
|
-
const qrString = this.generateQrString(amount);
|
|
152
|
-
const qrBuffer = await this.generateQRImage(qrString);
|
|
153
|
-
return {
|
|
154
|
-
qrString,
|
|
155
|
-
qrBuffer
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
generateQrString(amount) {
|
|
160
|
-
try {
|
|
161
|
-
if (!amount || amount <= 0) {
|
|
162
|
-
throw new Error('Nominal harus lebih besar dari 0');
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (!this.config.baseQrString) {
|
|
166
|
-
throw new Error('BaseQrString tidak tersedia. Gunakan readQRFromImage terlebih dahulu atau berikan baseQrString pada config.');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (!this.config.baseQrString.includes("5802ID")) {
|
|
170
|
-
throw new Error("Format QRIS tidak valid");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const finalAmount = Math.floor(amount);
|
|
174
|
-
const qrisBase = this.config.baseQrString.slice(0, -4).replace("010211", "010212");
|
|
175
|
-
const nominalStr = finalAmount.toString();
|
|
176
|
-
const nominalTag = `54${nominalStr.length.toString().padStart(2, '0')}${nominalStr}`;
|
|
177
|
-
const insertPosition = qrisBase.indexOf("5802ID");
|
|
178
|
-
const qrisWithNominal = qrisBase.slice(0, insertPosition) + nominalTag + qrisBase.slice(insertPosition);
|
|
179
|
-
const checksum = this.calculateCRC16(qrisWithNominal);
|
|
180
|
-
|
|
181
|
-
return qrisWithNominal + checksum;
|
|
182
|
-
} catch (error) {
|
|
183
|
-
throw new Error('Gagal generate string QRIS: ' + error.message);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
calculateCRC16(str) {
|
|
188
|
-
try {
|
|
189
|
-
if (!str) {
|
|
190
|
-
throw new Error('String tidak boleh kosong');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
let crc = 0xFFFF;
|
|
194
|
-
for (let i = 0; i < str.length; i++) {
|
|
195
|
-
crc ^= str.charCodeAt(i) << 8;
|
|
196
|
-
for (let j = 0; j < 8; j++) {
|
|
197
|
-
crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1);
|
|
198
|
-
}
|
|
199
|
-
crc &= 0xFFFF;
|
|
200
|
-
}
|
|
201
|
-
return crc.toString(16).toUpperCase().padStart(4, '0');
|
|
202
|
-
} catch (error) {
|
|
203
|
-
throw new Error('Gagal kalkulasi CRC16: ' + error.message);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
1
|
+
const QRCode = require("qrcode");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const sharp = require("sharp");
|
|
4
|
+
const jsQR = require("jsqr");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
// CJS sudah menyediakan __filename dan __dirname secara otomatis
|
|
7
|
+
|
|
8
|
+
class QRISGenerator {
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
this.config = {
|
|
11
|
+
baseQrString: config.baseQrString || '',
|
|
12
|
+
defaultQrPath: config.defaultQrPath || 'QRIS.png'
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Membaca QR code dari gambar dan mengekstrak baseQrString
|
|
18
|
+
* @param {string} imagePath - Path relatif atau absolut ke file gambar QR
|
|
19
|
+
* @returns {Promise<string>} - Promise yang menghasilkan baseQrString
|
|
20
|
+
*/
|
|
21
|
+
async readQRFromImage(imagePath) {
|
|
22
|
+
try {
|
|
23
|
+
// Konversi path relatif ke absolut jika diperlukan
|
|
24
|
+
const absolutePath = path.isAbsolute(imagePath)
|
|
25
|
+
? imagePath
|
|
26
|
+
: path.resolve(process.cwd(), imagePath);
|
|
27
|
+
|
|
28
|
+
// Periksa apakah file ada
|
|
29
|
+
if (!fs.existsSync(absolutePath)) {
|
|
30
|
+
throw new Error(`File tidak ditemukan: ${absolutePath}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Baca gambar menggunakan Sharp
|
|
34
|
+
const image = sharp(absolutePath);
|
|
35
|
+
const metadata = await image.metadata();
|
|
36
|
+
const { width, height } = metadata;
|
|
37
|
+
|
|
38
|
+
// Konversi ke raw pixel data
|
|
39
|
+
const rawData = await image
|
|
40
|
+
.raw()
|
|
41
|
+
.toBuffer();
|
|
42
|
+
|
|
43
|
+
// Format data untuk jsQR
|
|
44
|
+
const imageData = new Uint8ClampedArray(width * height * 4);
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < rawData.length; i += 3) { // RGB format
|
|
47
|
+
const pixelIndex = (i / 3) * 4; // Convert RGB index to RGBA index
|
|
48
|
+
imageData[pixelIndex] = rawData[i]; // R
|
|
49
|
+
imageData[pixelIndex + 1] = rawData[i + 1]; // G
|
|
50
|
+
imageData[pixelIndex + 2] = rawData[i + 2]; // B
|
|
51
|
+
imageData[pixelIndex + 3] = 255; // A (full opacity)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Dekode QR code menggunakan jsQR
|
|
55
|
+
const code = jsQR(imageData, width, height);
|
|
56
|
+
|
|
57
|
+
if (code) {
|
|
58
|
+
// Simpan baseQrString yang dibaca ke config
|
|
59
|
+
this.config.baseQrString = code.data;
|
|
60
|
+
return code.data;
|
|
61
|
+
} else {
|
|
62
|
+
throw new Error('QR code tidak terdeteksi dalam gambar');
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Error saat membaca QR code:', error);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Membaca QR code dari file default (biasanya QRIS.png)
|
|
72
|
+
* @returns {Promise<string>} - Promise yang menghasilkan baseQrString
|
|
73
|
+
*/
|
|
74
|
+
async readDefaultQR() {
|
|
75
|
+
try {
|
|
76
|
+
// Path default ke file QRIS.png
|
|
77
|
+
const defaultPath = path.resolve(process.cwd(), this.config.defaultQrPath);
|
|
78
|
+
return await this.readQRFromImage(defaultPath);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error(`Error saat membaca gambar default (${this.config.defaultQrPath}):`, error);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate QR code sebagai PNG buffer
|
|
87
|
+
* @param {string} qrString - String QR code yang akan digenerate
|
|
88
|
+
* @returns {Promise<Buffer>} - Buffer PNG dari QR code
|
|
89
|
+
*/
|
|
90
|
+
async generateQRImage(qrString) {
|
|
91
|
+
try {
|
|
92
|
+
if (!qrString) {
|
|
93
|
+
throw new Error('qrString tidak boleh kosong');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Gunakan QRCode.toBuffer langsung tanpa canvas
|
|
97
|
+
const qrBuffer = await QRCode.toBuffer(qrString, {
|
|
98
|
+
errorCorrectionLevel: 'H',
|
|
99
|
+
margin: 2,
|
|
100
|
+
width: 500,
|
|
101
|
+
color: {
|
|
102
|
+
dark: '#000000',
|
|
103
|
+
light: '#ffffff'
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return qrBuffer;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw new Error('Gagal generate QR: ' + error.message);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Membaca QR dari file kemudian generate QR baru dengan nominal
|
|
115
|
+
* @param {number} amount - Nominal pembayaran
|
|
116
|
+
* @param {string} qrImagePath - Path ke gambar QR (opsional)
|
|
117
|
+
* @returns {Promise<{qrString: string, qrBuffer: Buffer}>}
|
|
118
|
+
*/
|
|
119
|
+
async generateQRFromImage(amount, qrImagePath = null) {
|
|
120
|
+
try {
|
|
121
|
+
// Jika path gambar disediakan, baca dari path tersebut
|
|
122
|
+
if (qrImagePath) {
|
|
123
|
+
await this.readQRFromImage(qrImagePath);
|
|
124
|
+
}
|
|
125
|
+
// Jika tidak ada path dan baseQrString kosong, gunakan default
|
|
126
|
+
else if (!this.config.baseQrString) {
|
|
127
|
+
await this.readDefaultQR();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Generate QR string dengan nominal
|
|
131
|
+
const qrString = this.generateQrString(amount);
|
|
132
|
+
|
|
133
|
+
// Generate QR image
|
|
134
|
+
const qrBuffer = await this.generateQRImage(qrString);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
qrString,
|
|
138
|
+
qrBuffer
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw new Error('Gagal generate QR dari gambar: ' + error.message);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Generate QR dengan nominal tertentu
|
|
147
|
+
* @param {number} amount - Nominal pembayaran
|
|
148
|
+
* @returns {Promise<{qrString: string, qrBuffer: Buffer}>}
|
|
149
|
+
*/
|
|
150
|
+
async generateQR(amount) {
|
|
151
|
+
const qrString = this.generateQrString(amount);
|
|
152
|
+
const qrBuffer = await this.generateQRImage(qrString);
|
|
153
|
+
return {
|
|
154
|
+
qrString,
|
|
155
|
+
qrBuffer
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
generateQrString(amount) {
|
|
160
|
+
try {
|
|
161
|
+
if (!amount || amount <= 0) {
|
|
162
|
+
throw new Error('Nominal harus lebih besar dari 0');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!this.config.baseQrString) {
|
|
166
|
+
throw new Error('BaseQrString tidak tersedia. Gunakan readQRFromImage terlebih dahulu atau berikan baseQrString pada config.');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!this.config.baseQrString.includes("5802ID")) {
|
|
170
|
+
throw new Error("Format QRIS tidak valid");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const finalAmount = Math.floor(amount);
|
|
174
|
+
const qrisBase = this.config.baseQrString.slice(0, -4).replace("010211", "010212");
|
|
175
|
+
const nominalStr = finalAmount.toString();
|
|
176
|
+
const nominalTag = `54${nominalStr.length.toString().padStart(2, '0')}${nominalStr}`;
|
|
177
|
+
const insertPosition = qrisBase.indexOf("5802ID");
|
|
178
|
+
const qrisWithNominal = qrisBase.slice(0, insertPosition) + nominalTag + qrisBase.slice(insertPosition);
|
|
179
|
+
const checksum = this.calculateCRC16(qrisWithNominal);
|
|
180
|
+
|
|
181
|
+
return qrisWithNominal + checksum;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw new Error('Gagal generate string QRIS: ' + error.message);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
calculateCRC16(str) {
|
|
188
|
+
try {
|
|
189
|
+
if (!str) {
|
|
190
|
+
throw new Error('String tidak boleh kosong');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let crc = 0xFFFF;
|
|
194
|
+
for (let i = 0; i < str.length; i++) {
|
|
195
|
+
crc ^= str.charCodeAt(i) << 8;
|
|
196
|
+
for (let j = 0; j < 8; j++) {
|
|
197
|
+
crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1);
|
|
198
|
+
}
|
|
199
|
+
crc &= 0xFFFF;
|
|
200
|
+
}
|
|
201
|
+
return crc.toString(16).toUpperCase().padStart(4, '0');
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new Error('Gagal kalkulasi CRC16: ' + error.message);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
208
|
module.exports = QRISGenerator;
|
package/dist/qr-generator.mjs
CHANGED
|
@@ -1,211 +1,211 @@
|
|
|
1
|
-
import QRCode from 'qrcode';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import sharp from 'sharp';
|
|
4
|
-
import jsQR from 'jsqr';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
|
-
|
|
8
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname = path.dirname(__filename);
|
|
10
|
-
|
|
11
|
-
class QRISGenerator {
|
|
12
|
-
constructor(config = {}) {
|
|
13
|
-
this.config = {
|
|
14
|
-
baseQrString: config.baseQrString || '',
|
|
15
|
-
defaultQrPath: config.defaultQrPath || 'QRIS.png'
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Membaca QR code dari gambar dan mengekstrak baseQrString
|
|
21
|
-
* @param {string} imagePath - Path relatif atau absolut ke file gambar QR
|
|
22
|
-
* @returns {Promise<string>} - Promise yang menghasilkan baseQrString
|
|
23
|
-
*/
|
|
24
|
-
async readQRFromImage(imagePath) {
|
|
25
|
-
try {
|
|
26
|
-
// Konversi path relatif ke absolut jika diperlukan
|
|
27
|
-
const absolutePath = path.isAbsolute(imagePath)
|
|
28
|
-
? imagePath
|
|
29
|
-
: path.resolve(process.cwd(), imagePath);
|
|
30
|
-
|
|
31
|
-
// Periksa apakah file ada
|
|
32
|
-
if (!fs.existsSync(absolutePath)) {
|
|
33
|
-
throw new Error(`File tidak ditemukan: ${absolutePath}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Baca gambar menggunakan Sharp
|
|
37
|
-
const image = sharp(absolutePath);
|
|
38
|
-
const metadata = await image.metadata();
|
|
39
|
-
const { width, height } = metadata;
|
|
40
|
-
|
|
41
|
-
// Konversi ke raw pixel data
|
|
42
|
-
const rawData = await image
|
|
43
|
-
.raw()
|
|
44
|
-
.toBuffer();
|
|
45
|
-
|
|
46
|
-
// Format data untuk jsQR
|
|
47
|
-
const imageData = new Uint8ClampedArray(width * height * 4);
|
|
48
|
-
|
|
49
|
-
for (let i = 0; i < rawData.length; i += 3) { // RGB format
|
|
50
|
-
const pixelIndex = (i / 3) * 4; // Convert RGB index to RGBA index
|
|
51
|
-
imageData[pixelIndex] = rawData[i]; // R
|
|
52
|
-
imageData[pixelIndex + 1] = rawData[i + 1]; // G
|
|
53
|
-
imageData[pixelIndex + 2] = rawData[i + 2]; // B
|
|
54
|
-
imageData[pixelIndex + 3] = 255; // A (full opacity)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Dekode QR code menggunakan jsQR
|
|
58
|
-
const code = jsQR(imageData, width, height);
|
|
59
|
-
|
|
60
|
-
if (code) {
|
|
61
|
-
// Simpan baseQrString yang dibaca ke config
|
|
62
|
-
this.config.baseQrString = code.data;
|
|
63
|
-
return code.data;
|
|
64
|
-
} else {
|
|
65
|
-
throw new Error('QR code tidak terdeteksi dalam gambar');
|
|
66
|
-
}
|
|
67
|
-
} catch (error) {
|
|
68
|
-
console.error('Error saat membaca QR code:', error);
|
|
69
|
-
throw error;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Membaca QR code dari file default (biasanya QRIS.png)
|
|
75
|
-
* @returns {Promise<string>} - Promise yang menghasilkan baseQrString
|
|
76
|
-
*/
|
|
77
|
-
async readDefaultQR() {
|
|
78
|
-
try {
|
|
79
|
-
// Path default ke file QRIS.png
|
|
80
|
-
const defaultPath = path.resolve(process.cwd(), this.config.defaultQrPath);
|
|
81
|
-
return await this.readQRFromImage(defaultPath);
|
|
82
|
-
} catch (error) {
|
|
83
|
-
console.error(`Error saat membaca gambar default (${this.config.defaultQrPath}):`, error);
|
|
84
|
-
throw error;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Generate QR code sebagai PNG buffer
|
|
90
|
-
* @param {string} qrString - String QR code yang akan digenerate
|
|
91
|
-
* @returns {Promise<Buffer>} - Buffer PNG dari QR code
|
|
92
|
-
*/
|
|
93
|
-
async generateQRImage(qrString) {
|
|
94
|
-
try {
|
|
95
|
-
if (!qrString) {
|
|
96
|
-
throw new Error('qrString tidak boleh kosong');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Gunakan QRCode.toBuffer langsung tanpa canvas
|
|
100
|
-
const qrBuffer = await QRCode.toBuffer(qrString, {
|
|
101
|
-
errorCorrectionLevel: 'H',
|
|
102
|
-
margin: 2,
|
|
103
|
-
width: 500,
|
|
104
|
-
color: {
|
|
105
|
-
dark: '#000000',
|
|
106
|
-
light: '#ffffff'
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
return qrBuffer;
|
|
111
|
-
} catch (error) {
|
|
112
|
-
throw new Error('Gagal generate QR: ' + error.message);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Membaca QR dari file kemudian generate QR baru dengan nominal
|
|
118
|
-
* @param {number} amount - Nominal pembayaran
|
|
119
|
-
* @param {string} qrImagePath - Path ke gambar QR (opsional)
|
|
120
|
-
* @returns {Promise<{qrString: string, qrBuffer: Buffer}>}
|
|
121
|
-
*/
|
|
122
|
-
async generateQRFromImage(amount, qrImagePath = null) {
|
|
123
|
-
try {
|
|
124
|
-
// Jika path gambar disediakan, baca dari path tersebut
|
|
125
|
-
if (qrImagePath) {
|
|
126
|
-
await this.readQRFromImage(qrImagePath);
|
|
127
|
-
}
|
|
128
|
-
// Jika tidak ada path dan baseQrString kosong, gunakan default
|
|
129
|
-
else if (!this.config.baseQrString) {
|
|
130
|
-
await this.readDefaultQR();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Generate QR string dengan nominal
|
|
134
|
-
const qrString = this.generateQrString(amount);
|
|
135
|
-
|
|
136
|
-
// Generate QR image
|
|
137
|
-
const qrBuffer = await this.generateQRImage(qrString);
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
qrString,
|
|
141
|
-
qrBuffer
|
|
142
|
-
};
|
|
143
|
-
} catch (error) {
|
|
144
|
-
throw new Error('Gagal generate QR dari gambar: ' + error.message);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Generate QR dengan nominal tertentu
|
|
150
|
-
* @param {number} amount - Nominal pembayaran
|
|
151
|
-
* @returns {Promise<{qrString: string, qrBuffer: Buffer}>}
|
|
152
|
-
*/
|
|
153
|
-
async generateQR(amount) {
|
|
154
|
-
const qrString = this.generateQrString(amount);
|
|
155
|
-
const qrBuffer = await this.generateQRImage(qrString);
|
|
156
|
-
return {
|
|
157
|
-
qrString,
|
|
158
|
-
qrBuffer
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
generateQrString(amount) {
|
|
163
|
-
try {
|
|
164
|
-
if (!amount || amount <= 0) {
|
|
165
|
-
throw new Error('Nominal harus lebih besar dari 0');
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (!this.config.baseQrString) {
|
|
169
|
-
throw new Error('BaseQrString tidak tersedia. Gunakan readQRFromImage terlebih dahulu atau berikan baseQrString pada config.');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (!this.config.baseQrString.includes("5802ID")) {
|
|
173
|
-
throw new Error("Format QRIS tidak valid");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const finalAmount = Math.floor(amount);
|
|
177
|
-
const qrisBase = this.config.baseQrString.slice(0, -4).replace("010211", "010212");
|
|
178
|
-
const nominalStr = finalAmount.toString();
|
|
179
|
-
const nominalTag = `54${nominalStr.length.toString().padStart(2, '0')}${nominalStr}`;
|
|
180
|
-
const insertPosition = qrisBase.indexOf("5802ID");
|
|
181
|
-
const qrisWithNominal = qrisBase.slice(0, insertPosition) + nominalTag + qrisBase.slice(insertPosition);
|
|
182
|
-
const checksum = this.calculateCRC16(qrisWithNominal);
|
|
183
|
-
|
|
184
|
-
return qrisWithNominal + checksum;
|
|
185
|
-
} catch (error) {
|
|
186
|
-
throw new Error('Gagal generate string QRIS: ' + error.message);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
calculateCRC16(str) {
|
|
191
|
-
try {
|
|
192
|
-
if (!str) {
|
|
193
|
-
throw new Error('String tidak boleh kosong');
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
let crc = 0xFFFF;
|
|
197
|
-
for (let i = 0; i < str.length; i++) {
|
|
198
|
-
crc ^= str.charCodeAt(i) << 8;
|
|
199
|
-
for (let j = 0; j < 8; j++) {
|
|
200
|
-
crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1);
|
|
201
|
-
}
|
|
202
|
-
crc &= 0xFFFF;
|
|
203
|
-
}
|
|
204
|
-
return crc.toString(16).toUpperCase().padStart(4, '0');
|
|
205
|
-
} catch (error) {
|
|
206
|
-
throw new Error('Gagal kalkulasi CRC16: ' + error.message);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
1
|
+
import QRCode from 'qrcode';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
import jsQR from 'jsqr';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
class QRISGenerator {
|
|
12
|
+
constructor(config = {}) {
|
|
13
|
+
this.config = {
|
|
14
|
+
baseQrString: config.baseQrString || '',
|
|
15
|
+
defaultQrPath: config.defaultQrPath || 'QRIS.png'
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Membaca QR code dari gambar dan mengekstrak baseQrString
|
|
21
|
+
* @param {string} imagePath - Path relatif atau absolut ke file gambar QR
|
|
22
|
+
* @returns {Promise<string>} - Promise yang menghasilkan baseQrString
|
|
23
|
+
*/
|
|
24
|
+
async readQRFromImage(imagePath) {
|
|
25
|
+
try {
|
|
26
|
+
// Konversi path relatif ke absolut jika diperlukan
|
|
27
|
+
const absolutePath = path.isAbsolute(imagePath)
|
|
28
|
+
? imagePath
|
|
29
|
+
: path.resolve(process.cwd(), imagePath);
|
|
30
|
+
|
|
31
|
+
// Periksa apakah file ada
|
|
32
|
+
if (!fs.existsSync(absolutePath)) {
|
|
33
|
+
throw new Error(`File tidak ditemukan: ${absolutePath}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Baca gambar menggunakan Sharp
|
|
37
|
+
const image = sharp(absolutePath);
|
|
38
|
+
const metadata = await image.metadata();
|
|
39
|
+
const { width, height } = metadata;
|
|
40
|
+
|
|
41
|
+
// Konversi ke raw pixel data
|
|
42
|
+
const rawData = await image
|
|
43
|
+
.raw()
|
|
44
|
+
.toBuffer();
|
|
45
|
+
|
|
46
|
+
// Format data untuk jsQR
|
|
47
|
+
const imageData = new Uint8ClampedArray(width * height * 4);
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < rawData.length; i += 3) { // RGB format
|
|
50
|
+
const pixelIndex = (i / 3) * 4; // Convert RGB index to RGBA index
|
|
51
|
+
imageData[pixelIndex] = rawData[i]; // R
|
|
52
|
+
imageData[pixelIndex + 1] = rawData[i + 1]; // G
|
|
53
|
+
imageData[pixelIndex + 2] = rawData[i + 2]; // B
|
|
54
|
+
imageData[pixelIndex + 3] = 255; // A (full opacity)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Dekode QR code menggunakan jsQR
|
|
58
|
+
const code = jsQR(imageData, width, height);
|
|
59
|
+
|
|
60
|
+
if (code) {
|
|
61
|
+
// Simpan baseQrString yang dibaca ke config
|
|
62
|
+
this.config.baseQrString = code.data;
|
|
63
|
+
return code.data;
|
|
64
|
+
} else {
|
|
65
|
+
throw new Error('QR code tidak terdeteksi dalam gambar');
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Error saat membaca QR code:', error);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Membaca QR code dari file default (biasanya QRIS.png)
|
|
75
|
+
* @returns {Promise<string>} - Promise yang menghasilkan baseQrString
|
|
76
|
+
*/
|
|
77
|
+
async readDefaultQR() {
|
|
78
|
+
try {
|
|
79
|
+
// Path default ke file QRIS.png
|
|
80
|
+
const defaultPath = path.resolve(process.cwd(), this.config.defaultQrPath);
|
|
81
|
+
return await this.readQRFromImage(defaultPath);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error(`Error saat membaca gambar default (${this.config.defaultQrPath}):`, error);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generate QR code sebagai PNG buffer
|
|
90
|
+
* @param {string} qrString - String QR code yang akan digenerate
|
|
91
|
+
* @returns {Promise<Buffer>} - Buffer PNG dari QR code
|
|
92
|
+
*/
|
|
93
|
+
async generateQRImage(qrString) {
|
|
94
|
+
try {
|
|
95
|
+
if (!qrString) {
|
|
96
|
+
throw new Error('qrString tidak boleh kosong');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Gunakan QRCode.toBuffer langsung tanpa canvas
|
|
100
|
+
const qrBuffer = await QRCode.toBuffer(qrString, {
|
|
101
|
+
errorCorrectionLevel: 'H',
|
|
102
|
+
margin: 2,
|
|
103
|
+
width: 500,
|
|
104
|
+
color: {
|
|
105
|
+
dark: '#000000',
|
|
106
|
+
light: '#ffffff'
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return qrBuffer;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new Error('Gagal generate QR: ' + error.message);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Membaca QR dari file kemudian generate QR baru dengan nominal
|
|
118
|
+
* @param {number} amount - Nominal pembayaran
|
|
119
|
+
* @param {string} qrImagePath - Path ke gambar QR (opsional)
|
|
120
|
+
* @returns {Promise<{qrString: string, qrBuffer: Buffer}>}
|
|
121
|
+
*/
|
|
122
|
+
async generateQRFromImage(amount, qrImagePath = null) {
|
|
123
|
+
try {
|
|
124
|
+
// Jika path gambar disediakan, baca dari path tersebut
|
|
125
|
+
if (qrImagePath) {
|
|
126
|
+
await this.readQRFromImage(qrImagePath);
|
|
127
|
+
}
|
|
128
|
+
// Jika tidak ada path dan baseQrString kosong, gunakan default
|
|
129
|
+
else if (!this.config.baseQrString) {
|
|
130
|
+
await this.readDefaultQR();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Generate QR string dengan nominal
|
|
134
|
+
const qrString = this.generateQrString(amount);
|
|
135
|
+
|
|
136
|
+
// Generate QR image
|
|
137
|
+
const qrBuffer = await this.generateQRImage(qrString);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
qrString,
|
|
141
|
+
qrBuffer
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw new Error('Gagal generate QR dari gambar: ' + error.message);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Generate QR dengan nominal tertentu
|
|
150
|
+
* @param {number} amount - Nominal pembayaran
|
|
151
|
+
* @returns {Promise<{qrString: string, qrBuffer: Buffer}>}
|
|
152
|
+
*/
|
|
153
|
+
async generateQR(amount) {
|
|
154
|
+
const qrString = this.generateQrString(amount);
|
|
155
|
+
const qrBuffer = await this.generateQRImage(qrString);
|
|
156
|
+
return {
|
|
157
|
+
qrString,
|
|
158
|
+
qrBuffer
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
generateQrString(amount) {
|
|
163
|
+
try {
|
|
164
|
+
if (!amount || amount <= 0) {
|
|
165
|
+
throw new Error('Nominal harus lebih besar dari 0');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!this.config.baseQrString) {
|
|
169
|
+
throw new Error('BaseQrString tidak tersedia. Gunakan readQRFromImage terlebih dahulu atau berikan baseQrString pada config.');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!this.config.baseQrString.includes("5802ID")) {
|
|
173
|
+
throw new Error("Format QRIS tidak valid");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const finalAmount = Math.floor(amount);
|
|
177
|
+
const qrisBase = this.config.baseQrString.slice(0, -4).replace("010211", "010212");
|
|
178
|
+
const nominalStr = finalAmount.toString();
|
|
179
|
+
const nominalTag = `54${nominalStr.length.toString().padStart(2, '0')}${nominalStr}`;
|
|
180
|
+
const insertPosition = qrisBase.indexOf("5802ID");
|
|
181
|
+
const qrisWithNominal = qrisBase.slice(0, insertPosition) + nominalTag + qrisBase.slice(insertPosition);
|
|
182
|
+
const checksum = this.calculateCRC16(qrisWithNominal);
|
|
183
|
+
|
|
184
|
+
return qrisWithNominal + checksum;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw new Error('Gagal generate string QRIS: ' + error.message);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
calculateCRC16(str) {
|
|
191
|
+
try {
|
|
192
|
+
if (!str) {
|
|
193
|
+
throw new Error('String tidak boleh kosong');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let crc = 0xFFFF;
|
|
197
|
+
for (let i = 0; i < str.length; i++) {
|
|
198
|
+
crc ^= str.charCodeAt(i) << 8;
|
|
199
|
+
for (let j = 0; j < 8; j++) {
|
|
200
|
+
crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1);
|
|
201
|
+
}
|
|
202
|
+
crc &= 0xFFFF;
|
|
203
|
+
}
|
|
204
|
+
return crc.toString(16).toUpperCase().padStart(4, '0');
|
|
205
|
+
} catch (error) {
|
|
206
|
+
throw new Error('Gagal kalkulasi CRC16: ' + error.message);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
211
|
export default QRISGenerator;
|
package/package.json
CHANGED