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