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 +183 -506
- package/package.json +1 -1
- package/static/lib/client.js +104 -448
- package/templates/niki-wallet.tpl +29 -164
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
524
|
-
|
|
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
|
-
|
|
530
|
-
|
|
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;
|