nodebb-plugin-niki-loyalty 1.0.25 → 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.
- package/library.js +72 -336
- package/package.json +1 -1
- package/static/lib/client.js +104 -448
- package/templates/niki-wallet.tpl +29 -164
package/library.js
CHANGED
|
@@ -8,157 +8,106 @@ const nconf = require.main.require('nconf');
|
|
|
8
8
|
const Plugin = {};
|
|
9
9
|
|
|
10
10
|
// =========================
|
|
11
|
-
//
|
|
11
|
+
// SETTINGS & REWARDS
|
|
12
12
|
// =========================
|
|
13
13
|
const SETTINGS = {
|
|
14
14
|
pointsPerHeartbeat: 5,
|
|
15
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
|
|
26
16
|
};
|
|
27
17
|
|
|
18
|
+
// Rewards configuration (Ordered from highest cost to lowest)
|
|
19
|
+
const REWARDS = [
|
|
20
|
+
{ cost: 250, name: "Ücretsiz Kahve ☕" },
|
|
21
|
+
{ cost: 180, name: "%60 İndirimli Kahve" },
|
|
22
|
+
{ cost: 120, name: "%30 İndirimli Kahve" },
|
|
23
|
+
{ cost: 60, name: "1 Kurabiye 🍪" }
|
|
24
|
+
];
|
|
28
25
|
|
|
29
|
-
//
|
|
26
|
+
// TEST MODE (Set to true to bypass point checks)
|
|
30
27
|
const TEST_MODE_UNLIMITED = false;
|
|
31
28
|
|
|
32
29
|
// =========================
|
|
33
|
-
//
|
|
30
|
+
// HELPER FUNCTIONS
|
|
34
31
|
// =========================
|
|
35
32
|
function safeParseMaybeJson(x) {
|
|
36
33
|
if (x == null) return null;
|
|
37
34
|
if (typeof x === 'object') return x;
|
|
38
|
-
|
|
39
35
|
if (typeof x === 'string') {
|
|
40
|
-
try {
|
|
41
|
-
return JSON.parse(x);
|
|
42
|
-
} catch (e) {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
36
|
+
try { return JSON.parse(x); } catch (e) { return null; }
|
|
45
37
|
}
|
|
46
38
|
return null;
|
|
47
39
|
}
|
|
48
40
|
|
|
49
41
|
function safeStringify(obj) {
|
|
50
|
-
try {
|
|
51
|
-
return JSON.stringify(obj);
|
|
52
|
-
} catch (e) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
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;
|
|
42
|
+
try { return JSON.stringify(obj); } catch (e) { return null; }
|
|
81
43
|
}
|
|
82
44
|
|
|
83
45
|
// =========================
|
|
84
|
-
//
|
|
46
|
+
// LOGGING
|
|
85
47
|
// =========================
|
|
86
|
-
async function addUserLog(uid, type, amount, desc
|
|
48
|
+
async function addUserLog(uid, type, amount, desc) {
|
|
87
49
|
const logEntry = {
|
|
88
50
|
ts: Date.now(),
|
|
89
51
|
type, // 'earn' | 'spend'
|
|
90
52
|
amt: amount,
|
|
91
53
|
txt: desc,
|
|
92
|
-
...(extra ? { extra } : {}),
|
|
93
54
|
};
|
|
94
|
-
|
|
95
55
|
const payload = safeStringify(logEntry);
|
|
96
56
|
if (!payload) return;
|
|
97
|
-
|
|
98
57
|
await db.listAppend(`niki:activity:${uid}`, payload);
|
|
99
58
|
await db.listTrim(`niki:activity:${uid}`, -50, -1);
|
|
100
59
|
}
|
|
101
60
|
|
|
102
|
-
async function addKasaLog(staffUid, customerName, customerUid,
|
|
61
|
+
async function addKasaLog(staffUid, customerName, customerUid, rewardName, amount) {
|
|
103
62
|
const logEntry = {
|
|
104
63
|
ts: Date.now(),
|
|
105
64
|
staff: staffUid,
|
|
106
65
|
cust: customerName,
|
|
107
66
|
cuid: customerUid,
|
|
108
67
|
amt: amount,
|
|
109
|
-
|
|
110
|
-
rewardTitle: rewardTitle || '',
|
|
68
|
+
reward: rewardName // Store the specific reward name
|
|
111
69
|
};
|
|
112
|
-
|
|
113
70
|
const payload = safeStringify(logEntry);
|
|
114
71
|
if (!payload) return;
|
|
115
|
-
|
|
116
72
|
await db.listAppend('niki:kasa:history', payload);
|
|
117
73
|
await db.listTrim('niki:kasa:history', -100, -1);
|
|
118
74
|
}
|
|
119
75
|
|
|
120
76
|
// =========================
|
|
121
|
-
// INIT
|
|
77
|
+
// PLUGIN INIT
|
|
122
78
|
// =========================
|
|
123
79
|
Plugin.init = async function (params) {
|
|
124
80
|
const router = params.router;
|
|
125
81
|
const middleware = params.middleware;
|
|
126
82
|
|
|
127
|
-
//
|
|
128
|
-
// 1) HEARTBEAT (puan kazanma)
|
|
129
|
-
// -------------------------
|
|
83
|
+
// 1) HEARTBEAT (Earn Points)
|
|
130
84
|
router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
|
|
131
85
|
try {
|
|
132
86
|
const uid = req.uid;
|
|
133
|
-
const today =
|
|
87
|
+
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
134
88
|
const dailyKey = `niki:daily:${uid}:${today}`;
|
|
135
89
|
|
|
136
90
|
const currentDailyScore = parseInt((await db.getObjectField(dailyKey, 'score')) || 0, 10);
|
|
137
91
|
|
|
138
|
-
if (
|
|
92
|
+
if (currentDailyScore >= SETTINGS.dailyCap) {
|
|
139
93
|
return res.json({ earned: false, reason: 'daily_cap' });
|
|
140
94
|
}
|
|
141
95
|
|
|
142
96
|
await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
|
|
143
97
|
await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
|
|
144
98
|
|
|
145
|
-
const newBalance =
|
|
146
|
-
|
|
147
|
-
await addUserLog(uid, 'earn', SETTINGS.pointsPerHeartbeat, 'Aktiflik Puanı ⚡');
|
|
148
|
-
|
|
99
|
+
const newBalance = await user.getUserField(uid, 'niki_points');
|
|
149
100
|
return res.json({ earned: true, points: SETTINGS.pointsPerHeartbeat, total: newBalance });
|
|
150
101
|
} catch (err) {
|
|
151
102
|
return res.status(500).json({ earned: false, reason: 'server_error' });
|
|
152
103
|
}
|
|
153
104
|
});
|
|
154
105
|
|
|
155
|
-
//
|
|
156
|
-
// 2) WALLET DATA (cüzdan + geçmiş + rewards)
|
|
157
|
-
// -------------------------
|
|
106
|
+
// 2) WALLET DATA (Data + History + Rewards Info)
|
|
158
107
|
router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
|
|
159
108
|
try {
|
|
160
109
|
const uid = req.uid;
|
|
161
|
-
const today =
|
|
110
|
+
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
162
111
|
|
|
163
112
|
const [userData, dailyData, historyRaw] = await Promise.all([
|
|
164
113
|
user.getUserFields(uid, ['niki_points']),
|
|
@@ -172,144 +121,36 @@ Plugin.init = async function (params) {
|
|
|
172
121
|
let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
|
|
173
122
|
if (dailyPercent > 100) dailyPercent = 100;
|
|
174
123
|
|
|
175
|
-
const barPercent = Math.min(100, (currentPoints / SETTINGS.barMax) * 100);
|
|
176
|
-
|
|
177
124
|
const history = (historyRaw || [])
|
|
178
125
|
.map(safeParseMaybeJson)
|
|
179
126
|
.filter(Boolean)
|
|
180
127
|
.reverse();
|
|
181
128
|
|
|
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
129
|
return res.json({
|
|
192
130
|
points: currentPoints,
|
|
193
|
-
|
|
194
131
|
dailyScore,
|
|
195
132
|
dailyCap: SETTINGS.dailyCap,
|
|
196
133
|
dailyPercent,
|
|
197
|
-
|
|
198
|
-
barMax: SETTINGS.barMax,
|
|
199
|
-
barPercent,
|
|
200
|
-
|
|
201
|
-
rewards,
|
|
202
134
|
history,
|
|
135
|
+
rewards: REWARDS // Send reward tiers to frontend
|
|
203
136
|
});
|
|
204
137
|
} 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
|
-
});
|
|
138
|
+
return res.status(500).json({ points: 0, history: [] });
|
|
215
139
|
}
|
|
216
140
|
});
|
|
217
141
|
|
|
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,
|
|
259
|
-
});
|
|
260
|
-
|
|
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
|
-
});
|
|
277
|
-
} catch (err) {
|
|
278
|
-
return res.status(500).json({ success: false, message: 'Sunucu hatası' });
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
// -------------------------
|
|
284
|
-
// 4) KASA HISTORY (admin/mod)
|
|
285
|
-
// -------------------------
|
|
142
|
+
// 3) KASA HISTORY
|
|
286
143
|
router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
|
|
287
144
|
try {
|
|
288
|
-
const
|
|
289
|
-
|
|
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([]);
|
|
290
148
|
|
|
291
149
|
const raw = await db.getListRange('niki:kasa:history', 0, -1);
|
|
150
|
+
const rows = (raw || []).map(safeParseMaybeJson).filter(Boolean).reverse();
|
|
292
151
|
|
|
293
|
-
const
|
|
294
|
-
|
|
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
|
-
|
|
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']);
|
|
313
154
|
const userMap = {};
|
|
314
155
|
(users || []).forEach(u => { userMap[u.uid] = u; });
|
|
315
156
|
|
|
@@ -319,13 +160,11 @@ Plugin.init = async function (params) {
|
|
|
319
160
|
const uid = parseInt(r.cuid, 10);
|
|
320
161
|
const u = userMap[uid];
|
|
321
162
|
if (!u) return r;
|
|
322
|
-
|
|
323
163
|
return {
|
|
324
164
|
...r,
|
|
325
165
|
cust: u.username || r.cust || 'Bilinmeyen',
|
|
326
166
|
userslug: u.userslug || r.userslug || '',
|
|
327
167
|
picture: u.picture || r.picture || '',
|
|
328
|
-
iconBg: u['icon:bgColor'] || r.iconBg || '#4b5563',
|
|
329
168
|
profileUrl: (u.userslug ? `${rp}/user/${u.userslug}` : ''),
|
|
330
169
|
};
|
|
331
170
|
});
|
|
@@ -336,85 +175,20 @@ Plugin.init = async function (params) {
|
|
|
336
175
|
}
|
|
337
176
|
});
|
|
338
177
|
|
|
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ı!',
|
|
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
|
-
// -------------------------
|
|
178
|
+
// 4) GENERATE QR (Check if user has enough for MINIMUM reward)
|
|
407
179
|
router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
|
|
408
180
|
try {
|
|
409
181
|
const uid = req.uid;
|
|
410
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;
|
|
411
186
|
|
|
412
|
-
if (!TEST_MODE_UNLIMITED && points <
|
|
413
|
-
return res.json({ success: false, message:
|
|
187
|
+
if (!TEST_MODE_UNLIMITED && points < minCost) {
|
|
188
|
+
return res.json({ success: false, message: `En az ${minCost} puan gerekli.` });
|
|
414
189
|
}
|
|
415
190
|
|
|
416
|
-
const token =
|
|
417
|
-
|
|
191
|
+
const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
418
192
|
await db.set(`niki:qr:${token}`, uid);
|
|
419
193
|
await db.expire(`niki:qr:${token}`, 120);
|
|
420
194
|
|
|
@@ -423,75 +197,55 @@ router.post('/api/niki-loyalty/scan-coupon', middleware.ensureLoggedIn, async (r
|
|
|
423
197
|
return res.status(500).json({ success: false, message: 'Sunucu hatası' });
|
|
424
198
|
}
|
|
425
199
|
});
|
|
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
|
-
|
|
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
200
|
|
|
201
|
+
// 5) SCAN QR (Determine Reward & Deduct Points)
|
|
466
202
|
router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
|
|
467
203
|
try {
|
|
468
204
|
const token = (req.body && req.body.token) ? String(req.body.token) : '';
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
if (!
|
|
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' });
|
|
472
208
|
|
|
473
209
|
const customerUid = await db.get(`niki:qr:${token}`);
|
|
474
210
|
if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
|
|
475
211
|
|
|
476
212
|
const pts = parseInt((await user.getUserField(customerUid, 'niki_points')) || 0, 10);
|
|
477
|
-
|
|
478
|
-
|
|
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];
|
|
479
229
|
}
|
|
480
230
|
|
|
231
|
+
// Deduct Points
|
|
481
232
|
if (!TEST_MODE_UNLIMITED) {
|
|
482
|
-
await user.decrementUserFieldBy(customerUid, 'niki_points',
|
|
233
|
+
await user.decrementUserFieldBy(customerUid, 'niki_points', selectedReward.cost);
|
|
483
234
|
}
|
|
484
235
|
|
|
485
236
|
await db.delete(`niki:qr:${token}`);
|
|
486
237
|
|
|
487
238
|
const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
|
|
488
239
|
|
|
489
|
-
|
|
490
|
-
await
|
|
240
|
+
// Logs
|
|
241
|
+
await addUserLog(customerUid, 'spend', selectedReward.cost, selectedReward.name);
|
|
242
|
+
await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid, selectedReward.name, selectedReward.cost);
|
|
491
243
|
|
|
492
244
|
return res.json({
|
|
493
245
|
success: true,
|
|
494
246
|
customer: customerData,
|
|
247
|
+
rewardName: selectedReward.name,
|
|
248
|
+
cost: selectedReward.cost,
|
|
495
249
|
message: 'Onaylandı!',
|
|
496
250
|
});
|
|
497
251
|
} catch (err) {
|
|
@@ -499,41 +253,23 @@ router.post('/api/niki-loyalty/claim-auto', middleware.ensureLoggedIn, async (re
|
|
|
499
253
|
}
|
|
500
254
|
});
|
|
501
255
|
|
|
502
|
-
//
|
|
503
|
-
// 7) SAYFA ROTASI (kasa sayfası)
|
|
504
|
-
// -------------------------
|
|
256
|
+
// 6) PAGE ROUTES
|
|
505
257
|
routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
|
|
506
|
-
const
|
|
507
|
-
|
|
258
|
+
const isAdmin = await user.isAdministrator(req.uid);
|
|
259
|
+
const isMod = await user.isGlobalModerator(req.uid);
|
|
260
|
+
if (!isAdmin && !isMod) return res.render('403', {});
|
|
508
261
|
return res.render('niki-kasa', { title: 'Niki Kasa' });
|
|
509
262
|
});
|
|
510
263
|
};
|
|
511
264
|
|
|
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;
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
// client.js inject
|
|
522
265
|
Plugin.addScripts = async function (scripts) {
|
|
523
266
|
scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
|
|
524
267
|
return scripts;
|
|
525
268
|
};
|
|
526
269
|
|
|
527
|
-
// navigation
|
|
528
270
|
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
|
-
});
|
|
271
|
+
nav.push({ route: '/niki-wallet', title: 'Niki Cüzdan', enabled: true, iconClass: 'fa-coffee', text: 'Niki Cüzdan' });
|
|
536
272
|
return nav;
|
|
537
273
|
};
|
|
538
274
|
|
|
539
|
-
module.exports = Plugin;
|
|
275
|
+
module.exports = Plugin;
|