nodebb-plugin-niki-loyalty 1.0.25 → 1.0.26

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 CHANGED
@@ -3,537 +3,214 @@
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');
7
6
 
8
7
  const Plugin = {};
9
8
 
10
- // =========================
11
- // AYARLAR
12
- // =========================
9
+ // --- AYARLAR: ÖDÜL SEVİYELERİ ---
10
+ 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 🍪" }
15
+ ];
16
+
13
17
  const SETTINGS = {
14
- pointsPerHeartbeat: 5,
15
- dailyCap: 250,
16
-
17
- barMax: 250,
18
- rewards: [
19
- { id: 'cookie', at: 60, title: 'Ücretsiz Kurabiye', type: 'free_item', meta: { item: 'cookie' } },
20
- { id: 'c35', at: 120, title: '%35 İndirimli Kahve', type: 'discount', meta: { product: 'coffee', percent: 35 } },
21
- { id: 'c60', at: 180, title: '%60 İndirimli Kahve', type: 'discount', meta: { product: 'coffee', percent: 60 } },
22
- { id: 'coffee', at: 250, title: 'Ücretsiz Kahve', type: 'free_item', meta: { item: 'coffee' } },
23
- ],
24
-
25
- couponTTLSeconds: 10 * 60, // 10 dk
18
+ pointsPerHeartbeat: 5,
19
+ dailyCap: 250
26
20
  };
27
21
 
28
-
29
- // TEST: sınırsız kullanım (puan kontrolünü kapatmak için true)
30
- const TEST_MODE_UNLIMITED = false;
31
-
32
- // =========================
33
- // JSON SAFE HELPERS
34
- // =========================
35
- function safeParseMaybeJson(x) {
36
- if (x == null) return null;
37
- if (typeof x === 'object') return x;
38
-
39
- if (typeof x === 'string') {
22
+ // --- LOG FONKSİYONLARI ---
23
+ async function addUserLog(uid, type, amount, desc) {
40
24
  try {
41
- return JSON.parse(x);
42
- } catch (e) {
43
- return null;
44
- }
45
- }
46
- return null;
47
- }
48
-
49
- function safeStringify(obj) {
50
- try {
51
- return JSON.stringify(obj);
52
- } catch (e) {
53
- return null;
54
- }
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); }
55
29
  }
56
30
 
57
- function makeProfileUrl(userslug) {
58
- const rp = nconf.get('relative_path') || '';
59
- if (!userslug) return '';
60
- return `${rp}/user/${userslug}`;
61
- }
62
-
63
- function makeToken() {
64
- return Math.random().toString(36).substring(2) + Date.now().toString(36);
65
- }
66
-
67
- function getTodayKey() {
68
- return new Date().toISOString().slice(0, 10).replace(/-/g, '');
69
- }
70
-
71
- function findRewardById(id) {
72
- return SETTINGS.rewards.find(r => r.id === id) || null;
73
- }
74
-
75
- async function isStaff(uid) {
76
- const [isAdmin, isMod] = await Promise.all([
77
- user.isAdministrator(uid),
78
- user.isGlobalModerator(uid),
79
- ]);
80
- return isAdmin || isMod;
81
- }
82
-
83
- // =========================
84
- // LOG FONKSİYONLARI
85
- // =========================
86
- async function addUserLog(uid, type, amount, desc, extra) {
87
- const logEntry = {
88
- ts: Date.now(),
89
- type, // 'earn' | 'spend'
90
- amt: amount,
91
- txt: desc,
92
- ...(extra ? { extra } : {}),
93
- };
94
-
95
- const payload = safeStringify(logEntry);
96
- if (!payload) return;
97
-
98
- await db.listAppend(`niki:activity:${uid}`, payload);
99
- await db.listTrim(`niki:activity:${uid}`, -50, -1);
100
- }
101
-
102
- async function addKasaLog(staffUid, customerName, customerUid, amount, rewardId, rewardTitle) {
103
- const logEntry = {
104
- ts: Date.now(),
105
- staff: staffUid,
106
- cust: customerName,
107
- cuid: customerUid,
108
- amt: amount,
109
- rewardId: rewardId || '',
110
- rewardTitle: rewardTitle || '',
111
- };
112
-
113
- const payload = safeStringify(logEntry);
114
- if (!payload) return;
115
-
116
- await db.listAppend('niki:kasa:history', payload);
117
- await db.listTrim('niki:kasa:history', -100, -1);
31
+ 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); }
118
44
  }
119
45
 
120
- // =========================
121
- // INIT
122
- // =========================
123
46
  Plugin.init = async function (params) {
124
- const router = params.router;
125
- const middleware = params.middleware;
126
-
127
- // -------------------------
128
- // 1) HEARTBEAT (puan kazanma)
129
- // -------------------------
130
- router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
131
- try {
132
- const uid = req.uid;
133
- const today = getTodayKey();
134
- const dailyKey = `niki:daily:${uid}:${today}`;
135
-
136
- const currentDailyScore = parseInt((await db.getObjectField(dailyKey, 'score')) || 0, 10);
137
-
138
- if (!TEST_MODE_UNLIMITED && currentDailyScore >= SETTINGS.dailyCap) {
139
- return res.json({ earned: false, reason: 'daily_cap' });
140
- }
141
-
142
- await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
143
- await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
144
-
145
- const newBalance = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
146
-
147
- await addUserLog(uid, 'earn', SETTINGS.pointsPerHeartbeat, 'Aktiflik Puanı ⚡');
148
-
149
- return res.json({ earned: true, points: SETTINGS.pointsPerHeartbeat, total: newBalance });
150
- } catch (err) {
151
- return res.status(500).json({ earned: false, reason: 'server_error' });
152
- }
153
- });
154
-
155
- // -------------------------
156
- // 2) WALLET DATA (cüzdan + geçmiş + rewards)
157
- // -------------------------
158
- router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
159
- try {
160
- const uid = req.uid;
161
- const today = getTodayKey();
162
-
163
- const [userData, dailyData, historyRaw] = await Promise.all([
164
- user.getUserFields(uid, ['niki_points']),
165
- db.getObject(`niki:daily:${uid}:${today}`),
166
- db.getListRange(`niki:activity:${uid}`, 0, -1),
167
- ]);
168
-
169
- const currentPoints = parseInt(userData?.niki_points || 0, 10);
170
- const dailyScore = parseInt(dailyData?.score || 0, 10);
171
-
172
- let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
173
- if (dailyPercent > 100) dailyPercent = 100;
174
-
175
- const barPercent = Math.min(100, (currentPoints / SETTINGS.barMax) * 100);
176
-
177
- const history = (historyRaw || [])
178
- .map(safeParseMaybeJson)
179
- .filter(Boolean)
180
- .reverse();
181
-
182
- const rewards = SETTINGS.rewards.map(r => ({
183
- id: r.id,
184
- at: r.at,
185
- title: r.title,
186
- type: r.type,
187
- meta: r.meta,
188
- unlocked: currentPoints >= r.at,
189
- }));
190
-
191
- return res.json({
192
- points: currentPoints,
193
-
194
- dailyScore,
195
- dailyCap: SETTINGS.dailyCap,
196
- dailyPercent,
197
-
198
- barMax: SETTINGS.barMax,
199
- barPercent,
200
-
201
- rewards,
202
- history,
203
- });
204
- } catch (err) {
205
- return res.status(500).json({
206
- points: 0,
207
- dailyScore: 0,
208
- dailyCap: SETTINGS.dailyCap,
209
- dailyPercent: 0,
210
- barMax: SETTINGS.barMax,
211
- barPercent: 0,
212
- rewards: SETTINGS.rewards.map(r => ({ id: r.id, at: r.at, title: r.title, type: r.type, meta: r.meta, unlocked: false })),
213
- history: [],
214
- });
215
- }
216
- });
217
-
218
- // -------------------------
219
- // 3) CLAIM REWARD (Seçenek B: claim = puan düşer + kupon token üretir)
220
- // Client bu endpoint'i çağıracak.
221
- // -------------------------
222
- router.post('/api/niki-loyalty/claim', middleware.ensureLoggedIn, async (req, res) => {
223
- try {
224
- const uid = req.uid;
225
- const rewardId = (req.body && req.body.rewardId) ? String(req.body.rewardId) : '';
226
- const reward = findRewardById(rewardId);
227
- if (!reward) return res.json({ success: false, message: 'Geçersiz ödül' });
228
-
229
- const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
230
- if (!TEST_MODE_UNLIMITED && points < reward.at) {
231
- return res.json({ success: false, message: 'Yetersiz puan' });
232
- }
233
-
234
- // ✅ Aynı ödülden aktif kupon varsa tekrar üretme (spam engeli)
235
- const reserveKey = `niki:reserve:${uid}:${reward.id}`;
236
- const existingToken = await db.get(reserveKey);
237
- if (existingToken) {
238
- return res.json({
239
- success: true,
240
- token: existingToken,
241
- reward: { id: reward.id, at: reward.at, title: reward.title, type: reward.type, meta: reward.meta },
242
- message: 'Zaten aktif bir kuponun var. Kasada okutabilirsin.',
243
- });
244
- }
245
-
246
- // ✅ token oluştur
247
- const token = makeToken();
248
- const couponKey = `niki:coupon:${token}`;
249
-
250
- const couponPayload = safeStringify({
251
- token,
252
- ts: Date.now(),
253
- rewardId: reward.id,
254
- cost: reward.at, // ✅ puan düşülecek tutar
255
- title: reward.title,
256
- type: reward.type,
257
- meta: reward.meta,
258
- ownerUid: uid,
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 }); }
259
69
  });
260
70
 
261
- await db.set(couponKey, couponPayload);
262
- await db.expire(couponKey, SETTINGS.couponTTLSeconds);
263
-
264
- // rezervasyonu da tut
265
- await db.set(reserveKey, token);
266
- await db.expire(reserveKey, SETTINGS.couponTTLSeconds);
267
-
268
- // user log (claim logu - puan düşmeden)
269
- await addUserLog(uid, 'spend', 0, `Kupon oluşturuldu: ${reward.title}`, { rewardId: reward.id, token });
270
-
271
- return res.json({
272
- success: true,
273
- token,
274
- reward: { id: reward.id, at: reward.at, title: reward.title, type: reward.type, meta: reward.meta },
275
- message: 'Kupon hazır! Kasada okutunca puanın düşer.',
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
+ }
276
106
  });
277
- } catch (err) {
278
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
279
- }
280
- });
281
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
+ });
282
128
 
283
- // -------------------------
284
- // 4) KASA HISTORY (admin/mod)
285
- // -------------------------
286
- router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
287
- try {
288
- const staffOk = await isStaff(req.uid);
289
- if (!staffOk) return res.status(403).json([]);
290
-
291
- const raw = await db.getListRange('niki:kasa:history', 0, -1);
292
-
293
- const rows = (raw || [])
294
- .map((x) => {
295
- if (!x) return null;
296
- if (typeof x === 'object') return x;
297
- if (typeof x === 'string') {
298
- try { return JSON.parse(x); } catch (e) { return null; }
299
- }
300
- return null;
301
- })
302
- .filter(Boolean)
303
- .reverse();
304
-
305
- const uids = rows
306
- .map(r => parseInt(r.cuid, 10))
307
- .filter(n => Number.isFinite(n) && n > 0);
308
-
309
- const users = await user.getUsersFields(uids, [
310
- 'uid', 'username', 'userslug', 'picture', 'icon:bgColor',
311
- ]);
312
-
313
- const userMap = {};
314
- (users || []).forEach(u => { userMap[u.uid] = u; });
315
-
316
- const rp = nconf.get('relative_path') || '';
317
-
318
- const enriched = rows.map(r => {
319
- const uid = parseInt(r.cuid, 10);
320
- const u = userMap[uid];
321
- if (!u) return r;
322
-
323
- return {
324
- ...r,
325
- cust: u.username || r.cust || 'Bilinmeyen',
326
- userslug: u.userslug || r.userslug || '',
327
- picture: u.picture || r.picture || '',
328
- iconBg: u['icon:bgColor'] || r.iconBg || '#4b5563',
329
- profileUrl: (u.userslug ? `${rp}/user/${u.userslug}` : ''),
330
- };
331
- });
332
-
333
- return res.json(enriched);
334
- } catch (err) {
335
- return res.status(500).json([]);
336
- }
337
- });
338
-
339
- // -------------------------
340
- // 5) KASA: COUPON SCAN (admin/mod) ✅ yeni akış
341
- // Claim ile üretilen token kasada okutulur.
342
- // -------------------------
343
- router.post('/api/niki-loyalty/scan-coupon', middleware.ensureLoggedIn, async (req, res) => {
344
- try {
345
- const token = (req.body && req.body.token) ? String(req.body.token) : '';
346
-
347
- const staffOk = await isStaff(req.uid);
348
- if (!staffOk) return res.status(403).json({ success: false, message: 'Yetkisiz' });
349
-
350
- const raw = await db.get(`niki:coupon:${token}`);
351
- if (!raw) return res.json({ success: false, message: 'Geçersiz / Süresi dolmuş kupon' });
352
-
353
- const coupon = safeParseMaybeJson(raw);
354
- if (!coupon || !coupon.ownerUid || !coupon.rewardId) {
355
- await db.delete(`niki:coupon:${token}`);
356
- return res.json({ success: false, message: 'Kupon bozuk' });
357
- }
358
-
359
- const customerUid = parseInt(coupon.ownerUid, 10);
360
- const cost = parseInt(coupon.cost || 0, 10);
361
-
362
- // ✅ puan yeter mi? (scan anında kontrol)
363
- const pts = parseInt((await user.getUserField(customerUid, 'niki_points')) || 0, 10);
364
- if (!TEST_MODE_UNLIMITED && pts < cost) {
365
- return res.json({ success: false, message: 'Yetersiz bakiye (puan değişmiş olabilir)' });
366
- }
367
-
368
- // ✅ puan düş (asıl düşüş burada)
369
- if (!TEST_MODE_UNLIMITED && cost > 0) {
370
- await user.decrementUserFieldBy(customerUid, 'niki_points', cost);
371
- }
372
-
373
- // ✅ tek kullanımlık: kuponu sil
374
- await db.delete(`niki:coupon:${token}`);
375
- // ✅ rezervasyonu da sil
376
- await db.delete(`niki:reserve:${customerUid}:${coupon.rewardId}`);
377
-
378
- const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
379
-
380
- // user log (gerçek spend burada)
381
- await addUserLog(customerUid, 'spend', cost, `Kullanıldı: ${coupon.title}`, { rewardId: coupon.rewardId });
382
-
383
- // kasa log
384
- await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid, cost, coupon.rewardId, coupon.title);
385
-
386
- return res.json({
387
- success: true,
388
- customer: customerData,
389
- coupon: {
390
- rewardId: coupon.rewardId,
391
- title: coupon.title,
392
- type: coupon.type,
393
- meta: coupon.meta,
394
- cost,
395
- },
396
- message: 'Kupon onaylandı!',
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" }); }
397
145
  });
398
- } catch (err) {
399
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
400
- }
401
- });
402
-
403
- // -------------------------
404
- // 6) (ESKİ) QR OLUŞTUR / OKUT — opsiyonel uyumluluk
405
- // İstersen tamamen kaldırırız. Şimdilik 250 "Ücretsiz Kahve" gibi düşünebilirsin.
406
- // -------------------------
407
- router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
408
- try {
409
- const uid = req.uid;
410
- const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
411
-
412
- if (!TEST_MODE_UNLIMITED && points < SETTINGS.coffeeCost) {
413
- return res.json({ success: false, message: 'Yetersiz Puan' });
414
- }
415
-
416
- const token = makeToken();
417
-
418
- await db.set(`niki:qr:${token}`, uid);
419
- await db.expire(`niki:qr:${token}`, 120);
420
-
421
- return res.json({ success: true, token });
422
- } catch (err) {
423
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
424
- }
425
- });
426
- const TIERS = [60, 120, 180, 250];
427
-
428
- function pickTier(points) {
429
- // en büyük uygun tier
430
- let chosen = 0;
431
- for (const t of TIERS) if (points >= t) chosen = t;
432
- return chosen; // 0 ise yetersiz
433
- }
434
146
 
435
- router.post('/api/niki-loyalty/claim-auto', middleware.ensureLoggedIn, async (req, res) => {
436
- try {
437
- const uid = req.uid;
438
- const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
439
-
440
- if (TEST_MODE_UNLIMITED) {
441
- // testte 250 gibi davran
442
- const token = makeToken();
443
- await db.set(`niki:coupon:${token}`, safeStringify({ token, ts: Date.now(), ownerUid: uid, cost: 250 }));
444
- await db.expire(`niki:coupon:${token}`, 120);
445
- return res.json({ success: true, token, cost: 250 });
446
- }
447
-
448
- const cost = pickTier(points);
449
- if (!cost) return res.json({ success: false, message: 'En az 60 puan gerekli.' });
450
-
451
- const token = makeToken();
452
- await db.set(`niki:coupon:${token}`, safeStringify({
453
- token,
454
- ts: Date.now(),
455
- ownerUid: uid,
456
- cost, // ✅ kasada düşülecek miktar
457
- }));
458
- await db.expire(`niki:coupon:${token}`, 120); // ✅ 2 dk
459
-
460
- return res.json({ success: true, token, cost });
461
- } catch (e) {
462
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
463
- }
464
- });
465
-
466
- router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
467
- try {
468
- const token = (req.body && req.body.token) ? String(req.body.token) : '';
469
-
470
- const staffOk = await isStaff(req.uid);
471
- if (!staffOk) return res.status(403).json({ success: false, message: 'Yetkisiz' });
472
-
473
- const customerUid = await db.get(`niki:qr:${token}`);
474
- if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
475
-
476
- const pts = parseInt((await user.getUserField(customerUid, 'niki_points')) || 0, 10);
477
- if (!TEST_MODE_UNLIMITED && pts < SETTINGS.coffeeCost) {
478
- return res.json({ success: false, message: 'Yetersiz Bakiye' });
479
- }
480
-
481
- if (!TEST_MODE_UNLIMITED) {
482
- await user.decrementUserFieldBy(customerUid, 'niki_points', SETTINGS.coffeeCost);
483
- }
484
-
485
- await db.delete(`niki:qr:${token}`);
486
-
487
- const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
488
-
489
- await addUserLog(customerUid, 'spend', SETTINGS.coffeeCost, 'Kahve Keyfi ☕');
490
- await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid, SETTINGS.coffeeCost, 'coffee', 'Ücretsiz Kahve (QR)');
491
-
492
- return res.json({
493
- success: true,
494
- customer: customerData,
495
- message: 'Onaylandı!',
496
- });
497
- } catch (err) {
498
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
499
- }
500
- });
501
-
502
- // -------------------------
503
- // 7) SAYFA ROTASI (kasa sayfası)
504
- // -------------------------
505
- routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
506
- const staffOk = await isStaff(req.uid);
507
- if (!staffOk) return res.render('403', {});
508
- return res.render('niki-kasa', { title: 'Niki Kasa' });
509
- });
510
- };
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
+ });
511
197
 
512
- // -------------------------
513
- // plugin.json'da action:topic.get -> checkTopicVisit vardı.
514
- // Method yoksa NodeBB şikayet eder. Şimdilik NO-OP koydum.
515
- // Sonra istersen gerçekten "topic view" ile puan ekleriz.
516
- // -------------------------
517
- Plugin.checkTopicVisit = async function () {
518
- return;
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
+ });
519
204
  };
520
205
 
521
- // client.js inject
522
206
  Plugin.addScripts = async function (scripts) {
523
- scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
524
- return scripts;
207
+ scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
208
+ return scripts;
525
209
  };
526
210
 
527
- // navigation
528
211
  Plugin.addNavigation = async function (nav) {
529
- nav.push({
530
- route: '/niki-wallet',
531
- title: 'Niki Cüzdan',
532
- enabled: true,
533
- iconClass: 'fa-coffee',
534
- text: 'Niki Cüzdan',
535
- });
536
- return nav;
212
+ nav.push({ route: "/niki-wallet", title: "Niki Cüzdan", enabled: true, iconClass: "fa-coffee", text: "Niki Cüzdan" });
213
+ return nav;
537
214
  };
538
215
 
539
- module.exports = Plugin;
216
+ module.exports = Plugin;