nodebb-plugin-niki-loyalty 1.0.17 → 1.0.18
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/library.js +234 -189
- package/package.json +1 -1
package/library.js
CHANGED
|
@@ -3,248 +3,293 @@
|
|
|
3
3
|
const db = require.main.require('./src/database');
|
|
4
4
|
const user = require.main.require('./src/user');
|
|
5
5
|
const routeHelpers = require.main.require('./src/controllers/helpers');
|
|
6
|
+
const nconf = require.main.require('nconf');
|
|
6
7
|
|
|
7
8
|
const Plugin = {};
|
|
8
9
|
|
|
10
|
+
// =========================
|
|
11
|
+
// AYARLAR
|
|
12
|
+
// =========================
|
|
9
13
|
const SETTINGS = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
pointsPerHeartbeat: 500,
|
|
15
|
+
dailyCap: 2500000,
|
|
16
|
+
coffeeCost: 250,
|
|
13
17
|
};
|
|
14
18
|
|
|
19
|
+
// ✅ TEST: sınırsız kullanım (puan kontrolünü kapatmak için true)
|
|
20
|
+
const TEST_MODE_UNLIMITED = false;
|
|
21
|
+
|
|
15
22
|
// =========================
|
|
16
23
|
// JSON SAFE HELPERS
|
|
17
24
|
// =========================
|
|
18
25
|
function safeParseMaybeJson(x) {
|
|
19
|
-
|
|
26
|
+
if (x == null) return null;
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
// bazı DB’lerde object dönebilir
|
|
29
|
+
if (typeof x === 'object') return x;
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
+
if (typeof x === 'string') {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(x);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// "[object Object]" gibi bozuk kayıtları atla
|
|
36
|
+
return null;
|
|
31
37
|
}
|
|
32
|
-
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
function safeStringify(obj) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
try {
|
|
44
|
+
return JSON.stringify(obj);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeProfileUrl(userslug) {
|
|
51
|
+
const rp = nconf.get('relative_path') || '';
|
|
52
|
+
if (!userslug) return '';
|
|
53
|
+
return `${rp}/user/${userslug}`;
|
|
41
54
|
}
|
|
42
55
|
|
|
43
|
-
//
|
|
56
|
+
// =========================
|
|
57
|
+
// LOG FONKSİYONLARI
|
|
58
|
+
// =========================
|
|
44
59
|
async function addUserLog(uid, type, amount, desc) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
await db.listTrim(`niki:activity:${uid}`, -50, -1);
|
|
60
|
+
const logEntry = {
|
|
61
|
+
ts: Date.now(),
|
|
62
|
+
type, // 'earn' | 'spend'
|
|
63
|
+
amt: amount,
|
|
64
|
+
txt: desc,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const payload = safeStringify(logEntry);
|
|
68
|
+
if (!payload) return;
|
|
69
|
+
|
|
70
|
+
await db.listAppend(`niki:activity:${uid}`, payload);
|
|
71
|
+
await db.listTrim(`niki:activity:${uid}`, -50, -1);
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
async function addKasaLog(staffUid, customerName, customerUid) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
await db.listTrim('niki:kasa:history', -100, -1);
|
|
75
|
+
const logEntry = {
|
|
76
|
+
ts: Date.now(),
|
|
77
|
+
staff: staffUid,
|
|
78
|
+
cust: customerName, // bazen eski kayıtlarda boş olabilir, endpoint tamamlayacak
|
|
79
|
+
cuid: customerUid,
|
|
80
|
+
amt: SETTINGS.coffeeCost,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const payload = safeStringify(logEntry);
|
|
84
|
+
if (!payload) return;
|
|
85
|
+
|
|
86
|
+
await db.listAppend('niki:kasa:history', payload);
|
|
87
|
+
await db.listTrim('niki:kasa:history', -100, -1);
|
|
75
88
|
}
|
|
76
89
|
|
|
90
|
+
// =========================
|
|
91
|
+
// INIT
|
|
92
|
+
// =========================
|
|
77
93
|
Plugin.init = async function (params) {
|
|
78
|
-
|
|
79
|
-
|
|
94
|
+
const router = params.router;
|
|
95
|
+
const middleware = params.middleware;
|
|
80
96
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
// 1) HEARTBEAT (puan kazanma)
|
|
98
|
+
router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
const uid = req.uid;
|
|
101
|
+
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
102
|
+
const dailyKey = `niki:daily:${uid}:${today}`;
|
|
87
103
|
|
|
88
|
-
|
|
104
|
+
const currentDailyScore = parseInt((await db.getObjectField(dailyKey, 'score')) || 0, 10);
|
|
89
105
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
if (currentDailyScore >= SETTINGS.dailyCap) {
|
|
107
|
+
return res.json({ earned: false, reason: 'daily_cap' });
|
|
108
|
+
}
|
|
93
109
|
|
|
94
|
-
|
|
95
|
-
|
|
110
|
+
await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
|
|
111
|
+
await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
|
|
96
112
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
113
|
+
const newBalance = await user.getUserField(uid, 'niki_points');
|
|
114
|
+
return res.json({ earned: true, points: SETTINGS.pointsPerHeartbeat, total: newBalance });
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return res.status(500).json({ earned: false, reason: 'server_error' });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
103
119
|
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
120
|
+
// 2) WALLET DATA (cüzdan + geçmiş)
|
|
121
|
+
router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
|
|
122
|
+
try {
|
|
123
|
+
const uid = req.uid;
|
|
124
|
+
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
125
|
+
|
|
126
|
+
const [userData, dailyData, historyRaw] = await Promise.all([
|
|
127
|
+
user.getUserFields(uid, ['niki_points']),
|
|
128
|
+
db.getObject(`niki:daily:${uid}:${today}`),
|
|
129
|
+
db.getListRange(`niki:activity:${uid}`, 0, -1),
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
const currentPoints = parseInt(userData?.niki_points || 0, 10);
|
|
133
|
+
const dailyScore = parseInt(dailyData?.score || 0, 10);
|
|
134
|
+
|
|
135
|
+
let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
|
|
136
|
+
if (dailyPercent > 100) dailyPercent = 100;
|
|
137
|
+
|
|
138
|
+
// ✅ history parse güvenli
|
|
139
|
+
const history = (historyRaw || [])
|
|
140
|
+
.map(safeParseMaybeJson)
|
|
141
|
+
.filter(Boolean)
|
|
142
|
+
.reverse();
|
|
143
|
+
|
|
144
|
+
return res.json({
|
|
145
|
+
points: currentPoints,
|
|
146
|
+
dailyScore,
|
|
147
|
+
dailyCap: SETTINGS.dailyCap,
|
|
148
|
+
dailyPercent,
|
|
149
|
+
history,
|
|
150
|
+
});
|
|
151
|
+
} catch (err) {
|
|
152
|
+
return res.status(500).json({
|
|
153
|
+
points: 0,
|
|
154
|
+
dailyScore: 0,
|
|
155
|
+
dailyCap: SETTINGS.dailyCap,
|
|
156
|
+
dailyPercent: 0,
|
|
157
|
+
history: [],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// 3) KASA HISTORY (admin/mod)
|
|
163
|
+
router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
|
|
164
|
+
try {
|
|
165
|
+
const isAdmin = await user.isAdministrator(req.uid);
|
|
166
|
+
const isMod = await user.isGlobalModerator(req.uid);
|
|
167
|
+
if (!isAdmin && !isMod) return res.status(403).json([]);
|
|
168
|
+
|
|
169
|
+
const raw = await db.getListRange('niki:kasa:history', 0, -1);
|
|
170
|
+
|
|
171
|
+
const rows = (raw || [])
|
|
172
|
+
.map(safeParseMaybeJson)
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
.reverse();
|
|
175
|
+
|
|
176
|
+
const enriched = await Promise.all(rows.map(async (item) => {
|
|
177
|
+
if (!item || !item.cuid) return item;
|
|
144
178
|
|
|
145
|
-
// 3. KASA GEÇMİŞİ
|
|
146
|
-
router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
|
|
147
179
|
try {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!item || !item.cuid) return item;
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
const uData = await user.getUserFields(item.cuid, ['picture']);
|
|
166
|
-
return { ...item, picture: uData?.picture };
|
|
167
|
-
} catch (e) {
|
|
168
|
-
return item;
|
|
169
|
-
}
|
|
170
|
-
}));
|
|
171
|
-
|
|
172
|
-
res.json(enrichedHistory);
|
|
173
|
-
} catch (err) {
|
|
174
|
-
// ✅ asla crash yok
|
|
175
|
-
return res.status(500).json([]);
|
|
180
|
+
// ✅ userslug + username + picture al
|
|
181
|
+
const uData = await user.getUserFields(item.cuid, ['username', 'userslug', 'picture']);
|
|
182
|
+
const userslug = uData?.userslug;
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
...item,
|
|
186
|
+
cust: item.cust || uData?.username || 'Bilinmeyen',
|
|
187
|
+
picture: item.picture || uData?.picture || '',
|
|
188
|
+
userslug: userslug || item.userslug || '',
|
|
189
|
+
profileUrl: makeProfileUrl(userslug || item.userslug),
|
|
190
|
+
};
|
|
191
|
+
} catch (e) {
|
|
192
|
+
// tek item patlasa bile endpoint düşmesin
|
|
193
|
+
return item;
|
|
176
194
|
}
|
|
177
|
-
|
|
195
|
+
}));
|
|
178
196
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
197
|
+
return res.json(enriched);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
return res.status(500).json([]);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
184
202
|
|
|
185
|
-
|
|
203
|
+
// 4) QR OLUŞTUR
|
|
204
|
+
router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
|
|
205
|
+
try {
|
|
206
|
+
const uid = req.uid;
|
|
207
|
+
const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
|
|
186
208
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
209
|
+
if (!TEST_MODE_UNLIMITED && points < SETTINGS.coffeeCost) {
|
|
210
|
+
return res.json({ success: false, message: 'Yetersiz Puan' });
|
|
211
|
+
}
|
|
190
212
|
|
|
191
|
-
|
|
192
|
-
} catch (err) {
|
|
193
|
-
return res.status(500).json({ success: false, message: 'Sunucu hatası' });
|
|
194
|
-
}
|
|
195
|
-
});
|
|
213
|
+
const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
196
214
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
const { token } = req.body;
|
|
215
|
+
await db.set(`niki:qr:${token}`, uid);
|
|
216
|
+
await db.expire(`niki:qr:${token}`, 120);
|
|
201
217
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
218
|
+
return res.json({ success: true, token });
|
|
219
|
+
} catch (err) {
|
|
220
|
+
return res.status(500).json({ success: false, message: 'Sunucu hatası' });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
205
223
|
|
|
206
|
-
|
|
207
|
-
|
|
224
|
+
// 5) QR OKUT (admin/mod)
|
|
225
|
+
router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
|
|
226
|
+
try {
|
|
227
|
+
const token = (req.body && req.body.token) ? String(req.body.token) : '';
|
|
208
228
|
|
|
209
|
-
|
|
210
|
-
|
|
229
|
+
const isAdmin = await user.isAdministrator(req.uid);
|
|
230
|
+
const isMod = await user.isGlobalModerator(req.uid);
|
|
231
|
+
if (!isAdmin && !isMod) return res.status(403).json({ success: false, message: 'Yetkisiz' });
|
|
211
232
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
await db.delete(`niki:qr:${token}`);
|
|
233
|
+
const customerUid = await db.get(`niki:qr:${token}`);
|
|
234
|
+
if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
|
|
215
235
|
|
|
216
|
-
|
|
217
|
-
|
|
236
|
+
const pts = parseInt((await user.getUserField(customerUid, 'niki_points')) || 0, 10);
|
|
237
|
+
if (!TEST_MODE_UNLIMITED && pts < SETTINGS.coffeeCost) {
|
|
238
|
+
return res.json({ success: false, message: 'Yetersiz Bakiye' });
|
|
239
|
+
}
|
|
218
240
|
|
|
219
|
-
|
|
220
|
-
|
|
241
|
+
// ✅ puan düşür
|
|
242
|
+
if (!TEST_MODE_UNLIMITED) {
|
|
243
|
+
await user.decrementUserFieldBy(customerUid, 'niki_points', SETTINGS.coffeeCost);
|
|
244
|
+
}
|
|
221
245
|
|
|
222
|
-
|
|
223
|
-
|
|
246
|
+
// token tek kullanımlık
|
|
247
|
+
await db.delete(`niki:qr:${token}`);
|
|
224
248
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
249
|
+
// müşteri bilgisi
|
|
250
|
+
const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
|
|
251
|
+
|
|
252
|
+
// user log
|
|
253
|
+
await addUserLog(customerUid, 'spend', SETTINGS.coffeeCost, 'Kahve Keyfi ☕');
|
|
254
|
+
|
|
255
|
+
// kasa log
|
|
256
|
+
await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid);
|
|
257
|
+
|
|
258
|
+
return res.json({
|
|
259
|
+
success: true,
|
|
260
|
+
customer: customerData,
|
|
261
|
+
message: 'Onaylandı!',
|
|
262
|
+
});
|
|
263
|
+
} catch (err) {
|
|
264
|
+
return res.status(500).json({ success: false, message: 'Sunucu hatası' });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// 6) SAYFA ROTASI (kasa sayfası)
|
|
269
|
+
routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
|
|
270
|
+
const isAdmin = await user.isAdministrator(req.uid);
|
|
271
|
+
const isMod = await user.isGlobalModerator(req.uid);
|
|
272
|
+
if (!isAdmin && !isMod) return res.render('403', {});
|
|
273
|
+
return res.render('niki-kasa', { title: 'Niki Kasa' });
|
|
274
|
+
});
|
|
238
275
|
};
|
|
239
276
|
|
|
277
|
+
// client.js inject
|
|
240
278
|
Plugin.addScripts = async function (scripts) {
|
|
241
|
-
|
|
242
|
-
|
|
279
|
+
scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
|
|
280
|
+
return scripts;
|
|
243
281
|
};
|
|
244
282
|
|
|
283
|
+
// navigation
|
|
245
284
|
Plugin.addNavigation = async function (nav) {
|
|
246
|
-
|
|
247
|
-
|
|
285
|
+
nav.push({
|
|
286
|
+
route: '/niki-wallet',
|
|
287
|
+
title: 'Niki Cüzdan',
|
|
288
|
+
enabled: true,
|
|
289
|
+
iconClass: 'fa-coffee',
|
|
290
|
+
text: 'Niki Cüzdan',
|
|
291
|
+
});
|
|
292
|
+
return nav;
|
|
248
293
|
};
|
|
249
294
|
|
|
250
295
|
module.exports = Plugin;
|