wahdx-api 1.0.2 → 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.
@@ -1,202 +1,211 @@
1
- import QRCode from 'qrcode';
2
- import { createCanvas, loadImage } from 'canvas';
3
- import fs from 'fs';
4
- import sharp from 'sharp';
5
- import jsQR from 'jsqr';
6
- import path from 'path';
7
- import { fileURLToPath } from 'url';
8
-
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = path.dirname(__filename);
11
-
12
- class QRISGenerator {
13
- constructor(config = {}) {
14
- this.config = {
15
- baseQrString: config.baseQrString || '',
16
- logoPath: config.logoPath || null,
17
- defaultQrPath: config.defaultQrPath || 'QRIS.png'
18
- };
19
- }
20
-
21
- /**
22
- * Membaca QR code dari gambar dan mengekstrak baseQrString
23
- * @param {string} imagePath - Path relatif atau absolut ke file gambar QR
24
- * @returns {Promise<string>} - Promise yang menghasilkan baseQrString
25
- */
26
- async readQRFromImage(imagePath) {
27
- try {
28
- // Konversi path relatif ke absolut jika diperlukan
29
- const absolutePath = path.isAbsolute(imagePath)
30
- ? imagePath
31
- : path.resolve(process.cwd(), imagePath);
32
-
33
- // Periksa apakah file ada
34
- if (!fs.existsSync(absolutePath)) {
35
- throw new Error(`File tidak ditemukan: ${absolutePath}`);
36
- }
37
-
38
- // Baca gambar menggunakan Sharp
39
- const image = sharp(absolutePath);
40
- const metadata = await image.metadata();
41
- const { width, height } = metadata;
42
-
43
- // Konversi ke raw pixel data
44
- const rawData = await image
45
- .raw()
46
- .toBuffer();
47
-
48
- // Format data untuk jsQR
49
- const imageData = new Uint8ClampedArray(width * height * 4);
50
-
51
- for (let i = 0; i < rawData.length; i += 3) { // RGB format
52
- const pixelIndex = (i / 3) * 4; // Convert RGB index to RGBA index
53
- imageData[pixelIndex] = rawData[i]; // R
54
- imageData[pixelIndex + 1] = rawData[i + 1]; // G
55
- imageData[pixelIndex + 2] = rawData[i + 2]; // B
56
- imageData[pixelIndex + 3] = 255; // A (full opacity)
57
- }
58
-
59
- // Dekode QR code menggunakan jsQR
60
- const code = jsQR(imageData, width, height);
61
-
62
- if (code) {
63
- // Simpan baseQrString yang dibaca ke config
64
- this.config.baseQrString = code.data;
65
- return code.data;
66
- } else {
67
- throw new Error('QR code tidak terdeteksi dalam gambar');
68
- }
69
- } catch (error) {
70
- console.error('Error saat membaca QR code:', error);
71
- throw error;
72
- }
73
- }
74
-
75
- /**
76
- * Membaca QR code dari file default (biasanya QRIS.png)
77
- * @returns {Promise<string>} - Promise yang menghasilkan baseQrString
78
- */
79
- async readDefaultQR() {
80
- try {
81
- // Path default ke file QRIS.png
82
- const defaultPath = path.resolve(process.cwd(), this.config.defaultQrPath);
83
- return await this.readQRFromImage(defaultPath);
84
- } catch (error) {
85
- console.error(`Error saat membaca gambar default (${this.config.defaultQrPath}):`, error);
86
- throw error;
87
- }
88
- }
89
-
90
- async generateQRWithLogo(qrString) {
91
- try {
92
- if (!qrString) {
93
- throw new Error('qrString tidak boleh kosong');
94
- }
95
- const canvas = createCanvas(500, 500);
96
- const ctx = canvas.getContext('2d');
97
- await QRCode.toCanvas(canvas, qrString, {
98
- errorCorrectionLevel: 'H',
99
- margin: 2,
100
- width: 500,
101
- color: {
102
- dark: '#000000',
103
- light: '#ffffff'
104
- }
105
- });
106
- if (this.config.logoPath && fs.existsSync(this.config.logoPath)) {
107
- const logo = await loadImage(this.config.logoPath);
108
- const logoSize = canvas.width * 0.2;
109
- const logoPosition = (canvas.width - logoSize) / 2;
110
-
111
- ctx.fillStyle = '#FFFFFF';
112
- ctx.fillRect(logoPosition - 5, logoPosition - 5, logoSize + 10, logoSize + 10);
113
- ctx.drawImage(logo, logoPosition, logoPosition, logoSize, logoSize);
114
- }
115
- return canvas.toBuffer('image/png');
116
- } catch (error) {
117
- throw new Error('Gagal generate QR: ' + error.message);
118
- }
119
- }
120
-
121
- /**
122
- * Membaca QR dari file kemudian generate QR baru dengan nominal
123
- * @param {number} amount - Nominal pembayaran
124
- * @param {string} qrImagePath - Path ke gambar QR (opsional)
125
- * @returns {Promise<{qrString: string, qrBuffer: Buffer}>}
126
- */
127
- async generateQRFromImage(amount, qrImagePath = null) {
128
- try {
129
- // Jika path gambar disediakan, baca dari path tersebut
130
- if (qrImagePath) {
131
- await this.readQRFromImage(qrImagePath);
132
- }
133
- // Jika tidak ada path dan baseQrString kosong, gunakan default
134
- else if (!this.config.baseQrString) {
135
- await this.readDefaultQR();
136
- }
137
-
138
- // Generate QR string dengan nominal
139
- const qrString = this.generateQrString(amount);
140
-
141
- // Generate QR image
142
- const qrBuffer = await this.generateQRWithLogo(qrString);
143
-
144
- return {
145
- qrString,
146
- qrBuffer
147
- };
148
- } catch (error) {
149
- throw new Error('Gagal generate QR dari gambar: ' + error.message);
150
- }
151
- }
152
-
153
- generateQrString(amount) {
154
- try {
155
- if (!amount || amount <= 0) {
156
- throw new Error('Nominal harus lebih besar dari 0');
157
- }
158
-
159
- if (!this.config.baseQrString) {
160
- throw new Error('BaseQrString tidak tersedia. Gunakan readQRFromImage terlebih dahulu atau berikan baseQrString pada config.');
161
- }
162
-
163
- if (!this.config.baseQrString.includes("5802ID")) {
164
- throw new Error("Format QRIS tidak valid");
165
- }
166
-
167
- const finalAmount = Math.floor(amount);
168
- const qrisBase = this.config.baseQrString.slice(0, -4).replace("010211", "010212");
169
- const nominalStr = finalAmount.toString();
170
- const nominalTag = `54${nominalStr.length.toString().padStart(2, '0')}${nominalStr}`;
171
- const insertPosition = qrisBase.indexOf("5802ID");
172
- const qrisWithNominal = qrisBase.slice(0, insertPosition) + nominalTag + qrisBase.slice(insertPosition);
173
- const checksum = this.calculateCRC16(qrisWithNominal);
174
-
175
- return qrisWithNominal + checksum;
176
- } catch (error) {
177
- throw new Error('Gagal generate string QRIS: ' + error.message);
178
- }
179
- }
180
-
181
- calculateCRC16(str) {
182
- try {
183
- if (!str) {
184
- throw new Error('String tidak boleh kosong');
185
- }
186
-
187
- let crc = 0xFFFF;
188
- for (let i = 0; i < str.length; i++) {
189
- crc ^= str.charCodeAt(i) << 8;
190
- for (let j = 0; j < 8; j++) {
191
- crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1);
192
- }
193
- crc &= 0xFFFF;
194
- }
195
- return crc.toString(16).toUpperCase().padStart(4, '0');
196
- } catch (error) {
197
- throw new Error('Gagal kalkulasi CRC16: ' + error.message);
198
- }
199
- }
200
- }
201
-
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
+
202
211
  export default QRISGenerator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wahdx-api",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Package untuk generate QRIS dan cek payment status secara realtime dengan API OrderKuota dari https://api.wahdx.co",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -38,7 +38,6 @@
38
38
  "dependencies": {
39
39
  "@dotenvx/dotenvx": "^1.47.5",
40
40
  "axios": "^1.10.0",
41
- "canvas": "^3.1.2",
42
41
  "jsqr": "^1.4.0",
43
42
  "moment": "^2.29.4",
44
43
  "pdfkit": "^0.13.0",