nodebb-plugin-calendar-onekite 10.0.17 → 10.0.18

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/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # nodebb-plugin-calendar-onekite
2
+
3
+ Plugin NodeBB (testé avec NodeBB v4.x) qui ajoute un calendrier de réservation de matériel basé sur FullCalendar.
4
+
5
+ ## Fonctionnalités
6
+
7
+ - Page publique: `/calendar`
8
+ - Création de demandes de réservation (clic ou sélection de plage)
9
+ - Blocage temporaire des demandes en attente (expiration automatique)
10
+ - Validation/refus via l'ACP
11
+ - Synchronisation des items (matériel + prix) depuis HelloAsso
12
+ - Création de checkout-intents HelloAsso lors de la validation
13
+ - Purge du calendrier par année
14
+
15
+ ## Installation
16
+
17
+ Dans le dossier NodeBB:
18
+
19
+ ```bash
20
+ npm install /path/to/nodebb-plugin-calendar-onekite
21
+ ./nodebb build
22
+ ./nodebb restart
23
+ ```
24
+
25
+ Activez le plugin dans l'ACP.
26
+
27
+ ## Configuration (ACP)
28
+
29
+ ACP → Plugins → Calendar (OneKite)
30
+
31
+ - Groupes autorisés à demander une réservation (CSV)
32
+ - Groupes notifiés par email (CSV)
33
+ - Durée de blocage en attente (minutes)
34
+ - Paramètres HelloAsso (sandbox/prod, clientId/secret, organizationSlug, formType, formSlug, returnURL, ...)
35
+
36
+ ## Webhook HelloAsso
37
+
38
+ Endpoint: `/api/calendar-onekite/helloasso/webhook`
39
+
40
+ > Le plugin fournit un endpoint minimal pour marquer une réservation comme payée à partir d'un `checkoutIntentId`.
41
+ > Adaptez-le selon votre stratégie (notification HelloAsso / polling / etc.).
42
+
@@ -0,0 +1,534 @@
1
+ 'use strict';
2
+
3
+ const axios = require('axios');
4
+
5
+ const db = require.main.require('./src/database');
6
+ const meta = require.main.require('./src/meta');
7
+ const groups = require.main.require('./src/groups');
8
+ const user = require.main.require('./src/user');
9
+ const emailer = require.main.require('./src/emailer');
10
+ const plugins = require.main.require('./src/plugins');
11
+ const winston = require.main.require('./src/logger');
12
+ const nconf = require.main.require('nconf');
13
+
14
+ const SETTINGS_KEY = 'calendar-onekite';
15
+ const RES_COUNTER_KEY = 'calendar-onekite:nextid';
16
+ const RES_HASH_PREFIX = 'calendar-onekite:res:';
17
+ const RES_ZSET_BY_START = 'calendar-onekite:res:byStart';
18
+ const ITEMS_KEY = 'calendar-onekite:items';
19
+
20
+ let backgroundStarted = false;
21
+
22
+ function nowMs() {
23
+ return Date.now();
24
+ }
25
+
26
+ function parseCsv(str) {
27
+ return (str || '')
28
+ .split(',')
29
+ .map(s => s.trim())
30
+ .filter(Boolean);
31
+ }
32
+
33
+ async function getSettings() {
34
+ const defaults = {
35
+ requesterGroups: 'registered-users',
36
+ notifyGroups: 'administrators',
37
+ pendingHoldMinutes: '5',
38
+ apiEnv: 'sandbox',
39
+ helloassoClientId: '',
40
+ helloassoClientSecret: '',
41
+ helloassoOrganizationSlug: '',
42
+ helloassoFormType: '',
43
+ helloassoFormSlug: '',
44
+ helloassoReturnUrl: '',
45
+ helloassoBackUrl: '',
46
+ helloassoErrorUrl: '',
47
+ helloassoWebhookSecret: ''
48
+ };
49
+ const settings = await meta.settings.get(SETTINGS_KEY);
50
+ return Object.assign({}, defaults, settings || {});
51
+ }
52
+
53
+ function helloAssoBaseUrl(apiEnv) {
54
+ // HelloAsso docs show api.helloasso.com & api.helloasso-sandbox.com
55
+ return apiEnv === 'prod' ? 'https://api.helloasso.com' : 'https://api.helloasso-sandbox.com';
56
+ }
57
+
58
+ async function helloAssoGetToken(settings) {
59
+ const base = helloAssoBaseUrl(settings.apiEnv);
60
+ const url = `${base}/oauth2/token`;
61
+ const body = new URLSearchParams({
62
+ grant_type: 'client_credentials',
63
+ client_id: settings.helloassoClientId,
64
+ client_secret: settings.helloassoClientSecret,
65
+ });
66
+ const res = await axios.post(url, body, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
67
+ return res.data.access_token;
68
+ }
69
+
70
+ async function helloAssoFetchItems(settings) {
71
+ const token = await helloAssoGetToken(settings);
72
+ const base = helloAssoBaseUrl(settings.apiEnv);
73
+ const url = `${base}/v5/organizations/${encodeURIComponent(settings.helloassoOrganizationSlug)}/forms/${encodeURIComponent(settings.helloassoFormType)}/${encodeURIComponent(settings.helloassoFormSlug)}/items`;
74
+ const res = await axios.get(url, { headers: { Authorization: `Bearer ${token}` } });
75
+ // Normalise to our format
76
+ const items = (res.data?.data || res.data || []).map(it => ({
77
+ id: String(it.id ?? it.itemId ?? it.item?.id ?? ''),
78
+ name: it.name ?? it.itemName ?? it.item?.name ?? 'Item',
79
+ price: (it.price ?? it.amount ?? it.unitPrice ?? 0),
80
+ raw: it,
81
+ })).filter(it => it.id);
82
+ return items;
83
+ }
84
+
85
+ async function helloAssoCreateCheckoutIntent(settings, reservation) {
86
+ const token = await helloAssoGetToken(settings);
87
+ const base = helloAssoBaseUrl(settings.apiEnv);
88
+ const url = `${base}/v5/organizations/${encodeURIComponent(settings.helloassoOrganizationSlug)}/checkout-intents`;
89
+
90
+ const amountCents = Number(reservation.priceCents || 0);
91
+ const body = {
92
+ totalAmount: amountCents,
93
+ initialAmount: amountCents,
94
+ itemName: reservation.itemName,
95
+ backUrl: settings.helloassoBackUrl || undefined,
96
+ errorUrl: settings.helloassoErrorUrl || undefined,
97
+ returnUrl: settings.helloassoReturnUrl || undefined,
98
+ metadata: {
99
+ source: 'nodebb-plugin-calendar-onekite',
100
+ reservationId: reservation.id,
101
+ uid: reservation.uid,
102
+ },
103
+ };
104
+
105
+ const res = await axios.post(url, body, { headers: { Authorization: `Bearer ${token}` } });
106
+ return {
107
+ checkoutIntentId: String(res.data?.id ?? res.data?.checkoutIntentId ?? ''),
108
+ redirectUrl: res.data?.redirectUrl || res.data?.url || res.data?.paymentUrl || res.data?.redirect || null,
109
+ raw: res.data,
110
+ };
111
+ }
112
+
113
+ async function reservationKey(id) {
114
+ return `${RES_HASH_PREFIX}${id}`;
115
+ }
116
+
117
+ async function getNextReservationId() {
118
+ const id = await db.incrObjectField('global', RES_COUNTER_KEY);
119
+ return String(id);
120
+ }
121
+
122
+ async function loadReservation(id) {
123
+ const data = await db.getObject(await reservationKey(id));
124
+ if (!data || !data.id) return null;
125
+ // Convert some fields
126
+ data.start = Number(data.start);
127
+ data.end = Number(data.end);
128
+ data.createdAt = Number(data.createdAt);
129
+ data.updatedAt = Number(data.updatedAt);
130
+ data.expiresAt = Number(data.expiresAt || 0);
131
+ data.priceCents = Number(data.priceCents || 0);
132
+ return data;
133
+ }
134
+
135
+ async function saveReservation(reservation) {
136
+ reservation.updatedAt = nowMs();
137
+ await db.setObject(await reservationKey(reservation.id), reservation);
138
+ await db.sortedSetAdd(RES_ZSET_BY_START, reservation.start, reservation.id);
139
+ }
140
+
141
+ async function deleteReservation(id) {
142
+ await db.delete(await reservationKey(id));
143
+ await db.sortedSetRemove(RES_ZSET_BY_START, id);
144
+ }
145
+
146
+ function isExpiredPending(reservation) {
147
+ return reservation.status === 'pending' && reservation.expiresAt && reservation.expiresAt <= nowMs();
148
+ }
149
+
150
+ async function expirePendingReservations() {
151
+ try {
152
+ // Scan last ~180 days (fast enough). If you need bigger, you can widen.
153
+ const to = nowMs();
154
+ const from = to - 180 * 24 * 60 * 60 * 1000;
155
+ const ids = await db.getSortedSetRangeByScore(RES_ZSET_BY_START, 0, -1, from, to);
156
+ for (const id of ids) {
157
+ const r = await loadReservation(id);
158
+ if (r && isExpiredPending(r)) {
159
+ r.status = 'expired';
160
+ await saveReservation(r);
161
+ }
162
+ }
163
+ } catch (e) {
164
+ winston.error(`[calendar-onekite] expire job error: ${e.message}`);
165
+ }
166
+ }
167
+
168
+ async function userInAllowedGroups(uid, allowedGroups) {
169
+ const groupNames = parseCsv(allowedGroups);
170
+ for (const g of groupNames) {
171
+ try {
172
+ const isMember = await groups.isMember(uid, g);
173
+ if (isMember) return true;
174
+ } catch (e) {
175
+ // ignore missing group
176
+ }
177
+ }
178
+ return false;
179
+ }
180
+
181
+ async function notifyGroupsByEmail(groupList, subject, body) {
182
+ const groupNames = parseCsv(groupList);
183
+ const uids = new Set();
184
+
185
+ for (const g of groupNames) {
186
+ try {
187
+ const members = await groups.getMembers(g, 0, 10000);
188
+ for (const m of members) uids.add(m.uid);
189
+ } catch (e) {
190
+ // ignore
191
+ }
192
+ }
193
+
194
+ if (!uids.size) return;
195
+
196
+ const users = await user.getUsersFields([...uids], ['email', 'username', 'uid']);
197
+ const emails = users.map(u => u.email).filter(Boolean);
198
+ if (!emails.length) return;
199
+
200
+ // NodeBB's emailer expects a template usually. We'll use sendToEmail with raw text as fallback.
201
+ // If sendToEmail isn't available, we log.
202
+ if (typeof emailer.sendToEmail === 'function') {
203
+ await Promise.all(emails.map(email => emailer.sendToEmail('calendar-onekite', email, {
204
+ subject,
205
+ text: body,
206
+ }).catch(() => {})));
207
+ } else if (typeof emailer.send === 'function') {
208
+ await Promise.all(emails.map(email => emailer.send('calendar-onekite', email, {
209
+ subject,
210
+ text: body,
211
+ }).catch(() => {})));
212
+ } else {
213
+ winston.warn('[calendar-onekite] emailer API not found on this NodeBB version');
214
+ }
215
+ }
216
+
217
+ function toFullCalendarEvent(r) {
218
+ const titleParts = [r.itemName];
219
+ if (r.status === 'pending') titleParts.push('⏳');
220
+ if (r.status === 'awaiting_payment') titleParts.push('✅ 💳');
221
+ if (r.status === 'paid') titleParts.push('✅');
222
+ if (r.status === 'refused') titleParts.push('❌');
223
+ if (r.status === 'expired') titleParts.push('⌛');
224
+
225
+ return {
226
+ id: r.id,
227
+ title: titleParts.join(' '),
228
+ start: new Date(r.start).toISOString(),
229
+ end: new Date(r.end).toISOString(),
230
+ allDay: false,
231
+ extendedProps: {
232
+ status: r.status,
233
+ itemId: r.itemId,
234
+ itemName: r.itemName,
235
+ priceCents: r.priceCents,
236
+ uid: r.uid,
237
+ username: r.username,
238
+ paymentUrl: r.paymentUrl || null,
239
+ },
240
+ };
241
+ }
242
+
243
+ const controllers = {};
244
+
245
+ controllers.ensureBackgroundJob = async function () {
246
+ if (backgroundStarted) return;
247
+ backgroundStarted = true;
248
+ // Periodically expire pending holds
249
+ setInterval(expirePendingReservations, 60 * 1000).unref();
250
+ // One immediate run
251
+ expirePendingReservations();
252
+ };
253
+
254
+ controllers.renderCalendar = async function (req, res) {
255
+ const settings = await getSettings();
256
+ res.render('calendar', {
257
+ title: 'Calendrier',
258
+ calendarOnekite: {
259
+ requesterGroups: settings.requesterGroups,
260
+ },
261
+ });
262
+ };
263
+
264
+ controllers.renderAdmin = async function (req, res) {
265
+ const settings = await getSettings();
266
+ res.render('admin/plugins/calendar-onekite', {
267
+ title: 'Calendar (OneKite)',
268
+ settings,
269
+ });
270
+ };
271
+
272
+ controllers.apiGetSettings = async function (req, res) {
273
+ const settings = await getSettings();
274
+ // Only expose non-secrets
275
+ res.json({
276
+ requesterGroups: settings.requesterGroups,
277
+ pendingHoldMinutes: Number(settings.pendingHoldMinutes || 5),
278
+ apiEnv: settings.apiEnv,
279
+ orgSlug: settings.helloassoOrganizationSlug,
280
+ formType: settings.helloassoFormType,
281
+ formSlug: settings.helloassoFormSlug,
282
+ });
283
+ };
284
+
285
+ controllers.apiGetItems = async function (req, res) {
286
+ const items = await db.getObjectField('global', ITEMS_KEY);
287
+ let parsed = [];
288
+ try {
289
+ parsed = JSON.parse(items || '[]');
290
+ } catch (e) {}
291
+ res.json({ items: parsed });
292
+ };
293
+
294
+ controllers.apiListReservations = async function (req, res) {
295
+ await expirePendingReservations();
296
+ const start = req.query.start ? Date.parse(req.query.start) : null;
297
+ const end = req.query.end ? Date.parse(req.query.end) : null;
298
+ const from = Number.isFinite(start) ? start : (nowMs() - 30 * 24 * 60 * 60 * 1000);
299
+ const to = Number.isFinite(end) ? end : (nowMs() + 180 * 24 * 60 * 60 * 1000);
300
+
301
+ const ids = await db.getSortedSetRangeByScore(RES_ZSET_BY_START, 0, -1, from, to);
302
+ const reservations = (await Promise.all(ids.map(loadReservation))).filter(Boolean);
303
+ const events = reservations
304
+ .filter(r => r.status !== 'cancelled')
305
+ .map(toFullCalendarEvent);
306
+
307
+ res.json(events);
308
+ };
309
+
310
+ controllers.apiCreateReservation = async function (req, res) {
311
+ const settings = await getSettings();
312
+ const uid = req.uid;
313
+
314
+ const allowed = await userInAllowedGroups(uid, settings.requesterGroups);
315
+ if (!allowed) {
316
+ return res.status(403).json({ error: 'not-allowed' });
317
+ }
318
+
319
+ const { itemId, itemName, priceCents, start, end } = req.body || {};
320
+ const startMs = Number(start);
321
+ const endMs = Number(end);
322
+
323
+ if (!itemId || !itemName || !Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) {
324
+ return res.status(400).json({ error: 'invalid-payload' });
325
+ }
326
+
327
+ // Ensure not already booked (pending/awaiting_payment/paid) overlapping on the same item
328
+ await expirePendingReservations();
329
+ const overlapIds = await db.getSortedSetRangeByScore(RES_ZSET_BY_START, 0, -1, startMs - 365 * 24 * 60 * 60 * 1000, endMs);
330
+ for (const id of overlapIds) {
331
+ const r = await loadReservation(id);
332
+ if (!r) continue;
333
+ const active = ['pending', 'awaiting_payment', 'paid'].includes(r.status);
334
+ const overlaps = r.itemId === String(itemId) && r.start < endMs && r.end > startMs;
335
+ if (active && overlaps) {
336
+ return res.status(409).json({ error: 'already-booked' });
337
+ }
338
+ }
339
+
340
+ const id = await getNextReservationId();
341
+ const u = await user.getUserFields(uid, ['username']);
342
+ const holdMinutes = Math.max(1, Number(settings.pendingHoldMinutes || 5));
343
+ const reservation = {
344
+ id,
345
+ uid: String(uid),
346
+ username: u.username || 'user',
347
+ itemId: String(itemId),
348
+ itemName: String(itemName),
349
+ priceCents: Number(priceCents || 0),
350
+ start: startMs,
351
+ end: endMs,
352
+ status: 'pending',
353
+ createdAt: nowMs(),
354
+ updatedAt: nowMs(),
355
+ expiresAt: nowMs() + holdMinutes * 60 * 1000,
356
+ };
357
+
358
+ await saveReservation(reservation);
359
+
360
+ // Notify admins by email
361
+ const subject = `[OneKite] Nouvelle demande de réservation (#${id})`;
362
+ const body = `Une nouvelle demande de réservation a été créée.
363
+
364
+ Matériel: ${reservation.itemName}
365
+ Début: ${new Date(reservation.start).toLocaleString()}
366
+ Fin: ${new Date(reservation.end).toLocaleString()}
367
+ Utilisateur: ${reservation.username} (uid ${reservation.uid})
368
+
369
+ À valider/refuser dans l'ACP: /admin/plugins/calendar-onekite`;
370
+ notifyGroupsByEmail(settings.notifyGroups, subject, body).catch(() => {});
371
+
372
+ res.json({ ok: true, id, status: reservation.status, expiresAt: reservation.expiresAt });
373
+ };
374
+
375
+ controllers.apiCancelReservation = async function (req, res) {
376
+ const id = req.params.id;
377
+ const r = await loadReservation(id);
378
+ if (!r) return res.status(404).json({ error: 'not-found' });
379
+
380
+ const isOwner = String(r.uid) === String(req.uid);
381
+ const isAdmin = req.user && req.user.isAdmin;
382
+ if (!isOwner && !isAdmin) return res.status(403).json({ error: 'forbidden' });
383
+
384
+ r.status = 'cancelled';
385
+ await saveReservation(r);
386
+ res.json({ ok: true });
387
+ };
388
+
389
+ controllers.apiAdminListPending = async function (req, res) {
390
+ await expirePendingReservations();
391
+ const to = nowMs() + 365 * 24 * 60 * 60 * 1000;
392
+ const from = nowMs() - 90 * 24 * 60 * 60 * 1000;
393
+ const ids = await db.getSortedSetRangeByScore(RES_ZSET_BY_START, 0, -1, from, to);
394
+ const reservations = (await Promise.all(ids.map(loadReservation))).filter(Boolean);
395
+ res.json({
396
+ pending: reservations.filter(r => r.status === 'pending'),
397
+ awaiting_payment: reservations.filter(r => r.status === 'awaiting_payment'),
398
+ });
399
+ };
400
+
401
+ controllers.apiAdminSaveSettings = async function (req, res) {
402
+ try {
403
+ const current = await getSettings();
404
+ const toSave = { ...current, ...(req.body || {}) };
405
+ // Do not allow purging settings key from request
406
+ delete toSave.purgeYear;
407
+
408
+ await meta.settings.set(SETTINGS_KEY, toSave);
409
+ res.json({ ok: true });
410
+ } catch (err) {
411
+ winston.error('[calendar-onekite] failed to save settings: ' + err.message);
412
+ res.status(500).json({ error: err.message });
413
+ }
414
+ };
415
+
416
+ controllers.apiApproveReservation = async function (req, res) {
417
+ const id = req.params.id;
418
+ const settings = await getSettings();
419
+ const r = await loadReservation(id);
420
+ if (!r) return res.status(404).json({ error: 'not-found' });
421
+ if (r.status !== 'pending') return res.status(409).json({ error: 'invalid-status' });
422
+ if (isExpiredPending(r)) {
423
+ r.status = 'expired';
424
+ await saveReservation(r);
425
+ return res.status(409).json({ error: 'expired' });
426
+ }
427
+
428
+ // Create checkout intent (if HelloAsso configured)
429
+ let payment = null;
430
+ if (settings.helloassoClientId && settings.helloassoClientSecret && settings.helloassoOrganizationSlug) {
431
+ try {
432
+ payment = await helloAssoCreateCheckoutIntent(settings, r);
433
+ } catch (e) {
434
+ winston.error(`[calendar-onekite] helloasso checkout intent failed: ${e.message}`);
435
+ }
436
+ }
437
+
438
+ r.status = 'awaiting_payment';
439
+ r.approvedBy = String(req.uid);
440
+ r.checkoutIntentId = payment?.checkoutIntentId || '';
441
+ r.paymentUrl = payment?.redirectUrl || '';
442
+ await saveReservation(r);
443
+
444
+ // Notify requester (email if available)
445
+ try {
446
+ const u = await user.getUserFields(Number(r.uid), ['email', 'username']);
447
+ if (u.email) {
448
+ const subject = `[OneKite] Réservation validée (#${r.id})`;
449
+ const body = `Votre réservation a été validée.
450
+
451
+ Matériel: ${r.itemName}
452
+ Début: ${new Date(r.start).toLocaleString()}
453
+ Fin: ${new Date(r.end).toLocaleString()}
454
+
455
+ Lien de paiement: ${r.paymentUrl || '(non disponible)'}
456
+
457
+ Merci.`;
458
+ await notifyGroupsByEmail('', subject, body); // no-op
459
+ if (typeof emailer.sendToEmail === 'function') {
460
+ await emailer.sendToEmail('calendar-onekite', u.email, { subject, text: body });
461
+ }
462
+ }
463
+ } catch (e) {}
464
+
465
+ res.json({ ok: true, paymentUrl: r.paymentUrl || null });
466
+ };
467
+
468
+ controllers.apiRefuseReservation = async function (req, res) {
469
+ const id = req.params.id;
470
+ const r = await loadReservation(id);
471
+ if (!r) return res.status(404).json({ error: 'not-found' });
472
+ if (r.status !== 'pending') return res.status(409).json({ error: 'invalid-status' });
473
+
474
+ r.status = 'refused';
475
+ r.refusedBy = String(req.uid);
476
+ await saveReservation(r);
477
+
478
+ res.json({ ok: true });
479
+ };
480
+
481
+ controllers.apiSyncItems = async function (req, res) {
482
+ const settings = await getSettings();
483
+ if (!settings.helloassoClientId || !settings.helloassoClientSecret || !settings.helloassoOrganizationSlug || !settings.helloassoFormType || !settings.helloassoFormSlug) {
484
+ return res.status(400).json({ error: 'missing-helloasso-settings' });
485
+ }
486
+
487
+ const items = await helloAssoFetchItems(settings);
488
+ await db.setObjectField('global', ITEMS_KEY, JSON.stringify(items));
489
+ res.json({ ok: true, count: items.length });
490
+ };
491
+
492
+ controllers.apiPurge = async function (req, res) {
493
+ const year = Number(req.body?.year);
494
+ if (!Number.isInteger(year) || year < 2000 || year > 2200) {
495
+ return res.status(400).json({ error: 'invalid-year' });
496
+ }
497
+ const from = Date.parse(`${year}-01-01T00:00:00.000Z`);
498
+ const to = Date.parse(`${year + 1}-01-01T00:00:00.000Z`);
499
+ const ids = await db.getSortedSetRangeByScore(RES_ZSET_BY_START, 0, -1, from, to);
500
+ for (const id of ids) {
501
+ await deleteReservation(id);
502
+ }
503
+ res.json({ ok: true, purged: ids.length });
504
+ };
505
+
506
+ controllers.apiHelloAssoWebhook = async function (req, res) {
507
+ // Basic webhook handler (must be configured on HelloAsso side).
508
+ const settings = await getSettings();
509
+ const secret = settings.helloassoWebhookSecret;
510
+ if (secret && req.headers['x-calendar-onekite-secret'] !== secret) {
511
+ return res.status(401).json({ error: 'unauthorized' });
512
+ }
513
+
514
+ const reservationId = req.body?.metadata?.reservationId || req.body?.reservationId;
515
+ const status = req.body?.status || req.body?.code || req.body?.eventType;
516
+
517
+ if (!reservationId) {
518
+ return res.status(400).json({ error: 'missing-reservationId' });
519
+ }
520
+
521
+ const r = await loadReservation(String(reservationId));
522
+ if (!r) return res.status(404).json({ error: 'not-found' });
523
+
524
+ // Heuristic: if webhook indicates succeeded, mark paid
525
+ const success = ['succeeded', 'paid', 'PAYMENT_SUCCEEDED', 'PaymentSucceeded'].includes(String(status));
526
+ if (success) {
527
+ r.status = 'paid';
528
+ await saveReservation(r);
529
+ }
530
+
531
+ res.json({ ok: true });
532
+ };
533
+
534
+ module.exports = controllers;
package/lib/routes.js ADDED
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ const controllers = require('./controllers');
4
+
5
+ module.exports.init = function (router, middleware) {
6
+ // Public calendar page
7
+ router.get('/calendar', middleware.buildHeader, controllers.renderCalendar);
8
+ router.get('/api/calendar', controllers.renderCalendar);
9
+
10
+ // Admin (ACP) page
11
+ router.get('/admin/plugins/calendar-onekite', middleware.admin.buildHeader, controllers.renderAdmin);
12
+ router.get('/api/admin/plugins/calendar-onekite', controllers.renderAdmin);
13
+
14
+ // Public API
15
+ router.get('/api/calendar-onekite/settings', controllers.apiGetSettings);
16
+ router.get('/api/calendar-onekite/items', controllers.apiGetItems);
17
+ router.get('/api/calendar-onekite/reservations', controllers.apiListReservations);
18
+ router.post('/api/calendar-onekite/reservations', middleware.ensureLoggedIn, controllers.apiCreateReservation);
19
+ router.delete('/api/calendar-onekite/reservations/:id', middleware.ensureLoggedIn, controllers.apiCancelReservation);
20
+
21
+ // Admin API
22
+ router.get('/api/admin/calendar-onekite/pending', middleware.admin.checkPrivileges, controllers.apiAdminListPending);
23
+ router.post('/api/admin/calendar-onekite/settings', middleware.admin.checkPrivileges, controllers.apiAdminSaveSettings);
24
+ router.put('/api/admin/calendar-onekite/reservations/:id/approve', middleware.admin.checkPrivileges, controllers.apiApproveReservation);
25
+ router.put('/api/admin/calendar-onekite/reservations/:id/refuse', middleware.admin.checkPrivileges, controllers.apiRefuseReservation);
26
+ router.post('/api/admin/calendar-onekite/sync-items', middleware.admin.checkPrivileges, controllers.apiSyncItems);
27
+ router.post('/api/admin/calendar-onekite/purge', middleware.admin.checkPrivileges, controllers.apiPurge);
28
+
29
+ // HelloAsso webhook
30
+ router.post('/api/calendar-onekite/helloasso/webhook', controllers.apiHelloAssoWebhook);
31
+ };