nodebb-plugin-onekite-calendar 2.0.30 → 2.0.32

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog – calendar-onekite
2
2
 
3
+ ## 1.3.27
4
+ - Refactor : ajout d'un module `lib/utils.js` (format dates FR, lecture settings ACP, helpers listes/UIDs) pour supprimer les duplications.
5
+ - Cleanup : suppression d'un doublon de fonctions dans `helloassoWebhook.js` (formatFR + getReservationIdFromPayload).
6
+ - Perf : scheduler optimisé (chargement **batch** des réservations via `getReservations()` au lieu d'un `getReservation()` par id).
7
+
3
8
  ## 1.3.26
4
9
  - Scheduler : garde "send-once" rendue **atomique** en multi-instance (priorité à `db.incrObjectField(key, rid)`), pour garantir qu'au moins une instance envoie les emails/Discord avant suppression.
5
10
  - Scheduler : en cas d'erreur DB sur la garde, on **privilégie l'envoi** plutôt que "expire sans email" (dernier recours : autoriser l'envoi même si la garde échoue).
package/lib/admin.js CHANGED
@@ -3,20 +3,7 @@
3
3
  const meta = require.main.require('./src/meta');
4
4
  const user = require.main.require('./src/user');
5
5
  const emailer = require.main.require('./src/emailer');
6
- const nconf = require.main.require('nconf');
7
-
8
- function forumBaseUrl() {
9
- const base = String(nconf.get('url') || '').trim().replace(/\/$/, '');
10
- return base;
11
- }
12
-
13
- function formatFR(tsOrIso) {
14
- const d = new Date(typeof tsOrIso === 'string' && /^[0-9]+$/.test(tsOrIso) ? parseInt(tsOrIso, 10) : tsOrIso);
15
- const dd = String(d.getDate()).padStart(2, '0');
16
- const mm = String(d.getMonth() + 1).padStart(2, '0');
17
- const yyyy = d.getFullYear();
18
- return `${dd}/${mm}/${yyyy}`;
19
- }
6
+ const { forumBaseUrl, formatFR } = require('./utils');
20
7
 
21
8
  function buildHelloAssoItemName(baseLabel, itemNames, start, end) {
22
9
  const base = String(baseLabel || 'Réservation matériel Onekite').trim();
package/lib/api.js CHANGED
@@ -11,6 +11,7 @@ const db = require.main.require('./src/database');
11
11
  const logger = require.main.require('./src/logger');
12
12
 
13
13
  const dbLayer = require('./db');
14
+ const { formatFR } = require('./utils');
14
15
 
15
16
  // Resolve group identifiers from ACP.
16
17
  // Admins may enter a group *name* ("Test") or a *slug* ("onekite-ffvl-2026").
@@ -177,14 +178,6 @@ function overlap(aStart, aEnd, bStart, bEnd) {
177
178
  }
178
179
 
179
180
 
180
- function formatFR(tsOrIso) {
181
- const d = new Date(typeof tsOrIso === 'string' && /^[0-9]+$/.test(tsOrIso) ? parseInt(tsOrIso, 10) : tsOrIso);
182
- const dd = String(d.getDate()).padStart(2, '0');
183
- const mm = String(d.getMonth() + 1).padStart(2, '0');
184
- const yyyy = d.getFullYear();
185
- return `${dd}/${mm}/${yyyy}`;
186
- }
187
-
188
181
  function buildHelloAssoItemName(baseLabel, itemNames, start, end) {
189
182
  const base = String(baseLabel || '').trim();
190
183
  const items = Array.isArray(itemNames) ? itemNames.map((s) => String(s || '').trim()).filter(Boolean) : [];
@@ -10,6 +10,7 @@ const dbLayer = require('./db');
10
10
  const helloasso = require('./helloasso');
11
11
  const discord = require('./discord');
12
12
  const realtime = require('./realtime');
13
+ const { formatFR } = require('./utils');
13
14
 
14
15
  async function auditLog(action, actorUid, payload) {
15
16
  try {
@@ -49,14 +50,6 @@ async function sendEmail(template, uid, subject, data) {
49
50
  }
50
51
  }
51
52
 
52
- function formatFR(tsOrIso) {
53
- const d = new Date(tsOrIso);
54
- const dd = String(d.getDate()).padStart(2, '0');
55
- const mm = String(d.getMonth() + 1).padStart(2, '0');
56
- const yyyy = d.getFullYear();
57
- return `${dd}/${mm}/${yyyy}`;
58
- }
59
-
60
53
  function getReservationIdFromPayload(payload) {
61
54
  try {
62
55
  const data = payload && payload.data ? payload.data : null;
@@ -186,27 +179,6 @@ function isConfirmedPayment(payload) {
186
179
  }
187
180
  }
188
181
 
189
- function formatFR(tsOrIso) {
190
- const d = new Date(tsOrIso);
191
- const dd = String(d.getDate()).padStart(2, '0');
192
- const mm = String(d.getMonth() + 1).padStart(2, '0');
193
- const yyyy = d.getFullYear();
194
- return `${dd}/${mm}/${yyyy}`;
195
- }
196
-
197
- function getReservationIdFromPayload(payload) {
198
- try {
199
- const m = payload && payload.data ? payload.data.meta : null;
200
- if (!m) return null;
201
- if (typeof m === 'object' && m.reservationId) return String(m.reservationId);
202
- if (typeof m === 'object' && m.reservationID) return String(m.reservationID);
203
- return null;
204
- } catch (e) {
205
- return null;
206
- }
207
- }
208
-
209
-
210
182
  function getCheckoutIntentIdFromPayload(payload) {
211
183
  try {
212
184
  const data = payload && payload.data ? payload.data : null;
package/lib/scheduler.js CHANGED
@@ -7,6 +7,7 @@ const discord = require('./discord');
7
7
  const realtime = require('./realtime');
8
8
  const nconf = require.main.require('nconf');
9
9
  const groups = require.main.require('./src/groups');
10
+ const utils = require('./utils');
10
11
 
11
12
  let timer = null;
12
13
 
@@ -51,25 +52,7 @@ async function addOnce(key, value) {
51
52
  }
52
53
  }
53
54
 
54
- function getSetting(settings, key, fallback) {
55
- const v = settings && Object.prototype.hasOwnProperty.call(settings, key) ? settings[key] : undefined;
56
- if (v == null || v === '') return fallback;
57
- if (typeof v === 'object' && v && typeof v.value !== 'undefined') return v.value;
58
- return v;
59
- }
60
-
61
- function formatFR(ts) {
62
- const d = new Date(ts);
63
- const dd = String(d.getDate()).padStart(2, '0');
64
- const mm = String(d.getMonth() + 1).padStart(2, '0');
65
- const yyyy = d.getFullYear();
66
- return `${dd}/${mm}/${yyyy}`;
67
- }
68
-
69
- function forumBaseUrl() {
70
- const base = String(nconf.get('url') || '').trim().replace(/\/$/, '');
71
- return base;
72
- }
55
+ const { getSetting, formatFR, forumBaseUrl, normalizeAllowedGroups, normalizeUids, arrayifyNames } = utils;
73
56
 
74
57
  // Resolve group identifiers from ACP (name or slug) and return UIDs.
75
58
  async function getMembersByGroupIdentifier(groupIdentifier) {
@@ -111,30 +94,7 @@ async function getMembersByGroupIdentifier(groupIdentifier) {
111
94
  return Array.isArray(members) ? members : [];
112
95
  }
113
96
 
114
- function normalizeUids(members) {
115
- if (!Array.isArray(members)) return [];
116
- const out = [];
117
- for (const m of members) {
118
- if (Number.isInteger(m)) { out.push(m); continue; }
119
- if (typeof m === 'string' && m.trim() && !Number.isNaN(parseInt(m, 10))) { out.push(parseInt(m, 10)); continue; }
120
- if (m && typeof m === 'object' && (Number.isInteger(m.uid) || (typeof m.uid === 'string' && m.uid.trim()))) {
121
- const u = Number.isInteger(m.uid) ? m.uid : parseInt(m.uid, 10);
122
- if (!Number.isNaN(u)) out.push(u);
123
- }
124
- }
125
- return Array.from(new Set(out)).filter((u) => Number.isInteger(u) && u > 0);
126
- }
127
-
128
- function normalizeAllowedGroups(raw) {
129
- if (!raw) return [];
130
- if (Array.isArray(raw)) {
131
- return raw.map((s) => String(s || '').trim()).filter(Boolean);
132
- }
133
- return String(raw)
134
- .split(',')
135
- .map((s) => String(s || '').trim())
136
- .filter(Boolean);
137
- }
97
+ // normalizeUids/normalizeAllowedGroups are provided by utils
138
98
 
139
99
  async function getValidatorUids(settings) {
140
100
  const out = new Set();
@@ -189,12 +149,14 @@ async function expirePending() {
189
149
  const validatorUids = validatorReminderMins > 0 ? await getValidatorUids(settings) : [];
190
150
 
191
151
  const ids = await dbLayer.listAllReservationIds(5000);
192
- if (!ids || !ids.length) {
193
- return;
194
- }
152
+ if (!ids || !ids.length) return;
195
153
 
196
- for (const rid of ids) {
197
- const resv = await dbLayer.getReservation(rid);
154
+ // Batch load to avoid N+1 queries on Mongo/Redis adapters.
155
+ const reservations = await dbLayer.getReservations(ids);
156
+
157
+ for (let i = 0; i < ids.length; i += 1) {
158
+ const rid = ids[i];
159
+ const resv = reservations[i];
198
160
  if (!resv || resv.status !== 'pending') {
199
161
  continue;
200
162
  }
@@ -213,7 +175,7 @@ async function expirePending() {
213
175
  await sendEmail('calendar-onekite_validator_reminder', vuid, 'Location matériel - Demande en attente', {
214
176
  rid,
215
177
  requesterUsername,
216
- itemNames: (Array.isArray(resv.itemNames) ? resv.itemNames : (resv.itemName ? [resv.itemName] : [])).filter(Boolean),
178
+ itemNames: arrayifyNames(resv),
217
179
  dateRange: `Du ${formatFR(resv.start)} au ${formatFR(resv.end)}`,
218
180
  adminUrl,
219
181
  kind: 'pending',
@@ -238,7 +200,7 @@ async function expirePending() {
238
200
  await sendEmail('calendar-onekite_expired', requesterUid, 'Location matériel - Demande expirée', {
239
201
  uid: requesterUid,
240
202
  username: requesterUsername,
241
- itemNames: (Array.isArray(resv.itemNames) ? resv.itemNames : (resv.itemName ? [resv.itemName] : [])).filter(Boolean),
203
+ itemNames: arrayifyNames(resv),
242
204
  dateRange: `Du ${formatFR(resv.start)} au ${formatFR(resv.end)}`,
243
205
  delayMinutes: holdMins,
244
206
  reason,
@@ -252,7 +214,7 @@ async function expirePending() {
252
214
  await sendEmail('calendar-onekite_validator_expired', vuid, 'Location matériel - Demande expirée', {
253
215
  rid,
254
216
  requesterUsername,
255
- itemNames: (Array.isArray(resv.itemNames) ? resv.itemNames : (resv.itemName ? [resv.itemName] : [])).filter(Boolean),
217
+ itemNames: arrayifyNames(resv),
256
218
  dateRange: `Du ${formatFR(resv.start)} au ${formatFR(resv.end)}`,
257
219
  adminUrl,
258
220
  reason,
@@ -308,21 +270,16 @@ async function processAwaitingPayment() {
308
270
  }
309
271
  }
310
272
 
311
- function formatFR(ts) {
312
- const d = new Date(ts);
313
- const dd = String(d.getDate()).padStart(2, '0');
314
- const mm = String(d.getMonth() + 1).padStart(2, '0');
315
- const yyyy = d.getFullYear();
316
- return `${dd}/${mm}/${yyyy}`;
317
- }
318
-
319
273
  const adminUrl = (() => {
320
274
  const base = forumBaseUrl();
321
275
  return base ? `${base}/admin/plugins/calendar-onekite` : '/admin/plugins/calendar-onekite';
322
276
  })();
323
277
 
324
- for (const rid of ids) {
325
- const r = await dbLayer.getReservation(rid);
278
+ const reservations = await dbLayer.getReservations(ids);
279
+
280
+ for (let i = 0; i < ids.length; i += 1) {
281
+ const rid = ids[i];
282
+ const r = reservations[i];
326
283
  if (!r || r.status !== 'awaiting_payment') continue;
327
284
 
328
285
  const approvedAt = parseInt(r.approvedAt || r.validatedAt || 0, 10) || 0;
@@ -349,8 +306,8 @@ async function processAwaitingPayment() {
349
306
  await sendEmail('calendar-onekite_reminder', toUid, 'Location matériel - Rappel', {
350
307
  uid: toUid,
351
308
  username: (u && u.username) ? u.username : '',
352
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
353
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
309
+ itemName: arrayifyNames(r).join(', '),
310
+ itemNames: arrayifyNames(r),
354
311
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
355
312
  paymentUrl: r.paymentUrl || '',
356
313
  delayMinutes: holdMins,
@@ -383,8 +340,8 @@ async function processAwaitingPayment() {
383
340
  await sendEmail('calendar-onekite_expired', toUid, 'Location matériel - Demande expirée', {
384
341
  uid: toUid,
385
342
  username: (u && u.username) ? u.username : '',
386
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
387
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
343
+ itemName: arrayifyNames(r).join(', '),
344
+ itemNames: arrayifyNames(r),
388
345
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
389
346
  delayMinutes: holdMins,
390
347
  reason: 'Paiement non reçu dans le temps imparti.',
@@ -398,7 +355,7 @@ async function processAwaitingPayment() {
398
355
  await sendEmail('calendar-onekite_validator_expired', vuid, 'Location matériel - Demande expirée', {
399
356
  rid: r.rid || rid,
400
357
  requesterUsername: (u && u.username) ? u.username : (r.username || ''),
401
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
358
+ itemNames: arrayifyNames(r),
402
359
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
403
360
  adminUrl,
404
361
  reason: 'Paiement non reçu dans le temps imparti.',
package/lib/utils.js ADDED
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ const nconf = require.main.require('nconf');
4
+
5
+ function getSetting(settings, key, fallback) {
6
+ const v = settings && Object.prototype.hasOwnProperty.call(settings, key) ? settings[key] : undefined;
7
+ if (v == null || v === '') return fallback;
8
+ if (typeof v === 'object' && v && typeof v.value !== 'undefined') return v.value;
9
+ return v;
10
+ }
11
+
12
+ function formatFR(tsOrIso) {
13
+ const ts = (typeof tsOrIso === 'string' && tsOrIso.includes('-')) ? Date.parse(tsOrIso) : Number(tsOrIso);
14
+ const d = new Date(Number.isFinite(ts) ? ts : tsOrIso);
15
+ const dd = String(d.getDate()).padStart(2, '0');
16
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
17
+ const yyyy = d.getFullYear();
18
+ return `${dd}/${mm}/${yyyy}`;
19
+ }
20
+
21
+ function formatFRShort(tsOrIso) {
22
+ const ts = (typeof tsOrIso === 'string' && tsOrIso.includes('-')) ? Date.parse(tsOrIso) : Number(tsOrIso);
23
+ const d = new Date(Number.isFinite(ts) ? ts : tsOrIso);
24
+ const dd = String(d.getDate()).padStart(2, '0');
25
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
26
+ return `${dd}/${mm}`;
27
+ }
28
+
29
+ function forumBaseUrl() {
30
+ return String(nconf.get('url') || '').trim().replace(/\/$/, '');
31
+ }
32
+
33
+ function normalizeAllowedGroups(raw) {
34
+ if (!raw) return [];
35
+ if (Array.isArray(raw)) {
36
+ return raw.map((s) => String(s || '').trim()).filter(Boolean);
37
+ }
38
+ return String(raw)
39
+ .split(',')
40
+ .map((s) => String(s || '').trim())
41
+ .filter(Boolean);
42
+ }
43
+
44
+ function normalizeUids(members) {
45
+ if (!Array.isArray(members)) return [];
46
+ const out = [];
47
+ for (const m of members) {
48
+ if (Number.isInteger(m)) { out.push(m); continue; }
49
+ if (typeof m === 'string' && m.trim() && !Number.isNaN(parseInt(m, 10))) { out.push(parseInt(m, 10)); continue; }
50
+ if (m && typeof m === 'object' && (Number.isInteger(m.uid) || (typeof m.uid === 'string' && m.uid.trim()))) {
51
+ const u = Number.isInteger(m.uid) ? m.uid : parseInt(m.uid, 10);
52
+ if (!Number.isNaN(u)) out.push(u);
53
+ }
54
+ }
55
+ return Array.from(new Set(out)).filter((u) => Number.isInteger(u) && u > 0);
56
+ }
57
+
58
+ function arrayifyNames(obj) {
59
+ return (Array.isArray(obj && obj.itemNames) ? obj.itemNames : (obj && obj.itemName ? [obj.itemName] : [])).filter(Boolean);
60
+ }
61
+
62
+ module.exports = {
63
+ getSetting,
64
+ formatFR,
65
+ formatFRShort,
66
+ forumBaseUrl,
67
+ normalizeAllowedGroups,
68
+ normalizeUids,
69
+ arrayifyNames,
70
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.30",
3
+ "version": "2.0.32",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -39,5 +39,5 @@
39
39
  "acpScripts": [
40
40
  "public/admin.js"
41
41
  ],
42
- "version": "2.0.30"
42
+ "version": "2.0.32"
43
43
  }