nodebb-plugin-niki-loyalty 1.0.26 → 1.0.29

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.
Files changed (2) hide show
  1. package/library.js +266 -187
  2. package/package.json +1 -1
package/library.js CHANGED
@@ -3,214 +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
 
9
- // --- AYARLAR: ÖDÜL SEVİYELERİ ---
10
+ // =========================
11
+ // SETTINGS & REWARDS
12
+ // =========================
13
+ const SETTINGS = {
14
+ pointsPerHeartbeat: 5,
15
+ dailyCap: 250,
16
+ // coffeeCost: 250, // (eski kodda vardı ama artık tier sistemi var; istersen kullanırız)
17
+ };
18
+
19
+ // Rewards configuration (Ordered from highest cost to lowest)
10
20
  const REWARDS = [
11
- { cost: 250, name: "Ücretsiz Kahve ☕" }, // En büyük ödül en üstte olmalı
12
- { cost: 180, name: "%60 İndirimli Kahve" },
13
- { cost: 120, name: "%30 İndirimli Kahve" },
14
- { cost: 60, name: "1 Kurabiye 🍪" }
21
+ { cost: 250, name: 'Ücretsiz Kahve ☕' },
22
+ { cost: 180, name: '%60 İndirimli Kahve' },
23
+ { cost: 120, name: '%30 İndirimli Kahve' },
24
+ { cost: 60, name: '1 Kurabiye 🍪' },
15
25
  ];
16
26
 
17
- const SETTINGS = {
18
- pointsPerHeartbeat: 5,
19
- dailyCap: 250
20
- };
27
+ // TEST MODE (Set to true to bypass point checks)
28
+ const TEST_MODE_UNLIMITED = false;
21
29
 
22
- // --- LOG FONKSİYONLARI ---
23
- async function addUserLog(uid, type, amount, desc) {
30
+ // =========================
31
+ // HELPER FUNCTIONS
32
+ // =========================
33
+ function safeParseMaybeJson(x) {
34
+ if (x == null) return null;
35
+ if (typeof x === 'object') return x;
36
+ if (typeof x === 'string') {
24
37
  try {
25
- const logEntry = { ts: Date.now(), type: type, amt: amount, txt: desc };
26
- await db.listAppend(`niki:activity:${uid}`, logEntry);
27
- await db.listTrim(`niki:activity:${uid}`, -50, -1);
28
- } catch (e) { console.error("UserLog Error:", e); }
38
+ return JSON.parse(x);
39
+ } catch (e) {
40
+ return null;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+
46
+ function safeStringify(obj) {
47
+ try {
48
+ return JSON.stringify(obj);
49
+ } catch (e) {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ // =========================
55
+ // LOGGING
56
+ // =========================
57
+ async function addUserLog(uid, type, amount, desc) {
58
+ const logEntry = {
59
+ ts: Date.now(),
60
+ type, // 'earn' | 'spend'
61
+ amt: amount,
62
+ txt: desc,
63
+ };
64
+ const payload = safeStringify(logEntry);
65
+ if (!payload) return;
66
+ await db.listAppend(`niki:activity:${uid}`, payload);
67
+ await db.listTrim(`niki:activity:${uid}`, -50, -1);
29
68
  }
30
69
 
31
70
  async function addKasaLog(staffUid, customerName, customerUid, rewardName, amount) {
32
- try {
33
- const logEntry = {
34
- ts: Date.now(),
35
- staff: staffUid,
36
- cust: customerName,
37
- cuid: customerUid,
38
- amt: amount,
39
- reward: rewardName // Ödülün adını da kaydedelim
40
- };
41
- await db.listAppend('niki:kasa:history', logEntry);
42
- await db.listTrim('niki:kasa:history', -100, -1);
43
- } catch (e) { console.error("KasaLog Error:", e); }
71
+ const logEntry = {
72
+ ts: Date.now(),
73
+ staff: staffUid,
74
+ cust: customerName,
75
+ cuid: customerUid,
76
+ amt: amount,
77
+ reward: rewardName, // Store the specific reward name
78
+ };
79
+ const payload = safeStringify(logEntry);
80
+ if (!payload) return;
81
+ await db.listAppend('niki:kasa:history', payload);
82
+ await db.listTrim('niki:kasa:history', -100, -1);
44
83
  }
45
84
 
85
+ // =========================
86
+ // PLUGIN INIT
87
+ // =========================
46
88
  Plugin.init = async function (params) {
47
- const router = params.router;
48
- const middleware = params.middleware;
49
-
50
- // 1. HEARTBEAT (Puan Kazanma)
51
- router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
52
- try {
53
- const uid = req.uid;
54
- const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
55
- const dailyKey = `niki:daily:${uid}:${today}`;
56
-
57
- const currentDailyScore = await db.getObjectField(dailyKey, 'score') || 0;
58
-
59
- if (parseInt(currentDailyScore) >= SETTINGS.dailyCap) {
60
- return res.json({ earned: false, reason: 'daily_cap' });
61
- }
62
-
63
- await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
64
- await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
65
-
66
- const newBalance = await user.getUserField(uid, 'niki_points');
67
- return res.json({ earned: true, points: SETTINGS.pointsPerHeartbeat, total: newBalance });
68
- } catch (e) { return res.json({ earned: false }); }
69
- });
70
-
71
- // 2. WALLET DATA
72
- router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
73
- try {
74
- const uid = req.uid;
75
- const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
76
-
77
- const [userData, dailyData, rawHistory] = await Promise.all([
78
- user.getUserFields(uid, ['niki_points']),
79
- db.getObject(`niki:daily:${uid}:${today}`),
80
- db.getListRange(`niki:activity:${uid}`, 0, -1)
81
- ]);
82
-
83
- const history = (rawHistory || []).map(item => {
84
- if (typeof item === 'string') { try { return JSON.parse(item); } catch { return null; } }
85
- return item;
86
- }).filter(item => item !== null).reverse();
87
-
88
- const currentPoints = parseInt(userData.niki_points) || 0;
89
- const dailyScore = parseInt(dailyData ? dailyData.score : 0) || 0;
90
-
91
- // Yüzdeyi en büyük hedefe (250) göre hesapla
92
- let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
93
- if (dailyPercent > 100) dailyPercent = 100;
94
-
95
- res.json({
96
- points: currentPoints,
97
- dailyScore: dailyScore,
98
- dailyCap: SETTINGS.dailyCap,
99
- dailyPercent: dailyPercent,
100
- history: history,
101
- rewards: REWARDS // Frontend'e ödül listesini gönder
102
- });
103
- } catch (e) {
104
- res.json({ points: 0, dailyScore: 0, dailyCap: 250, dailyPercent: 0, history: [] });
89
+ const router = params.router;
90
+ const middleware = params.middleware;
91
+
92
+ // 1) HEARTBEAT (Earn Points) + ✅ EARN LOG (2. koddaki özellik)
93
+ router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
94
+ try {
95
+ const uid = req.uid;
96
+ const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
97
+ const dailyKey = `niki:daily:${uid}:${today}`;
98
+
99
+ const currentDailyScore = parseInt((await db.getObjectField(dailyKey, 'score')) || 0, 10);
100
+
101
+ if (currentDailyScore >= SETTINGS.dailyCap) {
102
+ return res.json({ earned: false, reason: 'daily_cap' });
103
+ }
104
+
105
+ await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
106
+ await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
107
+
108
+ // Log: Cüzdan geçmişinde "kazanım" görünsün
109
+ await addUserLog(uid, 'earn', SETTINGS.pointsPerHeartbeat, 'Ders Çalışma 📚');
110
+
111
+ const newBalance = await user.getUserField(uid, 'niki_points');
112
+ return res.json({ earned: true, points: SETTINGS.pointsPerHeartbeat, total: newBalance });
113
+ } catch (err) {
114
+ return res.status(500).json({ earned: false, reason: 'server_error' });
115
+ }
116
+ });
117
+
118
+ // 2) WALLET DATA (Data + History + Rewards Info)
119
+ router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
120
+ try {
121
+ const uid = req.uid;
122
+ const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
123
+
124
+ const [userData, dailyData, historyRaw] = await Promise.all([
125
+ user.getUserFields(uid, ['niki_points']),
126
+ db.getObject(`niki:daily:${uid}:${today}`),
127
+ db.getListRange(`niki:activity:${uid}`, 0, -1),
128
+ ]);
129
+
130
+ const currentPoints = parseInt(userData?.niki_points || 0, 10);
131
+ const dailyScore = parseInt(dailyData?.score || 0, 10);
132
+
133
+ let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
134
+ if (dailyPercent > 100) dailyPercent = 100;
135
+
136
+ const history = (historyRaw || []).map(safeParseMaybeJson).filter(Boolean).reverse();
137
+
138
+ return res.json({
139
+ points: currentPoints,
140
+ dailyScore,
141
+ dailyCap: SETTINGS.dailyCap,
142
+ dailyPercent,
143
+ history,
144
+ rewards: REWARDS,
145
+ });
146
+ } catch (err) {
147
+ return res.status(500).json({ points: 0, history: [] });
148
+ }
149
+ });
150
+
151
+ // 3) KASA HISTORY (+ ✅ iconBg + reward fallback (2. koddaki özellik))
152
+ router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
153
+ try {
154
+ const isAdmin = await user.isAdministrator(req.uid);
155
+ const isMod = await user.isGlobalModerator(req.uid);
156
+ if (!isAdmin && !isMod) return res.status(403).json([]);
157
+
158
+ const raw = await db.getListRange('niki:kasa:history', 0, -1);
159
+ const rows = (raw || []).map(safeParseMaybeJson).filter(Boolean).reverse();
160
+
161
+ const uids = rows.map(r => parseInt(r.cuid, 10)).filter(n => Number.isFinite(n) && n > 0);
162
+ const users = await user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'icon:bgColor']);
163
+ const userMap = {};
164
+ (users || []).forEach(u => {
165
+ userMap[u.uid] = u;
166
+ });
167
+
168
+ const rp = nconf.get('relative_path') || '';
169
+
170
+ const enriched = rows.map(r => {
171
+ const uid = parseInt(r.cuid, 10);
172
+ const u = userMap[uid];
173
+ if (!u) {
174
+ // yine de reward fallback yapalım
175
+ return { ...r, reward: r.reward || 'İşlem' };
176
+ }
177
+ return {
178
+ ...r,
179
+ cust: u.username || r.cust || 'Bilinmeyen',
180
+ userslug: u.userslug || r.userslug || '',
181
+ picture: u.picture || r.picture || '',
182
+ iconBg: u['icon:bgColor'] || r.iconBg || '#4b5563',
183
+ profileUrl: u.userslug ? `${rp}/user/${u.userslug}` : '',
184
+ reward: r.reward || 'İşlem',
185
+ };
186
+ });
187
+
188
+ return res.json(enriched);
189
+ } catch (err) {
190
+ return res.status(500).json([]);
191
+ }
192
+ });
193
+
194
+ // 4) GENERATE QR (Check if user has enough for MINIMUM reward)
195
+ router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
196
+ try {
197
+ const uid = req.uid;
198
+ const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
199
+
200
+ const minCost = REWARDS[REWARDS.length - 1].cost;
201
+
202
+ if (!TEST_MODE_UNLIMITED && points < minCost) {
203
+ return res.json({ success: false, message: `En az ${minCost} puan gerekli.` });
204
+ }
205
+
206
+ const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
207
+ await db.set(`niki:qr:${token}`, uid);
208
+ await db.expire(`niki:qr:${token}`, 120);
209
+
210
+ return res.json({ success: true, token });
211
+ } catch (err) {
212
+ return res.status(500).json({ success: false, message: 'Sunucu hatası' });
213
+ }
214
+ });
215
+
216
+ // 5) SCAN QR (Determine Reward & Deduct Points)
217
+ router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
218
+ try {
219
+ const token = req.body && req.body.token ? String(req.body.token) : '';
220
+ const isAdmin = await user.isAdministrator(req.uid);
221
+ const isMod = await user.isGlobalModerator(req.uid);
222
+ if (!isAdmin && !isMod) return res.status(403).json({ success: false, message: 'Yetkisiz' });
223
+
224
+ const customerUid = await db.get(`niki:qr:${token}`);
225
+ if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
226
+
227
+ const pts = parseInt((await user.getUserField(customerUid, 'niki_points')) || 0, 10);
228
+
229
+ // Calculate best possible reward
230
+ let selectedReward = null;
231
+ if (!TEST_MODE_UNLIMITED) {
232
+ for (const reward of REWARDS) {
233
+ if (pts >= reward.cost) {
234
+ selectedReward = reward;
235
+ break;
236
+ }
105
237
  }
106
- });
107
-
108
- // 3. KASA GEÇMİŞİ
109
- router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
110
- try {
111
- const isAdmin = await user.isAdministrator(req.uid);
112
- const isMod = await user.isGlobalModerator(req.uid);
113
- if (!isAdmin && !isMod) return res.status(403).json([]);
114
-
115
- const rawHistory = await db.getListRange('niki:kasa:history', 0, -1);
116
- const enrichedHistory = await Promise.all((rawHistory || []).reverse().map(async (item) => {
117
- try {
118
- if (typeof item === 'string') { try { item = JSON.parse(item); } catch (e) { return null; } }
119
- if (!item || !item.cuid) return null;
120
- const uData = await user.getUserFields(item.cuid, ['picture']);
121
- item.picture = uData ? uData.picture : null;
122
- return item;
123
- } catch (err) { return null; }
124
- }));
125
- res.json(enrichedHistory.filter(i => i !== null));
126
- } catch (e) { res.json([]); }
127
- });
128
-
129
- // 4. QR OLUŞTUR (En düşük ödül seviyesi kadar puanı var mı?)
130
- router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
131
- try {
132
- const uid = req.uid;
133
- const points = parseInt(await user.getUserField(uid, 'niki_points')) || 0;
134
-
135
- // En ucuz ödül (60 puan) kadar bile puanı yoksa izin verme
136
- const minCost = REWARDS[REWARDS.length - 1].cost;
137
- if (points < minCost) return res.json({ success: false, message: `En az ${minCost} puan gerekli.` });
138
-
139
- const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
140
- await db.set(`niki:qr:${token}`, uid);
141
- await db.expire(`niki:qr:${token}`, 120);
142
-
143
- return res.json({ success: true, token: token });
144
- } catch (e) { return res.json({ success: false, message: "Hata" }); }
145
- });
146
-
147
- // 5. QR OKUT (OTOMATİK ÖDÜL SEÇİMİ)
148
- router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
149
- try {
150
- const { token } = req.body;
151
- const isAdmin = await user.isAdministrator(req.uid);
152
- const isMod = await user.isGlobalModerator(req.uid);
153
- if (!isAdmin && !isMod) return res.status(403).json({ success: false, message: 'Yetkisiz' });
154
-
155
- const customerUid = await db.get(`niki:qr:${token}`);
156
- if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
157
-
158
- const pts = parseInt(await user.getUserField(customerUid, 'niki_points')) || 0;
159
-
160
- // --- HANGİ ÖDÜLÜ ALABİLİR? ---
161
- let selectedReward = null;
162
-
163
- // Ödülleri pahalıdan ucuza tara, ilk yeteni seç
164
- for (const reward of REWARDS) {
165
- if (pts >= reward.cost) {
166
- selectedReward = reward;
167
- break; // En büyüğü bulduk, döngüden çık
168
- }
169
- }
170
-
171
- if (!selectedReward) {
172
- return res.json({ success: false, message: 'Puan Yetersiz (Min: 60)' });
173
- }
174
-
175
- // İŞLEM
176
- await user.decrementUserFieldBy(customerUid, 'niki_points', selectedReward.cost);
177
- await db.delete(`niki:qr:${token}`);
178
-
179
- // LOGLAR
180
- const customerData = await user.getUserFields(customerUid, ['username', 'picture']);
181
-
182
- await addUserLog(customerUid, 'spend', selectedReward.cost, selectedReward.name);
183
- await addKasaLog(req.uid, customerData.username, customerUid, selectedReward.name, selectedReward.cost);
184
-
185
- // Frontend'e hangi ödülün verildiğini söyle
186
- return res.json({
187
- success: true,
188
- customer: customerData,
189
- rewardName: selectedReward.name,
190
- cost: selectedReward.cost
191
- });
192
-
193
- } catch (e) {
194
- return res.json({ success: false, message: "İşlem Hatası" });
238
+ if (!selectedReward) {
239
+ return res.json({ success: false, message: 'Puan Yetersiz' });
195
240
  }
196
- });
197
-
198
- routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
199
- const isAdmin = await user.isAdministrator(req.uid);
200
- const isMod = await user.isGlobalModerator(req.uid);
201
- if (!isAdmin && !isMod) return res.render('403', {});
202
- res.render('niki-kasa', { title: 'Niki Kasa' });
203
- });
241
+ } else {
242
+ selectedReward = REWARDS[0];
243
+ }
244
+
245
+ // Deduct Points
246
+ if (!TEST_MODE_UNLIMITED) {
247
+ await user.decrementUserFieldBy(customerUid, 'niki_points', selectedReward.cost);
248
+ }
249
+
250
+ await db.delete(`niki:qr:${token}`);
251
+
252
+ const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
253
+
254
+ // Logs
255
+ await addUserLog(customerUid, 'spend', selectedReward.cost, selectedReward.name);
256
+ await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid, selectedReward.name, selectedReward.cost);
257
+
258
+ return res.json({
259
+ success: true,
260
+ customer: customerData,
261
+ rewardName: selectedReward.name,
262
+ cost: selectedReward.cost,
263
+ message: 'Onaylandı!',
264
+ });
265
+ } catch (err) {
266
+ return res.status(500).json({ success: false, message: 'Sunucu hatası' });
267
+ }
268
+ });
269
+
270
+ // 6) PAGE ROUTES
271
+ routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
272
+ const isAdmin = await user.isAdministrator(req.uid);
273
+ const isMod = await user.isGlobalModerator(req.uid);
274
+ if (!isAdmin && !isMod) return res.render('403', {});
275
+ return res.render('niki-kasa', { title: 'Niki Kasa' });
276
+ });
204
277
  };
205
278
 
206
279
  Plugin.addScripts = async function (scripts) {
207
- scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
208
- return scripts;
280
+ scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
281
+ return scripts;
209
282
  };
210
283
 
211
284
  Plugin.addNavigation = async function (nav) {
212
- nav.push({ route: "/niki-wallet", title: "Niki Cüzdan", enabled: true, iconClass: "fa-coffee", text: "Niki Cüzdan" });
213
- return nav;
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;
214
293
  };
215
294
 
216
- module.exports = Plugin;
295
+ module.exports = Plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-niki-loyalty",
3
- "version": "1.0.26",
3
+ "version": "1.0.29",
4
4
  "description": "Niki The Cat Coffee Loyalty System - Earn points while studying on IEU Forum.",
5
5
  "main": "library.js",
6
6
  "nbbpm": {