wahdx-api 1.0.1 → 1.0.3
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 +22 -0
- package/dist/{index.js → index.cjs} +5 -10
- package/dist/index.mjs +56 -0
- package/dist/payment-checker.cjs +90 -0
- package/dist/qr-generator.cjs +208 -0
- package/dist/{qr-generator.js → qr-generator.mjs} +26 -17
- package/dist/receipt-generator.cjs +130 -0
- package/package.json +11 -4
- /package/dist/{payment-checker.js → payment-checker.mjs} +0 -0
- /package/dist/{receipt-generator.js → receipt-generator.mjs} +0 -0
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ Package untuk generate QRIS dan cek payment status secara realtime dengan API Or
|
|
|
8
8
|
- Generate kode QR dengan nominal tertentu
|
|
9
9
|
- Cek status pembayaran secara realtime
|
|
10
10
|
- Generate bukti transaksi (receipt)
|
|
11
|
+
- Kompatibel di semua platform (Windows, Linux, Mac)
|
|
11
12
|
|
|
12
13
|
## Instalasi
|
|
13
14
|
|
|
@@ -162,6 +163,27 @@ A: Anda bisa membeli tokenKey di halaman utama [https://api.wahdx.co](https://ap
|
|
|
162
163
|
### Q: Bagaimana cara mendapatkan kredensial API OrderKuota?
|
|
163
164
|
A: Silahkan kunjungi dokumentasi api [https://api.wahdx.co/api-docs](https://api.wahdx.co/api-docs) untuk mendapatkan token pada akun orderkuota anda.
|
|
164
165
|
|
|
166
|
+
### Q: Apakah module ini bisa digunakan di project CommonJS?
|
|
167
|
+
A: Ya! Package ini mendukung dual module system (ESM dan CommonJS). Anda bisa menggunakan dengan dua cara:
|
|
168
|
+
|
|
169
|
+
1. Dengan ES Modules (dalam file .js dengan `type: "module"` di package.json):
|
|
170
|
+
```javascript
|
|
171
|
+
import QRISPayment from 'wahdx-api';
|
|
172
|
+
|
|
173
|
+
const qrisPayment = new QRISPayment(config);
|
|
174
|
+
// gunakan qrisPayment
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
2. Dengan CommonJS (dalam file .cjs atau project tanpa `type: "module"`):
|
|
178
|
+
```javascript
|
|
179
|
+
const QRISPayment = require('wahdx-api');
|
|
180
|
+
|
|
181
|
+
const qrisPayment = new QRISPayment(config);
|
|
182
|
+
// gunakan qrisPayment
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Package secara otomatis mendeteksi format yang digunakan dan menyediakan versi yang sesuai.
|
|
186
|
+
|
|
165
187
|
## Lisensi
|
|
166
188
|
|
|
167
189
|
MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const QRISGenerator = require("./qr-generator.cjs");
|
|
2
|
+
const PaymentChecker = require("./payment-checker.cjs");
|
|
3
|
+
const ReceiptGenerator = require("./receipt-generator.cjs");
|
|
4
4
|
|
|
5
5
|
class QRISPayment {
|
|
6
6
|
constructor(config = {}) {
|
|
@@ -24,12 +24,7 @@ class QRISPayment {
|
|
|
24
24
|
* @returns {Promise<{qrString: string, qrBuffer: Buffer}>}
|
|
25
25
|
*/
|
|
26
26
|
async generateQR(amount) {
|
|
27
|
-
|
|
28
|
-
const qrBuffer = await this.qrGenerator.generateQRWithLogo(qrString);
|
|
29
|
-
return {
|
|
30
|
-
qrString,
|
|
31
|
-
qrBuffer
|
|
32
|
-
};
|
|
27
|
+
return await this.qrGenerator.generateQR(amount);
|
|
33
28
|
}
|
|
34
29
|
|
|
35
30
|
/**
|
|
@@ -58,4 +53,4 @@ class QRISPayment {
|
|
|
58
53
|
}
|
|
59
54
|
|
|
60
55
|
// Mengekspor kelas utama
|
|
61
|
-
|
|
56
|
+
module.exports = QRISPayment;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import QRISGenerator from './qr-generator.mjs';
|
|
2
|
+
import PaymentChecker from './payment-checker.mjs';
|
|
3
|
+
import ReceiptGenerator from './receipt-generator.mjs';
|
|
4
|
+
|
|
5
|
+
class QRISPayment {
|
|
6
|
+
constructor(config = {}) {
|
|
7
|
+
this.qrGenerator = new QRISGenerator(config);
|
|
8
|
+
this.paymentChecker = new PaymentChecker(config);
|
|
9
|
+
this.receiptGenerator = new ReceiptGenerator(config);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Membaca QR code dari file gambar dan mengekstrak baseQrString
|
|
14
|
+
* @param {string} imagePath - Path ke file gambar QR
|
|
15
|
+
* @returns {Promise<string>} - Promise yang menghasilkan baseQrString
|
|
16
|
+
*/
|
|
17
|
+
async readQRCode(imagePath) {
|
|
18
|
+
return await this.qrGenerator.readQRFromImage(imagePath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate QR dengan nominal tertentu
|
|
23
|
+
* @param {number} amount - Nominal pembayaran
|
|
24
|
+
* @returns {Promise<{qrString: string, qrBuffer: Buffer}>}
|
|
25
|
+
*/
|
|
26
|
+
async generateQR(amount) {
|
|
27
|
+
return await this.qrGenerator.generateQR(amount);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Membaca QR dari file gambar dan langsung menghasilkan QR baru dengan nominal
|
|
32
|
+
* @param {number} amount - Nominal pembayaran
|
|
33
|
+
* @param {string} qrImagePath - Path ke gambar QR (opsional)
|
|
34
|
+
* @returns {Promise<{qrString: string, qrBuffer: Buffer}>}
|
|
35
|
+
*/
|
|
36
|
+
async generateQRFromImage(amount, qrImagePath = null) {
|
|
37
|
+
return await this.qrGenerator.generateQRFromImage(amount, qrImagePath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async checkPayment(reference, amount) {
|
|
41
|
+
const result = await this.paymentChecker.checkPaymentStatus(reference, amount);
|
|
42
|
+
if (result.success && result.data.status === 'PAID') {
|
|
43
|
+
const receipt = await this.receiptGenerator.generateReceipt(result.data);
|
|
44
|
+
result.receipt = receipt;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async generateReceipt(transactionData) {
|
|
51
|
+
return await this.receiptGenerator.generateReceipt(transactionData);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Mengekspor kelas utama
|
|
56
|
+
export default QRISPayment;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const axios = require("axios");
|
|
2
|
+
|
|
3
|
+
class PaymentChecker {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
if (!config.tokenKey || !config.auth_username || !config.auth_token) {
|
|
6
|
+
throw new Error('tokenKey, auth_username, dan auth_token harus diisi');
|
|
7
|
+
}
|
|
8
|
+
this.config = {
|
|
9
|
+
tokenKey: config.tokenKey,
|
|
10
|
+
auth_username: config.auth_username,
|
|
11
|
+
auth_token: config.auth_token
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async checkPaymentStatus(reference, amount) {
|
|
16
|
+
try {
|
|
17
|
+
if (!reference || !amount || amount <= 0) {
|
|
18
|
+
throw new Error('Reference dan amount harus diisi dengan benar');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const response = await axios.post(
|
|
22
|
+
'https://api.wahdx.co/api/mutasi-orkut-v2',
|
|
23
|
+
{
|
|
24
|
+
username_orkut: this.config.auth_username,
|
|
25
|
+
token_orkut: this.config.auth_token
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
headers: {
|
|
29
|
+
'tokenKey': this.config.tokenKey,
|
|
30
|
+
'Content-Type': 'application/json'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (!response.data || !response.data.status || !response.data.data) {
|
|
36
|
+
throw new Error('Response tidak valid dari server');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const transactions = response.data.data;
|
|
40
|
+
|
|
41
|
+
const matchingTransactions = transactions.filter(tx => {
|
|
42
|
+
const txAmount = parseInt(tx.amount);
|
|
43
|
+
const txDate = new Date(tx.date);
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const timeDiff = now - txDate;
|
|
46
|
+
|
|
47
|
+
return txAmount === amount &&
|
|
48
|
+
tx.qris === "static" &&
|
|
49
|
+
tx.type === "CR" &&
|
|
50
|
+
timeDiff <= 5 * 60 * 1000;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (matchingTransactions.length > 0) {
|
|
54
|
+
const latestTransaction = matchingTransactions.reduce((latest, current) => {
|
|
55
|
+
const currentDate = new Date(current.date);
|
|
56
|
+
const latestDate = new Date(latest.date);
|
|
57
|
+
return currentDate > latestDate ? current : latest;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
success: true,
|
|
62
|
+
data: {
|
|
63
|
+
status: 'PAID',
|
|
64
|
+
amount: parseInt(latestTransaction.amount),
|
|
65
|
+
reference: latestTransaction.issuer_reff,
|
|
66
|
+
date: latestTransaction.date,
|
|
67
|
+
brand_name: latestTransaction.brand_name,
|
|
68
|
+
buyer_reff: latestTransaction.buyer_reff
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
data: {
|
|
76
|
+
status: 'UNPAID',
|
|
77
|
+
amount: amount,
|
|
78
|
+
reference: reference
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
error: 'Gagal cek status pembayaran: ' + error.message
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = PaymentChecker;
|
|
@@ -0,0 +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
|
+
|
|
208
|
+
module.exports = QRISGenerator;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import QRCode from 'qrcode';
|
|
2
|
-
import { createCanvas, loadImage } from 'canvas';
|
|
3
2
|
import fs from 'fs';
|
|
4
3
|
import sharp from 'sharp';
|
|
5
4
|
import jsQR from 'jsqr';
|
|
@@ -13,7 +12,6 @@ class QRISGenerator {
|
|
|
13
12
|
constructor(config = {}) {
|
|
14
13
|
this.config = {
|
|
15
14
|
baseQrString: config.baseQrString || '',
|
|
16
|
-
logoPath: config.logoPath || null,
|
|
17
15
|
defaultQrPath: config.defaultQrPath || 'QRIS.png'
|
|
18
16
|
};
|
|
19
17
|
}
|
|
@@ -87,14 +85,19 @@ class QRISGenerator {
|
|
|
87
85
|
}
|
|
88
86
|
}
|
|
89
87
|
|
|
90
|
-
|
|
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) {
|
|
91
94
|
try {
|
|
92
95
|
if (!qrString) {
|
|
93
96
|
throw new Error('qrString tidak boleh kosong');
|
|
94
97
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
await QRCode.
|
|
98
|
+
|
|
99
|
+
// Gunakan QRCode.toBuffer langsung tanpa canvas
|
|
100
|
+
const qrBuffer = await QRCode.toBuffer(qrString, {
|
|
98
101
|
errorCorrectionLevel: 'H',
|
|
99
102
|
margin: 2,
|
|
100
103
|
width: 500,
|
|
@@ -103,16 +106,8 @@ class QRISGenerator {
|
|
|
103
106
|
light: '#ffffff'
|
|
104
107
|
}
|
|
105
108
|
});
|
|
106
|
-
|
|
107
|
-
|
|
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');
|
|
109
|
+
|
|
110
|
+
return qrBuffer;
|
|
116
111
|
} catch (error) {
|
|
117
112
|
throw new Error('Gagal generate QR: ' + error.message);
|
|
118
113
|
}
|
|
@@ -139,7 +134,7 @@ class QRISGenerator {
|
|
|
139
134
|
const qrString = this.generateQrString(amount);
|
|
140
135
|
|
|
141
136
|
// Generate QR image
|
|
142
|
-
const qrBuffer = await this.
|
|
137
|
+
const qrBuffer = await this.generateQRImage(qrString);
|
|
143
138
|
|
|
144
139
|
return {
|
|
145
140
|
qrString,
|
|
@@ -150,6 +145,20 @@ class QRISGenerator {
|
|
|
150
145
|
}
|
|
151
146
|
}
|
|
152
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
|
+
|
|
153
162
|
generateQrString(amount) {
|
|
154
163
|
try {
|
|
155
164
|
if (!amount || amount <= 0) {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const PDFDocument = require("pdfkit");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const moment = require("moment");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
class ReceiptGenerator {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async generateReceipt(transactionData) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
try {
|
|
14
|
+
const doc = new PDFDocument({
|
|
15
|
+
size: [300, 450],
|
|
16
|
+
margin: 20,
|
|
17
|
+
layout: 'portrait'
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const fileName = `receipt_${transactionData.reference}_${Date.now()}.pdf`;
|
|
21
|
+
const filePath = `receipts/${fileName}`;
|
|
22
|
+
if (!fs.existsSync('receipts')) {
|
|
23
|
+
fs.mkdirSync('receipts');
|
|
24
|
+
}
|
|
25
|
+
const writeStream = fs.createWriteStream(filePath);
|
|
26
|
+
doc.pipe(writeStream);
|
|
27
|
+
|
|
28
|
+
if (this.config.logoPath && fs.existsSync(this.config.logoPath)) {
|
|
29
|
+
const logoWidth = 40;
|
|
30
|
+
const logoHeight = 40;
|
|
31
|
+
const logoX = 20 + (260 - logoWidth) / 2;
|
|
32
|
+
const logoY = 10;
|
|
33
|
+
doc.image(this.config.logoPath, logoX, logoY, { width: logoWidth, height: logoHeight });
|
|
34
|
+
doc.y = logoY + logoHeight + 4;
|
|
35
|
+
} else {
|
|
36
|
+
doc.moveDown(0.5);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
doc.fontSize(14)
|
|
40
|
+
.fillColor('black')
|
|
41
|
+
.font('Helvetica-Bold');
|
|
42
|
+
const headerText = 'QRIS PAYMENT RECEIPT';
|
|
43
|
+
const headerWidth = doc.widthOfString(headerText);
|
|
44
|
+
doc.text(headerText, 20 + (260 - headerWidth) / 2, doc.y, { width: headerWidth });
|
|
45
|
+
|
|
46
|
+
doc.fontSize(9)
|
|
47
|
+
.font('Helvetica');
|
|
48
|
+
const merchantText = this.config.storeName || 'STORE NAME';
|
|
49
|
+
const merchantWidth = doc.widthOfString(merchantText);
|
|
50
|
+
doc.text(merchantText, 20 + (260 - merchantWidth) / 2, doc.y, { width: merchantWidth });
|
|
51
|
+
|
|
52
|
+
doc.moveDown(0.3);
|
|
53
|
+
const lineY = doc.y;
|
|
54
|
+
doc.moveTo(20, lineY)
|
|
55
|
+
.lineTo(280, lineY)
|
|
56
|
+
.strokeColor('#888').stroke();
|
|
57
|
+
|
|
58
|
+
doc.moveDown(0.5);
|
|
59
|
+
doc.fontSize(9)
|
|
60
|
+
.font('Helvetica-Bold');
|
|
61
|
+
const detailsTitle = 'TRANSACTION DETAILS';
|
|
62
|
+
const detailsTitleWidth = doc.widthOfString(detailsTitle);
|
|
63
|
+
doc.text(detailsTitle, 20 + (260 - detailsTitleWidth) / 2, doc.y, { width: detailsTitleWidth });
|
|
64
|
+
doc.moveDown(0.2);
|
|
65
|
+
|
|
66
|
+
const formattedDate = moment(transactionData.date).format('DD/MM/YYYY HH:mm:ss');
|
|
67
|
+
const details = [
|
|
68
|
+
['Reference', transactionData.reference],
|
|
69
|
+
['Date', formattedDate],
|
|
70
|
+
['Amount', `Rp ${transactionData.amount.toLocaleString('id-ID')}`],
|
|
71
|
+
['Status', transactionData.status],
|
|
72
|
+
['Payment Method', transactionData.brand_name || '-'],
|
|
73
|
+
['Buyer Reference', transactionData.buyer_reff || '-']
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const tableWidth = 240;
|
|
77
|
+
const startX = (300 - tableWidth) / 2;
|
|
78
|
+
const labelWidth = 100;
|
|
79
|
+
const valueWidth = 130;
|
|
80
|
+
let y = doc.y + 5;
|
|
81
|
+
|
|
82
|
+
details.forEach(([label, value], idx) => {
|
|
83
|
+
doc.font('Helvetica-Bold')
|
|
84
|
+
.fontSize(9)
|
|
85
|
+
.fillColor('black')
|
|
86
|
+
.text(label, startX, y, { width: labelWidth, align: 'left' });
|
|
87
|
+
doc.font('Helvetica')
|
|
88
|
+
.fontSize(9)
|
|
89
|
+
.fillColor('black')
|
|
90
|
+
.text(value, startX + labelWidth, y, { width: valueWidth, align: 'right' });
|
|
91
|
+
|
|
92
|
+
y += 14;
|
|
93
|
+
doc.moveTo(startX, y)
|
|
94
|
+
.lineTo(startX + labelWidth + valueWidth, y)
|
|
95
|
+
.strokeColor('#e0e0e0').stroke();
|
|
96
|
+
y += 2;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
y += 6;
|
|
100
|
+
doc.moveTo(20, y)
|
|
101
|
+
.lineTo(280, y)
|
|
102
|
+
.strokeColor('#888').stroke();
|
|
103
|
+
|
|
104
|
+
y += 12;
|
|
105
|
+
doc.text('Thank you for your payment!', 0, y, { align: 'right', width: 260 });
|
|
106
|
+
y += 12;
|
|
107
|
+
doc.text(`Receipt No: ${transactionData.reference}`, 0, y, { align: 'right', width: 260 });
|
|
108
|
+
|
|
109
|
+
doc.end();
|
|
110
|
+
|
|
111
|
+
writeStream.on('finish', () => {
|
|
112
|
+
resolve({
|
|
113
|
+
success: true,
|
|
114
|
+
filePath: filePath,
|
|
115
|
+
fileName: fileName
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
writeStream.on('error', (error) => {
|
|
120
|
+
reject(error);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
} catch (error) {
|
|
124
|
+
reject(error);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = ReceiptGenerator;
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wahdx-api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Package untuk generate QRIS dan cek payment status secara realtime dengan API OrderKuota dari https://api.wahdx.co",
|
|
5
|
-
"main": "dist/index.
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"require": "./dist/index.cjs",
|
|
10
|
+
"import": "./dist/index.mjs"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
6
13
|
"author": "wahdalo",
|
|
7
14
|
"license": "MIT",
|
|
8
15
|
"type": "module",
|
|
@@ -17,7 +24,8 @@
|
|
|
17
24
|
],
|
|
18
25
|
"scripts": {
|
|
19
26
|
"test": "node tes.js",
|
|
20
|
-
"
|
|
27
|
+
"test:cjs": "node tes-cjs.cjs",
|
|
28
|
+
"start": "node dist/index.mjs",
|
|
21
29
|
"build": "node scripts/build.js",
|
|
22
30
|
"prepublishOnly": "npm run build",
|
|
23
31
|
"pack": "npm pack --dry-run"
|
|
@@ -30,7 +38,6 @@
|
|
|
30
38
|
"dependencies": {
|
|
31
39
|
"@dotenvx/dotenvx": "^1.47.5",
|
|
32
40
|
"axios": "^1.10.0",
|
|
33
|
-
"canvas": "^3.1.2",
|
|
34
41
|
"jsqr": "^1.4.0",
|
|
35
42
|
"moment": "^2.29.4",
|
|
36
43
|
"pdfkit": "^0.13.0",
|
|
File without changes
|
|
File without changes
|