nodebb-plugin-niki-loyalty 1.0.21 → 1.0.23
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 +268 -82
- package/package.json +1 -1
- package/static/lib/client.js +427 -224
- package/templates/niki-wallet.tpl +158 -29
package/library.js
CHANGED
|
@@ -13,9 +13,19 @@ const Plugin = {};
|
|
|
13
13
|
const SETTINGS = {
|
|
14
14
|
pointsPerHeartbeat: 5,
|
|
15
15
|
dailyCap: 250,
|
|
16
|
-
|
|
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
|
|
17
26
|
};
|
|
18
27
|
|
|
28
|
+
|
|
19
29
|
// ✅ TEST: sınırsız kullanım (puan kontrolünü kapatmak için true)
|
|
20
30
|
const TEST_MODE_UNLIMITED = false;
|
|
21
31
|
|
|
@@ -24,15 +34,12 @@ const TEST_MODE_UNLIMITED = false;
|
|
|
24
34
|
// =========================
|
|
25
35
|
function safeParseMaybeJson(x) {
|
|
26
36
|
if (x == null) return null;
|
|
27
|
-
|
|
28
|
-
// bazı DB’lerde object dönebilir
|
|
29
37
|
if (typeof x === 'object') return x;
|
|
30
38
|
|
|
31
39
|
if (typeof x === 'string') {
|
|
32
40
|
try {
|
|
33
41
|
return JSON.parse(x);
|
|
34
42
|
} catch (e) {
|
|
35
|
-
// "[object Object]" gibi bozuk kayıtları atla
|
|
36
43
|
return null;
|
|
37
44
|
}
|
|
38
45
|
}
|
|
@@ -53,15 +60,36 @@ function makeProfileUrl(userslug) {
|
|
|
53
60
|
return `${rp}/user/${userslug}`;
|
|
54
61
|
}
|
|
55
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
|
+
|
|
56
83
|
// =========================
|
|
57
84
|
// LOG FONKSİYONLARI
|
|
58
85
|
// =========================
|
|
59
|
-
async function addUserLog(uid, type, amount, desc) {
|
|
86
|
+
async function addUserLog(uid, type, amount, desc, extra) {
|
|
60
87
|
const logEntry = {
|
|
61
88
|
ts: Date.now(),
|
|
62
89
|
type, // 'earn' | 'spend'
|
|
63
90
|
amt: amount,
|
|
64
91
|
txt: desc,
|
|
92
|
+
...(extra ? { extra } : {}),
|
|
65
93
|
};
|
|
66
94
|
|
|
67
95
|
const payload = safeStringify(logEntry);
|
|
@@ -71,13 +99,15 @@ async function addUserLog(uid, type, amount, desc) {
|
|
|
71
99
|
await db.listTrim(`niki:activity:${uid}`, -50, -1);
|
|
72
100
|
}
|
|
73
101
|
|
|
74
|
-
async function addKasaLog(staffUid, customerName, customerUid) {
|
|
102
|
+
async function addKasaLog(staffUid, customerName, customerUid, amount, rewardId, rewardTitle) {
|
|
75
103
|
const logEntry = {
|
|
76
104
|
ts: Date.now(),
|
|
77
105
|
staff: staffUid,
|
|
78
|
-
cust: customerName,
|
|
106
|
+
cust: customerName,
|
|
79
107
|
cuid: customerUid,
|
|
80
|
-
amt:
|
|
108
|
+
amt: amount,
|
|
109
|
+
rewardId: rewardId || '',
|
|
110
|
+
rewardTitle: rewardTitle || '',
|
|
81
111
|
};
|
|
82
112
|
|
|
83
113
|
const payload = safeStringify(logEntry);
|
|
@@ -94,34 +124,41 @@ Plugin.init = async function (params) {
|
|
|
94
124
|
const router = params.router;
|
|
95
125
|
const middleware = params.middleware;
|
|
96
126
|
|
|
127
|
+
// -------------------------
|
|
97
128
|
// 1) HEARTBEAT (puan kazanma)
|
|
129
|
+
// -------------------------
|
|
98
130
|
router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
|
|
99
131
|
try {
|
|
100
132
|
const uid = req.uid;
|
|
101
|
-
const today =
|
|
133
|
+
const today = getTodayKey();
|
|
102
134
|
const dailyKey = `niki:daily:${uid}:${today}`;
|
|
103
135
|
|
|
104
136
|
const currentDailyScore = parseInt((await db.getObjectField(dailyKey, 'score')) || 0, 10);
|
|
105
137
|
|
|
106
|
-
if (currentDailyScore >= SETTINGS.dailyCap) {
|
|
138
|
+
if (!TEST_MODE_UNLIMITED && currentDailyScore >= SETTINGS.dailyCap) {
|
|
107
139
|
return res.json({ earned: false, reason: 'daily_cap' });
|
|
108
140
|
}
|
|
109
141
|
|
|
110
142
|
await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
|
|
111
143
|
await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
|
|
112
144
|
|
|
113
|
-
const newBalance = await user.getUserField(uid, 'niki_points');
|
|
145
|
+
const newBalance = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
|
|
146
|
+
|
|
147
|
+
await addUserLog(uid, 'earn', SETTINGS.pointsPerHeartbeat, 'Aktiflik Puanı ⚡');
|
|
148
|
+
|
|
114
149
|
return res.json({ earned: true, points: SETTINGS.pointsPerHeartbeat, total: newBalance });
|
|
115
150
|
} catch (err) {
|
|
116
151
|
return res.status(500).json({ earned: false, reason: 'server_error' });
|
|
117
152
|
}
|
|
118
153
|
});
|
|
119
154
|
|
|
120
|
-
//
|
|
155
|
+
// -------------------------
|
|
156
|
+
// 2) WALLET DATA (cüzdan + geçmiş + rewards)
|
|
157
|
+
// -------------------------
|
|
121
158
|
router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
|
|
122
159
|
try {
|
|
123
160
|
const uid = req.uid;
|
|
124
|
-
const today =
|
|
161
|
+
const today = getTodayKey();
|
|
125
162
|
|
|
126
163
|
const [userData, dailyData, historyRaw] = await Promise.all([
|
|
127
164
|
user.getUserFields(uid, ['niki_points']),
|
|
@@ -135,17 +172,33 @@ Plugin.init = async function (params) {
|
|
|
135
172
|
let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
|
|
136
173
|
if (dailyPercent > 100) dailyPercent = 100;
|
|
137
174
|
|
|
138
|
-
|
|
175
|
+
const barPercent = Math.min(100, (currentPoints / SETTINGS.barMax) * 100);
|
|
176
|
+
|
|
139
177
|
const history = (historyRaw || [])
|
|
140
178
|
.map(safeParseMaybeJson)
|
|
141
179
|
.filter(Boolean)
|
|
142
180
|
.reverse();
|
|
143
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
|
+
|
|
144
191
|
return res.json({
|
|
145
192
|
points: currentPoints,
|
|
193
|
+
|
|
146
194
|
dailyScore,
|
|
147
195
|
dailyCap: SETTINGS.dailyCap,
|
|
148
196
|
dailyPercent,
|
|
197
|
+
|
|
198
|
+
barMax: SETTINGS.barMax,
|
|
199
|
+
barPercent,
|
|
200
|
+
|
|
201
|
+
rewards,
|
|
149
202
|
history,
|
|
150
203
|
});
|
|
151
204
|
} catch (err) {
|
|
@@ -154,72 +207,203 @@ Plugin.init = async function (params) {
|
|
|
154
207
|
dailyScore: 0,
|
|
155
208
|
dailyCap: SETTINGS.dailyCap,
|
|
156
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 })),
|
|
157
213
|
history: [],
|
|
158
214
|
});
|
|
159
215
|
}
|
|
160
216
|
});
|
|
161
217
|
|
|
162
|
-
//
|
|
163
|
-
|
|
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) => {
|
|
164
223
|
try {
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const uid = parseInt(r.cuid, 10);
|
|
201
|
-
const u = userMap[uid];
|
|
202
|
-
if (!u) return r;
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
...r,
|
|
206
|
-
cust: u.username || r.cust || 'Bilinmeyen',
|
|
207
|
-
userslug: u.userslug || r.userslug || '',
|
|
208
|
-
picture: u.picture || r.picture || '',
|
|
209
|
-
iconBg: u['icon:bgColor'] || r.iconBg || '#4b5563',
|
|
210
|
-
profileUrl: (u.userslug ? `${rp}/user/${u.userslug}` : ''),
|
|
211
|
-
};
|
|
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,
|
|
212
259
|
});
|
|
213
260
|
|
|
214
|
-
|
|
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.',
|
|
276
|
+
});
|
|
215
277
|
} catch (err) {
|
|
216
|
-
return res.status(500).json(
|
|
278
|
+
return res.status(500).json({ success: false, message: 'Sunucu hatası' });
|
|
217
279
|
}
|
|
218
280
|
});
|
|
219
281
|
|
|
220
282
|
|
|
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' });
|
|
221
349
|
|
|
222
|
-
|
|
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ı!',
|
|
397
|
+
});
|
|
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
|
+
// -------------------------
|
|
223
407
|
router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
|
|
224
408
|
try {
|
|
225
409
|
const uid = req.uid;
|
|
@@ -229,7 +413,7 @@ router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (r
|
|
|
229
413
|
return res.json({ success: false, message: 'Yetersiz Puan' });
|
|
230
414
|
}
|
|
231
415
|
|
|
232
|
-
const token =
|
|
416
|
+
const token = makeToken();
|
|
233
417
|
|
|
234
418
|
await db.set(`niki:qr:${token}`, uid);
|
|
235
419
|
await db.expire(`niki:qr:${token}`, 120);
|
|
@@ -240,14 +424,12 @@ router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (r
|
|
|
240
424
|
}
|
|
241
425
|
});
|
|
242
426
|
|
|
243
|
-
// 5) QR OKUT (admin/mod)
|
|
244
427
|
router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
|
|
245
428
|
try {
|
|
246
429
|
const token = (req.body && req.body.token) ? String(req.body.token) : '';
|
|
247
430
|
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
if (!isAdmin && !isMod) return res.status(403).json({ success: false, message: 'Yetkisiz' });
|
|
431
|
+
const staffOk = await isStaff(req.uid);
|
|
432
|
+
if (!staffOk) return res.status(403).json({ success: false, message: 'Yetkisiz' });
|
|
251
433
|
|
|
252
434
|
const customerUid = await db.get(`niki:qr:${token}`);
|
|
253
435
|
if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
|
|
@@ -257,22 +439,16 @@ router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (r
|
|
|
257
439
|
return res.json({ success: false, message: 'Yetersiz Bakiye' });
|
|
258
440
|
}
|
|
259
441
|
|
|
260
|
-
// ✅ puan düşür
|
|
261
442
|
if (!TEST_MODE_UNLIMITED) {
|
|
262
443
|
await user.decrementUserFieldBy(customerUid, 'niki_points', SETTINGS.coffeeCost);
|
|
263
444
|
}
|
|
264
445
|
|
|
265
|
-
// token tek kullanımlık
|
|
266
446
|
await db.delete(`niki:qr:${token}`);
|
|
267
447
|
|
|
268
|
-
// müşteri bilgisi
|
|
269
448
|
const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
|
|
270
449
|
|
|
271
|
-
// user log
|
|
272
450
|
await addUserLog(customerUid, 'spend', SETTINGS.coffeeCost, 'Kahve Keyfi ☕');
|
|
273
|
-
|
|
274
|
-
// kasa log
|
|
275
|
-
await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid);
|
|
451
|
+
await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid, SETTINGS.coffeeCost, 'coffee', 'Ücretsiz Kahve (QR)');
|
|
276
452
|
|
|
277
453
|
return res.json({
|
|
278
454
|
success: true,
|
|
@@ -284,15 +460,25 @@ router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (r
|
|
|
284
460
|
}
|
|
285
461
|
});
|
|
286
462
|
|
|
287
|
-
//
|
|
463
|
+
// -------------------------
|
|
464
|
+
// 7) SAYFA ROTASI (kasa sayfası)
|
|
465
|
+
// -------------------------
|
|
288
466
|
routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
if (!isAdmin && !isMod) return res.render('403', {});
|
|
467
|
+
const staffOk = await isStaff(req.uid);
|
|
468
|
+
if (!staffOk) return res.render('403', {});
|
|
292
469
|
return res.render('niki-kasa', { title: 'Niki Kasa' });
|
|
293
470
|
});
|
|
294
471
|
};
|
|
295
472
|
|
|
473
|
+
// -------------------------
|
|
474
|
+
// ✅ plugin.json'da action:topic.get -> checkTopicVisit vardı.
|
|
475
|
+
// Method yoksa NodeBB şikayet eder. Şimdilik NO-OP koydum.
|
|
476
|
+
// Sonra istersen gerçekten "topic view" ile puan ekleriz.
|
|
477
|
+
// -------------------------
|
|
478
|
+
Plugin.checkTopicVisit = async function () {
|
|
479
|
+
return;
|
|
480
|
+
};
|
|
481
|
+
|
|
296
482
|
// client.js inject
|
|
297
483
|
Plugin.addScripts = async function (scripts) {
|
|
298
484
|
scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
|