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 +5 -0
- package/lib/admin.js +1 -14
- package/lib/api.js +1 -8
- package/lib/helloassoWebhook.js +1 -29
- package/lib/scheduler.js +23 -66
- package/lib/utils.js +70 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
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
|
|
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) : [];
|
package/lib/helloassoWebhook.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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
|
-
|
|
325
|
-
|
|
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: (
|
|
353
|
-
itemNames: (
|
|
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: (
|
|
387
|
-
itemNames: (
|
|
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: (
|
|
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
package/plugin.json
CHANGED