dra-qris-api 1.0.6 → 1.0.8
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.
Potentially problematic release.
This version of dra-qris-api might be problematic. Click here for more details.
- package/README.md +253 -0
- package/dist/chunk-4NA4NJ4G.js +390 -0
- package/dist/chunk-CSCJMH5H.js +363 -0
- package/dist/chunk-EBJCGUAH.js +7 -0
- package/dist/chunk-G7UHT62G.js +56 -0
- package/dist/chunk-XOTK7GV5.js +240 -0
- package/dist/chunk-Z7AZPJL4.js +545 -0
- package/dist/index.cjs +1623 -0
- package/dist/index.js +34 -0
- package/dist/lib/createQris.cjs +278 -0
- package/dist/lib/createQris.js +16 -0
- package/dist/lib/globalPending.cjs +26 -0
- package/dist/lib/globalPending.js +6 -0
- package/dist/lib/handlePaymentProof.cjs +1373 -0
- package/dist/lib/handlePaymentProof.js +14 -0
- package/dist/lib/merchantBalanceManager.cjs +423 -0
- package/dist/lib/merchantBalanceManager.js +6 -0
- package/dist/lib/securityValidator.cjs +578 -0
- package/dist/lib/securityValidator.js +6 -0
- package/dist/lib/submitProof.cjs +85 -0
- package/dist/lib/submitProof.js +6 -0
- package/package.json +6 -5
package/README.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# DRA QRIS API
|
|
4
|
+
|
|
5
|
+
## QRIS Dinamis Generator + Auto Payment Handler
|
|
6
|
+
|
|
7
|
+
Library Node.js untuk membuat QRIS, membaca bukti transfer secara otomatis, dan memverifikasi pembayaran melalui API.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img src="https://img.shields.io/badge/dra--qris--api-v1.0.4-purple?logo=npm" alt="Version"/>
|
|
13
|
+
<img src="https://img.shields.io/badge/Node.js-16%2B-green?logo=node.js" alt="Node.js"/>
|
|
14
|
+
<img src="https://img.shields.io/badge/OCR-Tesseract-blue?logo=tesseract" alt="OCR"/>
|
|
15
|
+
<img src="https://img.shields.io/badge/API-Secure-orange?logo=shield" alt="Secure"/>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## ⚡ Fitur Utama
|
|
23
|
+
|
|
24
|
+
- ⚡ Generate QRIS dinamis otomatis
|
|
25
|
+
- 🔍 Membaca foto bukti transfer otomatis
|
|
26
|
+
- 🤖 Auto Payment Handler untuk bot WhatsApp (untuk saat ini hanya pada bot WhatsApp)
|
|
27
|
+
- 🔐 HMAC Signature SHA-256 untuk keamanan request
|
|
28
|
+
- 🗂️ Sangat mudah diintegrasikan ke bot / server mana pun
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 🛠️ Instalasi
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
npm i dra-qris-api
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 📁 Struktur Fungsi
|
|
39
|
+
|
|
40
|
+
Library ini menyediakan 4 fitur utama:
|
|
41
|
+
|
|
42
|
+
<table>
|
|
43
|
+
<thead>
|
|
44
|
+
<tr>
|
|
45
|
+
<th>Fungsi</th>
|
|
46
|
+
<th>Keterangan</th>
|
|
47
|
+
</tr>
|
|
48
|
+
</thead>
|
|
49
|
+
<tbody>
|
|
50
|
+
<tr>
|
|
51
|
+
<td><code>createQris()</code></td>
|
|
52
|
+
<td>Membuat QRIS dan mengembalikan image buffer</td>
|
|
53
|
+
</tr>
|
|
54
|
+
<tr>
|
|
55
|
+
<td><code>submitProof()</code></td>
|
|
56
|
+
<td>Mengirim hasil OCR untuk verifikasi ke API</td>
|
|
57
|
+
</tr>
|
|
58
|
+
<tr>
|
|
59
|
+
<td><code>handlePaymentProof()</code></td>
|
|
60
|
+
<td>Handler otomatis untuk bukti transfer (gambar)</td>
|
|
61
|
+
</tr>
|
|
62
|
+
<tr>
|
|
63
|
+
<td><code>globalPending</code></td>
|
|
64
|
+
<td>Store transaksi yang sedang menunggu pembayaran</td>
|
|
65
|
+
</tr>
|
|
66
|
+
</tbody>
|
|
67
|
+
</table>
|
|
68
|
+
|
|
69
|
+
## 🧠 Arsitektur Singkat
|
|
70
|
+
|
|
71
|
+
```mermaid
|
|
72
|
+
flowchart TD
|
|
73
|
+
A["User Kirim Command Order"] --> B["createQris()"]
|
|
74
|
+
B --> C["Generate QRIS + ID"]
|
|
75
|
+
C --> D["Simpan ke globalPending"]
|
|
76
|
+
D --> E["User Kirim Bukti Transfer"]
|
|
77
|
+
E --> F["handlePaymentProof()"]
|
|
78
|
+
F --> G["OCR Tesseract"]
|
|
79
|
+
G --> H["submitProof()"]
|
|
80
|
+
H --> I{"Status Paid?"}
|
|
81
|
+
I -->|YES| J["Success Callback (bot handle)"]
|
|
82
|
+
I -->|NO| K["Reply: Pembayaran belum valid"]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 🚀 Cara Penggunaan
|
|
86
|
+
|
|
87
|
+
### 1. Import Function
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
const {
|
|
91
|
+
createQris,
|
|
92
|
+
submitProof,
|
|
93
|
+
handlePaymentProof,
|
|
94
|
+
globalPending,
|
|
95
|
+
} = require("dra-qris-api");
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2. Generate QRIS
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
const { id, buffer } = await createQris({
|
|
102
|
+
nominal: 15000,
|
|
103
|
+
merchantName: "NAMA MERCHANT",
|
|
104
|
+
qris: "STRING_QRIS",
|
|
105
|
+
apikey: "API_KEY", // Hubungi admin atau kunjungi https://api.denayrestapi.xyz untuk mendapatkan apikey
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Simpan transaksi:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
globalPending[userId] = {
|
|
113
|
+
id,
|
|
114
|
+
nominal: 15000,
|
|
115
|
+
merchantName: "NAMA MERCHANT",
|
|
116
|
+
description: "Pembelian Paket Premium",
|
|
117
|
+
meta: { type: "premium" },
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 3. Handler Bukti Transfer
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
if (m.message?.imageMessage) {
|
|
125
|
+
const result = await handlePaymentProof({
|
|
126
|
+
m,
|
|
127
|
+
NekonoidF,
|
|
128
|
+
reply,
|
|
129
|
+
botname: "MyBot",
|
|
130
|
+
apiKey: "API_KEY_KAMU", // Hubungi admin atau kunjungi https://api.denayrestapi.xyz untuk mendapatkan apikey
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (!result) return; // gambar biasa bukan bukti tf
|
|
134
|
+
|
|
135
|
+
if (result.status === "paid") {
|
|
136
|
+
await reply("🎉 Pembayaran berhasil diterima!");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 4. Struktur Return dari handlePaymentProof()
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
{
|
|
145
|
+
status: "paid",
|
|
146
|
+
ocrAmount: 15000,
|
|
147
|
+
ocrText: "...hasil bukti transfer...",
|
|
148
|
+
pending: {
|
|
149
|
+
id: "...",
|
|
150
|
+
nominal: 15000,
|
|
151
|
+
merchantName: "NAMA MERCHANT",
|
|
152
|
+
description: "Pembelian Paket Premium",
|
|
153
|
+
meta: {...},
|
|
154
|
+
},
|
|
155
|
+
apiResult: {...}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## 📦 Fitur: globalPending
|
|
160
|
+
|
|
161
|
+
Contoh penyimpanan transaksi:
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
globalPending["628xxx@s.whatsapp.net"] = {
|
|
165
|
+
id: "PAY123",
|
|
166
|
+
nominal: 20000,
|
|
167
|
+
description: "Topup Saldo",
|
|
168
|
+
meta: {
|
|
169
|
+
type: "topup",
|
|
170
|
+
amount: 20000,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Menghapus transaksi:
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
delete globalPending[m.sender];
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Implementasi Lengkap
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
if (m.message?.imageMessage) {
|
|
185
|
+
const result = await handlePaymentProof({
|
|
186
|
+
m,
|
|
187
|
+
NekonoidF: conn,
|
|
188
|
+
reply: (txt) => conn.sendMessage(m.chat, { text: txt }),
|
|
189
|
+
botname: "BotDigital",
|
|
190
|
+
apiKey: "API_KEY", // Hubungi admin atau kunjungi https://api.denayrestapi.xyz untuk mendapatkan apikey
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (result?.status === "paid") {
|
|
194
|
+
if (result.pending.meta.product === "A") {
|
|
195
|
+
// kasih produk
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
switch (command) {
|
|
201
|
+
case "buy":
|
|
202
|
+
{
|
|
203
|
+
const basePrice = 20000;
|
|
204
|
+
|
|
205
|
+
// Kode unik untuk mencegah gambar palsu
|
|
206
|
+
const uniqueCode = Math.floor(Math.random() * 100); // 0 - 99
|
|
207
|
+
const nominal = basePrice + uniqueCode;
|
|
208
|
+
|
|
209
|
+
const { id, buffer } = await createQris({
|
|
210
|
+
nominal,
|
|
211
|
+
merchantName: "TOKO SAYA",
|
|
212
|
+
qris: "QRIS STRING",
|
|
213
|
+
apikey: "API_KEY", // Hubungi admin atau kunjungi https://api.denayrestapi.xyz untuk mendapatkan apikey
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
globalPending[m.sender] = {
|
|
217
|
+
id,
|
|
218
|
+
nominal,
|
|
219
|
+
description: "Pembelian Produk A",
|
|
220
|
+
meta: { product: "A", uniqueCode },
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
await conn.sendMessage(m.chat, {
|
|
224
|
+
image: Buffer.from(buffer),
|
|
225
|
+
caption: `💳 *Pembayaran Produk A*\nNominal: Rp${nominal}\n\nSilakan scan QRIS dan unggah bukti transfer.`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## 🛡️ Keamanan
|
|
233
|
+
|
|
234
|
+
- Semua request memakai HMAC-SHA256 signature
|
|
235
|
+
- Timestamp + request-id otomatis
|
|
236
|
+
|
|
237
|
+
## NOTE
|
|
238
|
+
|
|
239
|
+
> Ini hanya menggunakan metode validasi pembayaran via bukti transfer, tidak melakukan pembayaran otomatis layaknya API QRIS langsung. Silahkan gunakan API QRIS langsung jika cara ini terbilang rumit, cara ini hanya sebagai alternatif.
|
|
240
|
+
|
|
241
|
+
## 👨💻 Sosial Media Kreator
|
|
242
|
+
|
|
243
|
+
<p align="center">
|
|
244
|
+
<a href="https://github.com/irukadevsindie">
|
|
245
|
+
<img src="https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white"/>
|
|
246
|
+
</a>
|
|
247
|
+
<a href="https://t.me/irukaid">
|
|
248
|
+
<img src="https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white"/>
|
|
249
|
+
</a>
|
|
250
|
+
<a href="https://instagram.com/irukadevs.id">
|
|
251
|
+
<img src="https://img.shields.io/badge/Instagram-E4405F?style=for-the-badge&logo=instagram&logoColor=white"/>
|
|
252
|
+
</a>
|
|
253
|
+
</p>
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// lib/merchantBalanceManager.js
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
var MerchantBalanceManager = class {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.balances = /* @__PURE__ */ new Map();
|
|
7
|
+
this.mutations = /* @__PURE__ */ new Map();
|
|
8
|
+
this.transactions = /* @__PURE__ */ new Map();
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Hash API key untuk privacy
|
|
12
|
+
*/
|
|
13
|
+
hashApiKey(apiKey) {
|
|
14
|
+
return crypto.createHash("sha256").update(apiKey).digest("hex").substring(0, 16);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Update merchant balance setelah payment berhasil
|
|
18
|
+
*/
|
|
19
|
+
async updateBalance({
|
|
20
|
+
apiKey,
|
|
21
|
+
merchantName,
|
|
22
|
+
transactionId,
|
|
23
|
+
amount,
|
|
24
|
+
description
|
|
25
|
+
}) {
|
|
26
|
+
const merchantHash = this.hashApiKey(apiKey);
|
|
27
|
+
const key = `${merchantHash}:${merchantName}`;
|
|
28
|
+
let balanceData = this.balances.get(key) || {
|
|
29
|
+
merchantHash,
|
|
30
|
+
merchantName,
|
|
31
|
+
balance: 0,
|
|
32
|
+
totalTransactions: 0,
|
|
33
|
+
totalIncome: 0,
|
|
34
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
35
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
36
|
+
};
|
|
37
|
+
const balanceBefore = balanceData.balance;
|
|
38
|
+
const balanceAfter = balanceBefore + amount;
|
|
39
|
+
balanceData.balance = balanceAfter;
|
|
40
|
+
balanceData.totalTransactions += 1;
|
|
41
|
+
balanceData.totalIncome += amount;
|
|
42
|
+
balanceData.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
43
|
+
this.balances.set(key, balanceData);
|
|
44
|
+
const mutation = {
|
|
45
|
+
transactionId,
|
|
46
|
+
amount,
|
|
47
|
+
description,
|
|
48
|
+
balanceBefore,
|
|
49
|
+
balanceAfter,
|
|
50
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
51
|
+
};
|
|
52
|
+
const mutationKey = `${key}:${Date.now()}_${transactionId}`;
|
|
53
|
+
if (!this.mutations.has(key)) {
|
|
54
|
+
this.mutations.set(key, []);
|
|
55
|
+
}
|
|
56
|
+
this.mutations.get(key).push(mutation);
|
|
57
|
+
this.transactions.set(transactionId, {
|
|
58
|
+
merchantHash,
|
|
59
|
+
merchantName,
|
|
60
|
+
amount,
|
|
61
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
merchantName,
|
|
65
|
+
amount,
|
|
66
|
+
balanceBefore,
|
|
67
|
+
balanceAfter,
|
|
68
|
+
timestamp: mutation.timestamp
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get merchant balance
|
|
73
|
+
*/
|
|
74
|
+
async getBalance(apiKey, merchantName = null) {
|
|
75
|
+
const merchantHash = this.hashApiKey(apiKey);
|
|
76
|
+
if (merchantName) {
|
|
77
|
+
const key = `${merchantHash}:${merchantName}`;
|
|
78
|
+
return this.balances.get(key) || null;
|
|
79
|
+
}
|
|
80
|
+
const results = [];
|
|
81
|
+
for (const [key, value] of this.balances.entries()) {
|
|
82
|
+
if (key.startsWith(merchantHash + ":")) {
|
|
83
|
+
results.push(value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get balance mutations (transaction history)
|
|
90
|
+
*/
|
|
91
|
+
async getMutations(apiKey, merchantName = null, limit = 50) {
|
|
92
|
+
const merchantHash = this.hashApiKey(apiKey);
|
|
93
|
+
if (merchantName) {
|
|
94
|
+
const key = `${merchantHash}:${merchantName}`;
|
|
95
|
+
const mutations = this.mutations.get(key) || [];
|
|
96
|
+
return mutations.slice(-limit).reverse();
|
|
97
|
+
}
|
|
98
|
+
const allMutations = [];
|
|
99
|
+
for (const [key, value] of this.mutations.entries()) {
|
|
100
|
+
if (key.startsWith(merchantHash + ":")) {
|
|
101
|
+
allMutations.push(...value);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
allMutations.sort(
|
|
105
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
106
|
+
);
|
|
107
|
+
return allMutations.slice(0, limit);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get merchant statistics
|
|
111
|
+
*/
|
|
112
|
+
async getMerchantStats(apiKey, merchantName = null) {
|
|
113
|
+
const merchantHash = this.hashApiKey(apiKey);
|
|
114
|
+
if (merchantName) {
|
|
115
|
+
const key = `${merchantHash}:${merchantName}`;
|
|
116
|
+
const balance = this.balances.get(key);
|
|
117
|
+
if (!balance) {
|
|
118
|
+
return {
|
|
119
|
+
totalTransactions: 0,
|
|
120
|
+
totalIncome: 0,
|
|
121
|
+
balance: 0,
|
|
122
|
+
merchantName
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
totalTransactions: balance.totalTransactions,
|
|
127
|
+
totalIncome: balance.totalIncome,
|
|
128
|
+
balance: balance.balance,
|
|
129
|
+
merchantName: balance.merchantName,
|
|
130
|
+
createdAt: balance.createdAt,
|
|
131
|
+
updatedAt: balance.updatedAt
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
let totalTransactions = 0;
|
|
135
|
+
let totalIncome = 0;
|
|
136
|
+
let totalBalance = 0;
|
|
137
|
+
let merchantCount = 0;
|
|
138
|
+
for (const [key, value] of this.balances.entries()) {
|
|
139
|
+
if (key.startsWith(merchantHash + ":")) {
|
|
140
|
+
totalTransactions += value.totalTransactions;
|
|
141
|
+
totalIncome += value.totalIncome;
|
|
142
|
+
totalBalance += value.balance;
|
|
143
|
+
merchantCount++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
merchantCount,
|
|
148
|
+
totalTransactions,
|
|
149
|
+
totalIncome,
|
|
150
|
+
totalBalance,
|
|
151
|
+
averageBalance: merchantCount > 0 ? totalBalance / merchantCount : 0
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get transaction history dengan filtering
|
|
156
|
+
*/
|
|
157
|
+
async getTransactionHistory(apiKey, filters = {}) {
|
|
158
|
+
const merchantHash = this.hashApiKey(apiKey);
|
|
159
|
+
const {
|
|
160
|
+
merchantName,
|
|
161
|
+
status,
|
|
162
|
+
startDate,
|
|
163
|
+
endDate,
|
|
164
|
+
minAmount,
|
|
165
|
+
maxAmount,
|
|
166
|
+
limit = 100
|
|
167
|
+
} = filters;
|
|
168
|
+
const mutations = await this.getMutations(apiKey, merchantName, 1e3);
|
|
169
|
+
let filtered = [...mutations];
|
|
170
|
+
if (startDate) {
|
|
171
|
+
const start = new Date(startDate).getTime();
|
|
172
|
+
filtered = filtered.filter(
|
|
173
|
+
(m) => new Date(m.timestamp).getTime() >= start
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (endDate) {
|
|
177
|
+
const end = new Date(endDate).getTime();
|
|
178
|
+
filtered = filtered.filter((m) => new Date(m.timestamp).getTime() <= end);
|
|
179
|
+
}
|
|
180
|
+
if (minAmount) {
|
|
181
|
+
filtered = filtered.filter((m) => m.amount >= minAmount);
|
|
182
|
+
}
|
|
183
|
+
if (maxAmount) {
|
|
184
|
+
filtered = filtered.filter((m) => m.amount <= maxAmount);
|
|
185
|
+
}
|
|
186
|
+
return filtered.slice(0, limit);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Get daily sales summary
|
|
190
|
+
*/
|
|
191
|
+
async getDailySales(apiKey, merchantName = null, date = null) {
|
|
192
|
+
const targetDate = date || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
193
|
+
const mutations = await this.getMutations(apiKey, merchantName, 1e3);
|
|
194
|
+
const dailyMutations = mutations.filter(
|
|
195
|
+
(m) => m.timestamp.startsWith(targetDate)
|
|
196
|
+
);
|
|
197
|
+
const totalSales = dailyMutations.reduce((sum, m) => sum + m.amount, 0);
|
|
198
|
+
const transactionCount = dailyMutations.length;
|
|
199
|
+
return {
|
|
200
|
+
date: targetDate,
|
|
201
|
+
merchantName,
|
|
202
|
+
totalSales,
|
|
203
|
+
transactionCount,
|
|
204
|
+
averageTransaction: transactionCount > 0 ? totalSales / transactionCount : 0,
|
|
205
|
+
transactions: dailyMutations
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get monthly report
|
|
210
|
+
*/
|
|
211
|
+
async getMonthlyReport(apiKey, merchantName = null, year = null, month = null) {
|
|
212
|
+
const now = /* @__PURE__ */ new Date();
|
|
213
|
+
const targetYear = year || now.getFullYear();
|
|
214
|
+
const targetMonth = month || now.getMonth() + 1;
|
|
215
|
+
const monthStr = `${targetYear}-${String(targetMonth).padStart(2, "0")}`;
|
|
216
|
+
const mutations = await this.getMutations(apiKey, merchantName, 1e4);
|
|
217
|
+
const monthlyMutations = mutations.filter(
|
|
218
|
+
(m) => m.timestamp.startsWith(monthStr)
|
|
219
|
+
);
|
|
220
|
+
const totalRevenue = monthlyMutations.reduce((sum, m) => sum + m.amount, 0);
|
|
221
|
+
const transactionCount = monthlyMutations.length;
|
|
222
|
+
const dailyBreakdown = {};
|
|
223
|
+
monthlyMutations.forEach((m) => {
|
|
224
|
+
const day = m.timestamp.split("T")[0];
|
|
225
|
+
if (!dailyBreakdown[day]) {
|
|
226
|
+
dailyBreakdown[day] = {
|
|
227
|
+
date: day,
|
|
228
|
+
totalSales: 0,
|
|
229
|
+
transactionCount: 0
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
dailyBreakdown[day].totalSales += m.amount;
|
|
233
|
+
dailyBreakdown[day].transactionCount += 1;
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
year: targetYear,
|
|
237
|
+
month: targetMonth,
|
|
238
|
+
merchantName,
|
|
239
|
+
totalRevenue,
|
|
240
|
+
transactionCount,
|
|
241
|
+
averageTransaction: transactionCount > 0 ? totalRevenue / transactionCount : 0,
|
|
242
|
+
dailyBreakdown: Object.values(dailyBreakdown).sort(
|
|
243
|
+
(a, b) => a.date.localeCompare(b.date)
|
|
244
|
+
)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Export data untuk backup/reporting
|
|
249
|
+
*/
|
|
250
|
+
async exportData(apiKey, format = "json") {
|
|
251
|
+
const merchantHash = this.hashApiKey(apiKey);
|
|
252
|
+
const balances = await this.getBalance(apiKey);
|
|
253
|
+
const mutations = await this.getMutations(apiKey, null, 1e4);
|
|
254
|
+
const stats = await this.getMerchantStats(apiKey);
|
|
255
|
+
const data = {
|
|
256
|
+
exportDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
257
|
+
merchantHash,
|
|
258
|
+
balances,
|
|
259
|
+
mutations,
|
|
260
|
+
stats
|
|
261
|
+
};
|
|
262
|
+
if (format === "json") {
|
|
263
|
+
return JSON.stringify(data, null, 2);
|
|
264
|
+
}
|
|
265
|
+
if (format === "csv") {
|
|
266
|
+
let csv = "Date,Merchant,Amount,Balance Before,Balance After,Description\n";
|
|
267
|
+
mutations.forEach((m) => {
|
|
268
|
+
const balance = balances.find((b) => b.merchantName === m.merchantName);
|
|
269
|
+
csv += `${m.timestamp},${(balance == null ? void 0 : balance.merchantName) || "N/A"},${m.amount},${m.balanceBefore},${m.balanceAfter},"${m.description}"
|
|
270
|
+
`;
|
|
271
|
+
});
|
|
272
|
+
return csv;
|
|
273
|
+
}
|
|
274
|
+
return data;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Reset balance (untuk testing atau koreksi)
|
|
278
|
+
*/
|
|
279
|
+
async resetBalance(apiKey, merchantName) {
|
|
280
|
+
const merchantHash = this.hashApiKey(apiKey);
|
|
281
|
+
const key = `${merchantHash}:${merchantName}`;
|
|
282
|
+
this.balances.delete(key);
|
|
283
|
+
this.mutations.delete(key);
|
|
284
|
+
return {
|
|
285
|
+
success: true,
|
|
286
|
+
message: `Balance reset for ${merchantName}`
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Sync dengan API eksternal (untuk production)
|
|
291
|
+
*/
|
|
292
|
+
async syncWithAPI(apiKey, baseURL) {
|
|
293
|
+
var _a;
|
|
294
|
+
try {
|
|
295
|
+
const balanceResp = await axios.get(`${baseURL}/api/qris?op=balance`, {
|
|
296
|
+
headers: { "x-api-key": apiKey }
|
|
297
|
+
});
|
|
298
|
+
const mutationsResp = await axios.get(
|
|
299
|
+
`${baseURL}/api/qris?op=balance-mutations&limit=100`,
|
|
300
|
+
{
|
|
301
|
+
headers: { "x-api-key": apiKey }
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
if (balanceResp.data.balance) {
|
|
305
|
+
const balance = balanceResp.data.balance;
|
|
306
|
+
const merchantHash = this.hashApiKey(apiKey);
|
|
307
|
+
const key = `${merchantHash}:${balance.merchantName}`;
|
|
308
|
+
this.balances.set(key, balance);
|
|
309
|
+
}
|
|
310
|
+
if (mutationsResp.data.mutations) {
|
|
311
|
+
const mutations = mutationsResp.data.mutations;
|
|
312
|
+
mutations.forEach((mutation) => {
|
|
313
|
+
var _a2;
|
|
314
|
+
const merchantHash = this.hashApiKey(apiKey);
|
|
315
|
+
const merchantName = ((_a2 = mutation.description.match(/from (.+)$/)) == null ? void 0 : _a2[1]) || "Unknown";
|
|
316
|
+
const key = `${merchantHash}:${merchantName}`;
|
|
317
|
+
if (!this.mutations.has(key)) {
|
|
318
|
+
this.mutations.set(key, []);
|
|
319
|
+
}
|
|
320
|
+
this.mutations.get(key).push(mutation);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
success: true,
|
|
325
|
+
synced: {
|
|
326
|
+
balances: 1,
|
|
327
|
+
mutations: ((_a = mutationsResp.data.mutations) == null ? void 0 : _a.length) || 0
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
} catch (err) {
|
|
331
|
+
console.error("Sync error:", err);
|
|
332
|
+
return {
|
|
333
|
+
success: false,
|
|
334
|
+
error: err.message
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Get top merchants by revenue
|
|
340
|
+
*/
|
|
341
|
+
async getTopMerchants(apiKey, limit = 10) {
|
|
342
|
+
const balances = await this.getBalance(apiKey);
|
|
343
|
+
return balances.sort((a, b) => b.totalIncome - a.totalIncome).slice(0, limit).map((balance, index) => ({
|
|
344
|
+
rank: index + 1,
|
|
345
|
+
merchantName: balance.merchantName,
|
|
346
|
+
totalIncome: balance.totalIncome,
|
|
347
|
+
totalTransactions: balance.totalTransactions,
|
|
348
|
+
averageTransaction: balance.totalIncome / balance.totalTransactions,
|
|
349
|
+
currentBalance: balance.balance
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Calculate growth rate
|
|
354
|
+
*/
|
|
355
|
+
async getGrowthRate(apiKey, merchantName, days = 7) {
|
|
356
|
+
const mutations = await this.getMutations(apiKey, merchantName, 1e3);
|
|
357
|
+
const now = /* @__PURE__ */ new Date();
|
|
358
|
+
const periodStart = new Date(now.getTime() - days * 24 * 60 * 60 * 1e3);
|
|
359
|
+
const recentMutations = mutations.filter(
|
|
360
|
+
(m) => new Date(m.timestamp).getTime() >= periodStart.getTime()
|
|
361
|
+
);
|
|
362
|
+
const currentPeriodRevenue = recentMutations.reduce(
|
|
363
|
+
(sum, m) => sum + m.amount,
|
|
364
|
+
0
|
|
365
|
+
);
|
|
366
|
+
const prevPeriodStart = new Date(
|
|
367
|
+
periodStart.getTime() - days * 24 * 60 * 60 * 1e3
|
|
368
|
+
);
|
|
369
|
+
const prevMutations = mutations.filter((m) => {
|
|
370
|
+
const ts = new Date(m.timestamp).getTime();
|
|
371
|
+
return ts >= prevPeriodStart.getTime() && ts < periodStart.getTime();
|
|
372
|
+
});
|
|
373
|
+
const previousPeriodRevenue = prevMutations.reduce(
|
|
374
|
+
(sum, m) => sum + m.amount,
|
|
375
|
+
0
|
|
376
|
+
);
|
|
377
|
+
const growthRate = previousPeriodRevenue > 0 ? (currentPeriodRevenue - previousPeriodRevenue) / previousPeriodRevenue * 100 : 0;
|
|
378
|
+
return {
|
|
379
|
+
period: `${days} days`,
|
|
380
|
+
currentPeriodRevenue,
|
|
381
|
+
previousPeriodRevenue,
|
|
382
|
+
growthRate: parseFloat(growthRate.toFixed(2)),
|
|
383
|
+
trend: growthRate > 0 ? "up" : growthRate < 0 ? "down" : "stable"
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
export {
|
|
389
|
+
MerchantBalanceManager
|
|
390
|
+
};
|