nodebb-plugin-niki-loyalty 1.0.16 → 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 +266 -145
- package/package.json +1 -1
package/library.js
CHANGED
|
@@ -3,172 +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
|
|
|
15
|
-
//
|
|
19
|
+
// ✅ TEST: sınırsız kullanım (puan kontrolünü kapatmak için true)
|
|
20
|
+
const TEST_MODE_UNLIMITED = false;
|
|
21
|
+
|
|
22
|
+
// =========================
|
|
23
|
+
// JSON SAFE HELPERS
|
|
24
|
+
// =========================
|
|
25
|
+
function safeParseMaybeJson(x) {
|
|
26
|
+
if (x == null) return null;
|
|
27
|
+
|
|
28
|
+
// bazı DB’lerde object dönebilir
|
|
29
|
+
if (typeof x === 'object') return x;
|
|
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;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function safeStringify(obj) {
|
|
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}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// =========================
|
|
57
|
+
// LOG FONKSİYONLARI
|
|
58
|
+
// =========================
|
|
16
59
|
async function addUserLog(uid, type, amount, desc) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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);
|
|
26
72
|
}
|
|
27
73
|
|
|
28
74
|
async function addKasaLog(staffUid, customerName, customerUid) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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);
|
|
39
88
|
}
|
|
40
89
|
|
|
90
|
+
// =========================
|
|
91
|
+
// INIT
|
|
92
|
+
// =========================
|
|
41
93
|
Plugin.init = async function (params) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
94
|
+
const router = params.router;
|
|
95
|
+
const middleware = params.middleware;
|
|
96
|
+
|
|
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}`;
|
|
103
|
+
|
|
104
|
+
const currentDailyScore = parseInt((await db.getObjectField(dailyKey, 'score')) || 0, 10);
|
|
105
|
+
|
|
106
|
+
if (currentDailyScore >= SETTINGS.dailyCap) {
|
|
107
|
+
return res.json({ earned: false, reason: 'daily_cap' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
|
|
111
|
+
await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
|
|
112
|
+
|
|
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
|
+
});
|
|
119
|
+
|
|
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;
|
|
178
|
+
|
|
179
|
+
try {
|
|
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;
|
|
55
194
|
}
|
|
195
|
+
}));
|
|
196
|
+
|
|
197
|
+
return res.json(enriched);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
return res.status(500).json([]);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
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);
|
|
208
|
+
|
|
209
|
+
if (!TEST_MODE_UNLIMITED && points < SETTINGS.coffeeCost) {
|
|
210
|
+
return res.json({ success: false, message: 'Yetersiz Puan' });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
214
|
+
|
|
215
|
+
await db.set(`niki:qr:${token}`, uid);
|
|
216
|
+
await db.expire(`niki:qr:${token}`, 120);
|
|
56
217
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
dailyScore: dailyScore,
|
|
83
|
-
dailyCap: SETTINGS.dailyCap,
|
|
84
|
-
dailyPercent: dailyPercent,
|
|
85
|
-
history: (history || []).reverse()
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// --- İŞTE EKSİK OLAN KISIM BURASIYDI ---
|
|
90
|
-
// 3. KASA GEÇMİŞİ (Personel Ekranı İçin)
|
|
91
|
-
router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
|
|
92
|
-
const isAdmin = await user.isAdministrator(req.uid);
|
|
93
|
-
const isMod = await user.isGlobalModerator(req.uid);
|
|
94
|
-
|
|
95
|
-
if (!isAdmin && !isMod) return res.status(403).json([]);
|
|
96
|
-
|
|
97
|
-
// Veritabanından listeyi çek
|
|
98
|
-
const history = await db.getListRange('niki:kasa:history', 0, -1);
|
|
99
|
-
|
|
100
|
-
// Kullanıcı resimlerini de ekleyerek zenginleştir
|
|
101
|
-
const enrichedHistory = await Promise.all((history || []).reverse().map(async (item) => {
|
|
102
|
-
const uData = await user.getUserFields(item.cuid, ['picture']);
|
|
103
|
-
item.picture = uData.picture;
|
|
104
|
-
return item;
|
|
105
|
-
}));
|
|
106
|
-
|
|
107
|
-
res.json(enrichedHistory);
|
|
108
|
-
});
|
|
109
|
-
// ---------------------------------------
|
|
110
|
-
|
|
111
|
-
// 4. QR OLUŞTUR
|
|
112
|
-
router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
|
|
113
|
-
const uid = req.uid;
|
|
114
|
-
const points = parseInt(await user.getUserField(uid, 'niki_points')) || 0;
|
|
115
|
-
|
|
116
|
-
if (points < SETTINGS.coffeeCost) return res.json({ success: false, message: 'Yetersiz Puan' });
|
|
117
|
-
|
|
118
|
-
const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
119
|
-
await db.set(`niki:qr:${token}`, uid);
|
|
120
|
-
await db.expire(`niki:qr:${token}`, 120);
|
|
121
|
-
|
|
122
|
-
return res.json({ success: true, token: token });
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// 5. QR OKUT (Ödeme Alma)
|
|
126
|
-
router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
|
|
127
|
-
const { token } = req.body;
|
|
128
|
-
|
|
129
|
-
const isAdmin = await user.isAdministrator(req.uid);
|
|
130
|
-
const isMod = await user.isGlobalModerator(req.uid);
|
|
131
|
-
if (!isAdmin && !isMod) return res.status(403).json({ success: false, message: 'Yetkisiz' });
|
|
132
|
-
|
|
133
|
-
const customerUid = await db.get(`niki:qr:${token}`);
|
|
134
|
-
if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
|
|
135
|
-
|
|
136
|
-
const pts = parseInt(await user.getUserField(customerUid, 'niki_points')) || 0;
|
|
137
|
-
if (pts < SETTINGS.coffeeCost) return res.json({ success: false, message: 'Yetersiz Bakiye' });
|
|
138
|
-
|
|
139
|
-
// İŞLEM
|
|
218
|
+
return res.json({ success: true, token });
|
|
219
|
+
} catch (err) {
|
|
220
|
+
return res.status(500).json({ success: false, message: 'Sunucu hatası' });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
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) : '';
|
|
228
|
+
|
|
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' });
|
|
232
|
+
|
|
233
|
+
const customerUid = await db.get(`niki:qr:${token}`);
|
|
234
|
+
if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
|
|
235
|
+
|
|
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
|
+
}
|
|
240
|
+
|
|
241
|
+
// ✅ puan düşür
|
|
242
|
+
if (!TEST_MODE_UNLIMITED) {
|
|
140
243
|
await user.decrementUserFieldBy(customerUid, 'niki_points', SETTINGS.coffeeCost);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// token tek kullanımlık
|
|
247
|
+
await db.delete(`niki:qr:${token}`);
|
|
248
|
+
|
|
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
|
+
});
|
|
162
275
|
};
|
|
163
276
|
|
|
277
|
+
// client.js inject
|
|
164
278
|
Plugin.addScripts = async function (scripts) {
|
|
165
|
-
|
|
166
|
-
|
|
279
|
+
scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
|
|
280
|
+
return scripts;
|
|
167
281
|
};
|
|
168
282
|
|
|
283
|
+
// navigation
|
|
169
284
|
Plugin.addNavigation = async function (nav) {
|
|
170
|
-
|
|
171
|
-
|
|
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;
|
|
172
293
|
};
|
|
173
294
|
|
|
174
|
-
module.exports = Plugin;
|
|
295
|
+
module.exports = Plugin;
|