nodebb-plugin-calendar-onekite 11.1.33 → 11.1.35

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/lib/admin.js CHANGED
@@ -1,135 +1,277 @@
1
-
2
1
  'use strict';
3
2
 
4
3
  const meta = require.main.require('./src/meta');
5
- const User = require.main.require('./src/user');
6
- const helloasso = require('./helloasso');
7
- const reservationDb = require('./db');
8
- const { csvToList, toDateOnlyISO } = require('./utils');
4
+ const user = require.main.require('./src/user');
5
+ const emailer = require.main.require('./src/emailer');
9
6
 
10
- async function getSettings(req, res) {
11
- const s = await meta.settings.get('calendar-onekite');
12
- if (s && s.helloassoClientSecret) {
13
- s.helloassoClientSecret = '***';
7
+ async function sendEmail(template, toEmail, subject, data) {
8
+ if (!toEmail) return;
9
+ try {
10
+ if (typeof emailer.sendToEmail === 'function') {
11
+ await emailer.sendToEmail(template, toEmail, subject, data);
12
+ return;
13
+ }
14
+ if (typeof emailer.send === 'function') {
15
+ if (emailer.send.length >= 4) {
16
+ await emailer.send(template, toEmail, subject, data);
17
+ return;
18
+ }
19
+ if (emailer.send.length === 3) {
20
+ await emailer.send(template, toEmail, data);
21
+ return;
22
+ }
23
+ await emailer.send(template, toEmail, subject, data);
24
+ }
25
+ } catch (err) {
26
+ console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String(err && err.message || err) });
14
27
  }
15
- res.json({ ok:true, settings: s || {} });
16
28
  }
17
29
 
18
- async function saveSettings(req, res) {
19
- const payload = req.body || {};
20
- const normalizeCsv = (v) => String(v || '').split(',').map(s => s.trim()).filter(Boolean).join(',');
30
+ const dbLayer = require('./db');
31
+ const helloasso = require('./helloasso');
21
32
 
22
- const current = await meta.settings.get('calendar-onekite') || {};
23
- const toSave = {
24
- helloassoEnv: payload.helloassoEnv || current.helloassoEnv || 'sandbox',
25
- helloassoClientId: payload.helloassoClientId ?? current.helloassoClientId ?? '',
26
- helloassoOrganizationSlug: payload.helloassoOrganizationSlug ?? current.helloassoOrganizationSlug ?? '',
27
- helloassoFormType: payload.helloassoFormType || current.helloassoFormType || 'shop',
28
- helloassoFormSlug: payload.helloassoFormSlug ?? current.helloassoFormSlug ?? '',
33
+ const ADMIN_PRIV = 'admin:settings';
29
34
 
30
- creatorGroups: normalizeCsv(payload.creatorGroups ?? current.creatorGroups),
31
- validatorGroups: normalizeCsv(payload.validatorGroups ?? current.validatorGroups),
32
- notifyGroups: normalizeCsv(payload.notifyGroups ?? current.notifyGroups),
35
+ const admin = {};
33
36
 
34
- holdMinutes: Math.max(1, parseInt(payload.holdMinutes ?? current.holdMinutes, 10) || 5),
35
- };
37
+ admin.renderAdmin = async function (req, res) {
38
+ res.render('admin/plugins/calendar-onekite', {
39
+ title: 'Calendar OneKite',
40
+ });
41
+ };
36
42
 
37
- // secret only if provided and not masked
38
- if (payload.helloassoClientSecret && payload.helloassoClientSecret !== '***') {
39
- toSave.helloassoClientSecret = payload.helloassoClientSecret;
40
- } else if (current.helloassoClientSecret) {
41
- toSave.helloassoClientSecret = current.helloassoClientSecret;
42
- }
43
+ admin.getSettings = async function (req, res) {
44
+ const settings = await meta.settings.get('calendar-onekite');
45
+ res.json(settings || {});
46
+ };
43
47
 
44
- await meta.settings.set('calendar-onekite', toSave);
45
- res.json({ ok:true });
46
- }
48
+ admin.saveSettings = async function (req, res) {
49
+ await meta.settings.set('calendar-onekite', req.body || {});
50
+ res.json({ ok: true });
51
+ };
47
52
 
48
- async function listPending(req, res) {
49
- // Fetch by broad range (last 2 years) and filter pending
50
- const now = Date.now();
51
- const ids = await reservationDb.listReservationIdsByStartRange(now - 86400000*365*2, now + 86400000*365*2);
53
+ admin.listPending = async function (req, res) {
54
+ const ids = await dbLayer.listAllReservationIds(5000);
52
55
  const pending = [];
53
56
  for (const rid of ids) {
54
- const r = await reservationDb.getReservation(rid);
55
- if (!r) continue;
56
- if (r.status !== 'pending') continue;
57
- const user = await User.getUserFields(Number(r.uid), ['username']);
58
- pending.push({
59
- rid,
60
- username: user?.username || '',
61
- itemName: r.itemName,
62
- startDate: toDateOnlyISO(Number(r.startTs)),
63
- endDate: toDateOnlyISO(Number(r.endTs)),
64
- status: r.status,
57
+ const r = await dbLayer.getReservation(rid);
58
+ if (r && r.status === 'pending') {
59
+ pending.push(r);
60
+ }
61
+ }
62
+ pending.sort((a, b) => parseInt(a.start, 10) - parseInt(b.start, 10));
63
+ res.json(pending);
64
+ };
65
+
66
+ admin.approveReservation = async function (req, res) {
67
+ const rid = req.params.rid;
68
+ const r = await dbLayer.getReservation(rid);
69
+ if (!r) return res.status(404).json({ error: 'not-found' });
70
+
71
+ r.status = 'approved';
72
+ r.adminNote = String((req.body && (req.body.adminNote || req.body.note)) || '').trim();
73
+ r.pickupTime = String((req.body && (req.body.pickupTime || req.body.pickup)) || '').trim();
74
+ r.approvedAt = Date.now();
75
+
76
+ // Create HelloAsso payment link if configured
77
+ const settings = await meta.settings.get('calendar-onekite');
78
+ const env = settings.helloassoEnv || 'prod';
79
+ const token = await helloasso.getAccessToken({
80
+ env,
81
+ clientId: settings.helloassoClientId,
82
+ clientSecret: settings.helloassoClientSecret,
83
+ });
84
+
85
+ let paymentUrl = null;
86
+ if (token) {
87
+ const requester = await user.getUserFields(r.uid, ['username', 'email']);
88
+ // Determine amount from HelloAsso items (pricing comes from HelloAsso)
89
+ let totalAmount = 0;
90
+ try {
91
+ const items = await helloasso.listItems({
92
+ env,
93
+ token,
94
+ organizationSlug: settings.helloassoOrganizationSlug,
95
+ formType: settings.helloassoFormType,
96
+ formSlug: settings.helloassoFormSlug,
97
+ });
98
+ const normalized = (items || []).map((it) => ({
99
+ id: String(it.id || it.itemId || it.reference || it.name),
100
+ price: it.price || it.amount || it.unitPrice || 0,
101
+ })).filter(it => it.id);
102
+ const match = normalized.find(it => it.id === String(r.itemId));
103
+ totalAmount = match ? parseInt(match.price, 10) || 0 : 0;
104
+ } catch (e) {
105
+ totalAmount = 0;
106
+ }
107
+
108
+ if (!totalAmount) {
109
+ return res.status(400).json({ error: 'item-price-not-found' });
110
+ }
111
+ paymentUrl = await helloasso.createCheckoutIntent({
112
+ env,
113
+ token,
114
+ organizationSlug: settings.helloassoOrganizationSlug,
115
+ formType: settings.helloassoFormType,
116
+ formSlug: settings.helloassoFormSlug,
117
+ totalAmount,
118
+ payerEmail: requester && requester.email,
65
119
  });
66
120
  }
67
- res.json({ ok:true, pending });
68
- }
69
121
 
70
- async function approveReservation(req, res) {
71
- const api = require('./api');
72
- return api.approveReservation(req, res);
73
- }
122
+ if (paymentUrl) {
123
+ r.paymentUrl = paymentUrl;
124
+ }
74
125
 
75
- async function refuseReservation(req, res) {
76
- const api = require('./api');
77
- return api.refuseReservation(req, res);
78
- }
126
+ await dbLayer.saveReservation(r);
127
+
128
+ // Email requester
129
+ try {
130
+ const requester = await user.getUserFields(r.uid, ['username', 'email']);
131
+ if (requester && requester.email) {
132
+ await sendEmail('calendar-onekite_approved', requester.email, 'Réservation validée', {
133
+ username: requester.username,
134
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
135
+ start: new Date(parseInt(r.start, 10)).toISOString(),
136
+ end: new Date(parseInt(r.end, 10)).toISOString(),
137
+ paymentUrl: paymentUrl || '',
138
+ adminNote: r.adminNote || '',
139
+ pickupTime: r.pickupTime || '',
140
+ });
141
+ }
142
+ } catch (e) {}
143
+
144
+ res.json({ ok: true, paymentUrl: paymentUrl || null });
145
+ };
146
+
147
+ admin.refuseReservation = async function (req, res) {
148
+ const rid = req.params.rid;
149
+ const r = await dbLayer.getReservation(rid);
150
+ if (!r) return res.status(404).json({ error: 'not-found' });
151
+
152
+ r.status = 'refused';
153
+ await dbLayer.saveReservation(r);
154
+
155
+ try {
156
+ const requester = await user.getUserFields(r.uid, ['username', 'email']);
157
+ if (requester && requester.email) {
158
+ await sendEmail('calendar-onekite_refused', requester.email, 'Réservation refusée', {
159
+ username: requester.username,
160
+ itemName: r.itemName,
161
+ start: new Date(parseInt(r.start, 10)).toISOString(),
162
+ end: new Date(parseInt(r.end, 10)).toISOString(),
163
+ });
164
+ }
165
+ } catch (e) {}
166
+
167
+ res.json({ ok: true });
168
+ };
79
169
 
80
- async function purgeByYear(req, res) {
81
- const year = String(req.body.year || '').trim();
170
+ admin.purgeByYear = async function (req, res) {
171
+ const year = (req.body && req.body.year ? String(req.body.year) : '').trim();
82
172
  if (!/^\d{4}$/.test(year)) {
83
- return res.status(400).json({ ok:false, error:'invalid-year' });
173
+ return res.status(400).json({ error: 'invalid-year' });
84
174
  }
85
175
  const y = parseInt(year, 10);
86
- const start = Date.UTC(y,0,1,0,0,0,0);
87
- const end = Date.UTC(y+1,0,1,0,0,0,0);
176
+ const startTs = new Date(Date.UTC(y, 0, 1)).getTime();
177
+ const endTs = new Date(Date.UTC(y + 1, 0, 1)).getTime() - 1;
88
178
 
89
- const ids = await reservationDb.listReservationIdsByStartRange(start, end, 100000);
179
+ const ids = await dbLayer.listReservationIdsByStartRange(startTs, endTs, 100000);
180
+ let count = 0;
90
181
  for (const rid of ids) {
91
- const r = await reservationDb.getReservation(rid);
92
- if (!r) continue;
93
- const st = Number(r.startTs);
94
- if (st >= start && st < end) await reservationDb.deleteReservation(rid);
182
+ await dbLayer.removeReservation(rid);
183
+ count++;
95
184
  }
96
- res.json({ ok:true, purged: ids.length });
97
- }
185
+ res.json({ ok: true, removed: count });
186
+ };
187
+
188
+ // Debug endpoint to validate HelloAsso connectivity and item loading
189
+ admin.debugHelloAsso = async function (req, res) {
190
+ const settings = await meta.settings.get('calendar-onekite');
191
+ const env = (settings && settings.helloassoEnv) || 'prod';
192
+
193
+ // Never expose secrets in debug output
194
+ const safeSettings = {
195
+ helloassoEnv: env,
196
+ helloassoClientId: settings && settings.helloassoClientId ? String(settings.helloassoClientId) : '',
197
+ helloassoClientSecret: settings && settings.helloassoClientSecret ? '***' : '',
198
+ helloassoOrganizationSlug: settings && settings.helloassoOrganizationSlug ? String(settings.helloassoOrganizationSlug) : '',
199
+ helloassoFormType: settings && settings.helloassoFormType ? String(settings.helloassoFormType) : '',
200
+ helloassoFormSlug: settings && settings.helloassoFormSlug ? String(settings.helloassoFormSlug) : '',
201
+ };
202
+
203
+ const out = {
204
+ ok: true,
205
+ settings: safeSettings,
206
+ token: { ok: false },
207
+ // Catalog = what you actually want for a shop (available products/material)
208
+ catalog: { ok: false, count: 0, sample: [], keys: [] },
209
+ // Sold items = items present in orders (can be 0 if no sales yet)
210
+ soldItems: { ok: false, count: 0, sample: [] },
211
+ };
98
212
 
99
- async function debugHelloAsso(req, res) {
100
- const s = await meta.settings.get('calendar-onekite') || {};
101
- const safe = { ...s, helloassoClientSecret: s.helloassoClientSecret ? '***' : '' };
102
213
  try {
103
- const tokenOk = !!(await helloasso.getToken(s));
104
- const cat = await helloasso.getCatalog(s);
105
- res.json({
106
- ok:true,
107
- settings: safe,
108
- token: { ok: tokenOk },
109
- items: { ok:true, count: (cat.items||[]).length, sample: (cat.items||[]).slice(0,10) }
110
- });
111
- } catch (e) {
112
- res.json({
113
- ok:true,
114
- settings: safe,
115
- token: { ok:false },
116
- items: { ok:false, count: 0, sample: [] },
117
- error: String(e && e.message || e),
214
+ const token = await helloasso.getAccessToken({
215
+ env,
216
+ clientId: settings.helloassoClientId,
217
+ clientSecret: settings.helloassoClientSecret,
118
218
  });
119
- }
120
- }
219
+ if (!token) {
220
+ out.token = { ok: false, error: 'token-null' };
221
+ return res.json(out);
222
+ }
223
+ out.token = { ok: true };
121
224
 
122
- async function renderAdmin(req, res) {
123
- res.render('admin/plugins/calendar-onekite', {});
124
- }
225
+ // Catalog items (via /public)
226
+ try {
227
+ const { publicForm, items } = await helloasso.listCatalogItems({
228
+ env,
229
+ token,
230
+ organizationSlug: settings.helloassoOrganizationSlug,
231
+ formType: settings.helloassoFormType,
232
+ formSlug: settings.helloassoFormSlug,
233
+ });
234
+
235
+ const arr = Array.isArray(items) ? items : [];
236
+ out.catalog.ok = true;
237
+ out.catalog.count = arr.length;
238
+ out.catalog.keys = publicForm && typeof publicForm === 'object' ? Object.keys(publicForm) : [];
239
+ out.catalog.sample = arr.slice(0, 10).map((it) => ({
240
+ id: it.id,
241
+ name: it.name,
242
+ price: it.price ?? null,
243
+ }));
244
+ } catch (e) {
245
+ out.catalog = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [], keys: [] };
246
+ }
247
+
248
+ // Sold items
249
+ try {
250
+ const items = await helloasso.listItems({
251
+ env,
252
+ token,
253
+ organizationSlug: settings.helloassoOrganizationSlug,
254
+ formType: settings.helloassoFormType,
255
+ formSlug: settings.helloassoFormSlug,
256
+ });
257
+ const arr = Array.isArray(items) ? items : [];
258
+ out.soldItems.ok = true;
259
+ out.soldItems.count = arr.length;
260
+ out.soldItems.sample = arr.slice(0, 10).map((it) => ({
261
+ id: it.id || it.itemId || it.reference || it.name,
262
+ name: it.name || it.label || it.itemName,
263
+ price: it.price || it.amount || it.unitPrice || null,
264
+ }));
265
+ } catch (e) {
266
+ out.soldItems = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [] };
267
+ }
125
268
 
126
- module.exports = {
127
- renderAdmin,
128
- getSettings,
129
- saveSettings,
130
- listPending,
131
- approveReservation,
132
- refuseReservation,
133
- purgeByYear,
134
- debugHelloAsso,
269
+ return res.json(out);
270
+ } catch (e) {
271
+ out.ok = false;
272
+ out.token = { ok: false, error: String(e && e.message ? e.message : e) };
273
+ return res.json(out);
274
+ }
135
275
  };
276
+
277
+ module.exports = admin;