nodebb-plugin-niki-loyalty 1.0.26 → 1.0.28

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