nodebb-plugin-calendar-onekite 11.1.30 → 11.1.31

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/api.js CHANGED
@@ -4,12 +4,6 @@ const meta = require.main.require('./src/meta');
4
4
  const dbi = require('./db');
5
5
  const hello = require('./helloasso');
6
6
 
7
- const BLOCKING_STATUSES = new Set(['pending', 'awaiting_payment', 'approved', 'paid']);
8
-
9
- function overlaps(aStart, aEnd, bStart, bEnd) {
10
- return Number(aStart) < Number(bEnd) && Number(bStart) < Number(aEnd);
11
- }
12
-
13
7
  function parseDateParam(s) {
14
8
  // FullCalendar sends ISO date/time. Accept 'YYYY-MM-DD' too.
15
9
  const d = new Date(s);
@@ -35,35 +29,6 @@ function daysBetweenInclusive(startYMD, endYMDExclusive) {
35
29
 
36
30
  let catalogCache = { at: 0, items: [], raw: null, ok: false, err: null };
37
31
 
38
- async function sweepExpiredPending() {
39
- const now = Date.now();
40
- const ids = await dbi.listExpiredIds(now, 5000);
41
- if (!ids.length) return 0;
42
- const rows = await dbi.getReservations(ids);
43
- let expiredCount = 0;
44
- for (const r of rows) {
45
- if (!r) continue;
46
- const status = r.status || 'pending';
47
- const expiresAt = Number(r.expiresAt || 0);
48
- if (status === 'pending' && expiresAt && expiresAt <= now) {
49
- r.status = 'expired';
50
- r.expiredAt = now;
51
- r.expiresAt = '';
52
- await dbi.saveReservation(r);
53
- await dbi.clearExpiryIndex(r.id);
54
- expiredCount++;
55
- } else {
56
- // Not actually expired, remove from expiry index to avoid repeated scans
57
- if (expiresAt && expiresAt > now) {
58
- // keep
59
- } else {
60
- await dbi.clearExpiryIndex(r.id);
61
- }
62
- }
63
- }
64
- return expiredCount;
65
- }
66
-
67
32
  async function getSettings() {
68
33
  const settings = await meta.settings.get('calendar-onekite');
69
34
  return settings || {};
@@ -96,7 +61,6 @@ async function getCatalog(req, res) {
96
61
  }
97
62
 
98
63
  async function getEvents(req, res) {
99
- await sweepExpiredPending();
100
64
  const start = parseDateParam(req.query.start);
101
65
  const end = parseDateParam(req.query.end);
102
66
  const startTs = start ? start.getTime() : Date.now() - 365*86400000;
@@ -105,17 +69,11 @@ async function getEvents(req, res) {
105
69
  const ids = await dbi.listReservationIdsByStartRange(startTs, endTs, 5000);
106
70
  const rows = await dbi.getReservations(ids);
107
71
 
108
- const events = rows
109
- .filter(r => {
110
- const status = r.status || 'pending';
111
- // Hide expired requests from calendar
112
- return status !== 'expired';
113
- })
114
- .map(r => {
72
+ const events = rows.map(r => {
115
73
  const itemNames = (r.items || []).map(it => it.name).join(', ');
116
74
  const title = itemNames || 'Réservation';
117
75
  const status = r.status || 'pending';
118
- const icon = status === 'approved' ? '✅' : status === 'awaiting_payment' ? '💳' : status === 'refused' ? '⛔' : '⏳';
76
+ const icon = status === 'approved' ? '✅' : status === 'refused' ? '⛔' : '⏳';
119
77
  return {
120
78
  id: r.id,
121
79
  title: `${icon} ${title}`,
@@ -139,9 +97,6 @@ async function createReservation(req, res) {
139
97
  const uid = req.uid;
140
98
  if (!uid) return res.status(403).json({ status: { code: 'forbidden', message: 'Not logged in' } });
141
99
 
142
- // Expire old pending holds before checking availability
143
- await sweepExpiredPending();
144
-
145
100
  const body = req.body || {};
146
101
  const startYMD = body.start;
147
102
  const endYMD = body.end;
@@ -170,35 +125,6 @@ async function createReservation(req, res) {
170
125
  const startTs = new Date(startYMD + 'T00:00:00Z').getTime();
171
126
  const endTs = new Date(endYMD + 'T00:00:00Z').getTime();
172
127
 
173
- // Prevent double-booking (reserved OR awaiting payment OR approved) for any selected item
174
- // Query candidates by start range; then filter by overlap and item intersection
175
- const candidateIds = await dbi.listReservationIdsByStartRange(startTs - 86400000, endTs + 86400000, 10000);
176
- const candidates = await dbi.getReservations(candidateIds);
177
- const wanted = new Set(itemIds);
178
- const conflicts = [];
179
- for (const r of candidates) {
180
- if (!r) continue;
181
- const status = r.status || 'pending';
182
- if (!BLOCKING_STATUSES.has(status)) continue;
183
- if (status === 'pending' && r.expiresAt && Number(r.expiresAt) <= Date.now()) continue;
184
- if (!overlaps(startTs, endTs, Number(r.startTs), Number(r.endTs))) continue;
185
- const rItemIds = (r.items || []).map(it => String(it.id));
186
- const hit = rItemIds.filter(x => wanted.has(x));
187
- if (hit.length) {
188
- conflicts.push({ id: r.id, status, itemIds: hit, startYMD: r.startYMD, endYMD: r.endYMD });
189
- }
190
- }
191
- if (conflicts.length) {
192
- return res.status(409).json({
193
- status: { code: 'conflict', message: 'Matériel déjà réservé ou en attente de paiement.' },
194
- conflicts,
195
- });
196
- }
197
-
198
- const settings = await getSettings();
199
- const holdMinutes = Math.max(1, parseInt(settings.holdMinutes, 10) || 5);
200
- const expiresAt = Date.now() + holdMinutes * 60 * 1000;
201
-
202
128
  const resv = {
203
129
  id,
204
130
  uid: String(uid),
@@ -211,7 +137,6 @@ async function createReservation(req, res) {
211
137
  totalCents,
212
138
  status: 'pending',
213
139
  createdAt: Date.now(),
214
- expiresAt,
215
140
  };
216
141
  await dbi.saveReservation(resv);
217
142
 
@@ -222,7 +147,6 @@ module.exports = {
222
147
  getEvents,
223
148
  getCatalog,
224
149
  createReservation,
225
- _sweepExpiredPending: sweepExpiredPending,
226
150
  _getCatalogItems: getCatalogItems,
227
151
  _catalogCache: () => catalogCache,
228
152
  };
package/lib/db.js CHANGED
@@ -4,7 +4,6 @@ const db = require.main.require('./src/database');
4
4
 
5
5
  const KEYS = {
6
6
  Z_BY_START: 'calendar-onekite:reservations:byStart',
7
- Z_BY_EXPIRES: 'calendar-onekite:reservations:byExpires',
8
7
  HASH: (id) => `calendar-onekite:reservation:${id}`,
9
8
  NEXT_ID: 'calendar-onekite:reservation:nextId',
10
9
  };
@@ -17,13 +16,6 @@ async function nextId() {
17
16
  async function saveReservation(resv) {
18
17
  await db.setObject(KEYS.HASH(resv.id), resv);
19
18
  await db.sortedSetAdd(KEYS.Z_BY_START, resv.startTs, resv.id);
20
-
21
- // Track expiration for pending reservations
22
- if (resv.expiresAt) {
23
- await db.sortedSetAdd(KEYS.Z_BY_EXPIRES, Number(resv.expiresAt), resv.id);
24
- } else {
25
- await db.sortedSetRemove(KEYS.Z_BY_EXPIRES, resv.id);
26
- }
27
19
  }
28
20
 
29
21
  async function getReservation(id) {
@@ -46,17 +38,6 @@ async function listReservationIdsByStartRange(startTs, endTs, limit = 2000) {
46
38
  async function deleteReservation(id) {
47
39
  await db.delete(KEYS.HASH(id));
48
40
  await db.sortedSetRemove(KEYS.Z_BY_START, id);
49
- await db.sortedSetRemove(KEYS.Z_BY_EXPIRES, id);
50
- }
51
-
52
- async function listExpiredIds(nowTs, limit = 5000) {
53
- // NodeBB db API: getSortedSetRangeByScore(set, start, stop, min, max)
54
- const ids = await db.getSortedSetRangeByScore(KEYS.Z_BY_EXPIRES, 0, limit - 1, 0, nowTs);
55
- return ids || [];
56
- }
57
-
58
- async function clearExpiryIndex(id) {
59
- await db.sortedSetRemove(KEYS.Z_BY_EXPIRES, id);
60
41
  }
61
42
 
62
43
  async function listAllIds(limit=100000) {
@@ -71,7 +52,5 @@ module.exports = {
71
52
  getReservations,
72
53
  listReservationIdsByStartRange,
73
54
  deleteReservation,
74
- listExpiredIds,
75
- clearExpiryIndex,
76
55
  listAllIds,
77
56
  };
package/library.js CHANGED
@@ -7,7 +7,6 @@ const api = require('./lib/api');
7
7
  const admin = require('./lib/admin');
8
8
 
9
9
  const Plugin = {};
10
- let sweeper = null;
11
10
 
12
11
  function isFn(fn) { return typeof fn === 'function'; }
13
12
  function safeMws(arr) { return (Array.isArray(arr) ? arr : []).filter(isFn); }
@@ -76,15 +75,6 @@ Plugin.init = async function (params) {
76
75
  router.get('/api/admin/plugins/calendar-onekite/pending', ...adminMws, admin.getPending);
77
76
  router.post('/api/admin/plugins/calendar-onekite/purge', ...adminMws, admin.purgeByYear);
78
77
  router.get('/api/admin/plugins/calendar-onekite/debug', ...adminMws, admin.debugHelloAsso);
79
-
80
- // Background expiry sweeper: ensures pending holds are released even if nobody loads the calendar
81
- if (!sweeper) {
82
- sweeper = setInterval(() => {
83
- api._sweepExpiredPending().catch(() => {});
84
- }, 60 * 1000);
85
- // do not keep the process alive just for the timer
86
- if (typeof sweeper.unref === 'function') sweeper.unref();
87
- }
88
78
  };
89
79
 
90
80
  Plugin.addAdminNavigation = async function (header) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "11.1.30",
3
+ "version": "11.1.31",
4
4
  "description": "Calendar reservations with HelloAsso integration for OneKite",
5
5
  "main": "library.js",
6
6
  "scripts": {},
package/lib/email.js DELETED
@@ -1,13 +0,0 @@
1
- 'use strict';
2
-
3
- const emailer = require.main.require('./src/emailer');
4
-
5
- async function sendToUid(template, uid, data) {
6
- try {
7
- await emailer.send(template, uid, data);
8
- } catch (e) {
9
- // don't crash server
10
- }
11
- }
12
-
13
- module.exports = { sendToUid };
package/lib/logic.js DELETED
@@ -1,120 +0,0 @@
1
- 'use strict';
2
-
3
- const db = require('./db');
4
- const { getCatalogItems } = require('./helloasso');
5
- const settings = require('./settings');
6
-
7
- const BLOCKING_STATUSES = new Set(['pending', 'awaiting_payment', 'approved', 'paid']);
8
-
9
- function dateToTs(dateStr) {
10
- // interpret as UTC midnight
11
- return Date.parse(dateStr + 'T00:00:00.000Z');
12
- }
13
- function addDays(dateStr, days) {
14
- const ts = dateToTs(dateStr) + days * 86400000;
15
- const d = new Date(ts);
16
- return d.toISOString().slice(0,10);
17
- }
18
- function overlap(aStart, aEnd, bStart, bEnd) {
19
- return aStart < bEnd && bStart < aEnd;
20
- }
21
-
22
- async function sweepExpired(nowTs = Date.now()) {
23
- const expiredIds = await db.listExpiredIds(nowTs);
24
- for (const rid of expiredIds) {
25
- const r = await db.getReservation(rid);
26
- if (!r) continue;
27
- if (r.status === 'pending' && r.expiresAt && r.expiresAt <= nowTs) {
28
- r.status = 'expired';
29
- r.expiresAt = 0;
30
- await db.saveReservation(r);
31
- } else {
32
- // remove from expires index
33
- r.expiresAt = 0;
34
- await db.saveReservation(r);
35
- }
36
- }
37
- }
38
-
39
- async function listEvents(startDate, endDate) {
40
- await sweepExpired();
41
- const startTs = dateToTs(startDate);
42
- const endTs = dateToTs(endDate);
43
- const ids = await db.listReservationIdsByStartRange(startTs - 86400000*400, endTs); // wide window
44
- const events = [];
45
- for (const id of ids) {
46
- const r = await db.getReservation(id);
47
- if (!r) continue;
48
- if (r.status === 'expired') continue;
49
- if (overlap(dateToTs(r.startDate), dateToTs(r.endDate), startTs, endTs)) {
50
- events.push(r);
51
- }
52
- }
53
- return events;
54
- }
55
-
56
- async function computeTotal(itemIds, days) {
57
- const { items } = await getCatalogItems();
58
- const map = new Map(items.map(i => [i.id, i]));
59
- let sum = 0;
60
- for (const id of itemIds) {
61
- const it = map.get(String(id));
62
- if (it) sum += it.priceCents;
63
- }
64
- return sum * days;
65
- }
66
-
67
- async function checkConflicts(itemIds, startDate, endDate) {
68
- const events = await listEvents(startDate, endDate);
69
- const conflicts = [];
70
- const wantSet = new Set(itemIds.map(String));
71
- for (const r of events) {
72
- if (!BLOCKING_STATUSES.has(r.status)) continue;
73
- const has = (r.itemIds || []).some(x => wantSet.has(String(x)));
74
- if (has && overlap(dateToTs(r.startDate), dateToTs(r.endDate), dateToTs(startDate), dateToTs(endDate))) {
75
- conflicts.push({ rid: r.rid, status: r.status, startDate: r.startDate, endDate: r.endDate, itemIds: r.itemIds });
76
- }
77
- }
78
- return conflicts;
79
- }
80
-
81
- async function createReservation(uid, itemIds, startDate, endDate, note='') {
82
- await sweepExpired();
83
- const s = await settings.get();
84
- const holdMinutes = parseInt(s.holdMinutes, 10) || 5;
85
-
86
- // normalize dates and days (end exclusive)
87
- const startTs = dateToTs(startDate);
88
- const endTs = dateToTs(endDate);
89
- const days = Math.max(1, Math.round((endTs - startTs) / 86400000));
90
- const conflicts = await checkConflicts(itemIds, startDate, endDate);
91
- if (conflicts.length) {
92
- const err = new Error('conflict');
93
- err.code = 'conflict';
94
- err.conflicts = conflicts;
95
- throw err;
96
- }
97
- const totalCents = await computeTotal(itemIds, days);
98
-
99
- const rid = await db.nextId();
100
- const now = Date.now();
101
- const expiresAt = now + holdMinutes * 60 * 1000;
102
-
103
- const title = `${itemIds.length} matériel(s)`;
104
- const resv = { rid, uid, itemIds: itemIds.map(String), startDate, endDate, startTs, endTs, status: 'pending', createdAt: now, expiresAt, days, totalCents, title, note };
105
- await db.saveReservation(resv);
106
- return resv;
107
- }
108
-
109
- async function setStatus(rid, status) {
110
- const r = await db.getReservation(rid);
111
- if (!r) return null;
112
- r.status = status;
113
- if (status !== 'pending') {
114
- r.expiresAt = 0;
115
- }
116
- await db.saveReservation(r);
117
- return r;
118
- }
119
-
120
- module.exports = { listEvents, createReservation, checkConflicts, sweepExpired, setStatus, dateToTs };
package/lib/mailer.js DELETED
@@ -1,78 +0,0 @@
1
- 'use strict';
2
-
3
- const Emailer = (() => {
4
- try { return require.main.require('./src/emailer'); } catch (e) { return null; }
5
- })();
6
- const User = (() => {
7
- try { return require.main.require('./src/user'); } catch (e) { return null; }
8
- })();
9
- const Groups = (() => {
10
- try { return require.main.require('./src/groups'); } catch (e) { return null; }
11
- })();
12
- const meta = (() => {
13
- try { return require.main.require('./src/meta'); } catch (e) { return null; }
14
- })();
15
-
16
- const PLUGIN_ID = 'calendar-onekite';
17
-
18
- async function getGroupMemberUids(groupName) {
19
- if (!Groups) return [];
20
- if (typeof Groups.getMembers === 'function') {
21
- return await Groups.getMembers(groupName, 0, -1);
22
- }
23
- if (typeof Groups.getMemberUsers === 'function') {
24
- const users = await Groups.getMemberUsers(groupName, 0, 1000);
25
- return users.map(u => u && u.uid).filter(Boolean);
26
- }
27
- return [];
28
- }
29
-
30
- async function getEmailsFromUids(uids) {
31
- if (!User) return [];
32
- const data = await User.getUsersFields(uids, ['email']);
33
- return (data || []).map(u => u && u.email).filter(Boolean);
34
- }
35
-
36
- async function sendEmailToEmails(template, emails, data) {
37
- if (!Emailer || !emails.length) return { ok: false, skipped: true };
38
- // Try different APIs depending on NodeBB version
39
- try {
40
- if (typeof Emailer.sendToEmail === 'function') {
41
- for (const email of emails) {
42
- // eslint-disable-next-line no-await-in-loop
43
- await Emailer.sendToEmail(template, email, data);
44
- }
45
- return { ok: true };
46
- }
47
- if (typeof Emailer.send === 'function') {
48
- for (const email of emails) {
49
- // eslint-disable-next-line no-await-in-loop
50
- await Emailer.send(template, email, data);
51
- }
52
- return { ok: true };
53
- }
54
- } catch (e) {
55
- return { ok: false, error: e.message };
56
- }
57
- return { ok: false, error: 'Emailer API not found' };
58
- }
59
-
60
- async function notifyGroups(template, groupsCsv, data) {
61
- const groups = String(groupsCsv || '').split(',').map(s => s.trim()).filter(Boolean);
62
- const allUids = [];
63
- for (const g of groups) {
64
- // eslint-disable-next-line no-await-in-loop
65
- const uids = await getGroupMemberUids(g);
66
- allUids.push(...uids);
67
- }
68
- const uniq = Array.from(new Set(allUids.map(String))).map(Number).filter(n => n > 0);
69
- const emails = await getEmailsFromUids(uniq);
70
- return await sendEmailToEmails(template, emails, data);
71
- }
72
-
73
- async function notifyUser(template, uid, data) {
74
- const emails = await getEmailsFromUids([uid]);
75
- return await sendEmailToEmails(template, emails, data);
76
- }
77
-
78
- module.exports = { notifyGroups, notifyUser };
@@ -1,69 +0,0 @@
1
- 'use strict';
2
-
3
- const Groups = (() => {
4
- try { return require.main.require('./src/groups'); } catch (e) { return null; }
5
- })();
6
- const meta = (() => {
7
- try { return require.main.require('./src/meta'); } catch (e) { return null; }
8
- })();
9
-
10
- const PLUGIN_ID = 'calendar-onekite';
11
-
12
- function safeMiddlewares(mws) {
13
- return (Array.isArray(mws) ? mws : []).filter(fn => typeof fn === 'function');
14
- }
15
-
16
- function ensureLoggedIn(req, res, next) {
17
- if (req && req.uid && Number(req.uid) > 0) return next();
18
- res.status(401).json({ ok: false, error: 'not-logged-in' });
19
- }
20
-
21
- async function isInAnyGroup(uid, groupsCsv) {
22
- if (!Groups || !uid || uid <= 0) return false;
23
- const groups = String(groupsCsv || '').split(',').map(s => s.trim()).filter(Boolean);
24
- if (!groups.length) return false;
25
- for (const g of groups) {
26
- // Groups.isMember(uid, groupName)
27
- if (typeof Groups.isMember === 'function') {
28
- // eslint-disable-next-line no-await-in-loop
29
- const ok = await Groups.isMember(uid, g);
30
- if (ok) return true;
31
- }
32
- }
33
- return false;
34
- }
35
-
36
- async function ensureAdminGroup(req, res, next) {
37
- try {
38
- const uid = Number(req.uid) || 0;
39
- if (uid <= 0) return res.status(401).json({ ok: false, error: 'not-logged-in' });
40
- // Default: administrators group
41
- const ok = await isInAnyGroup(uid, 'administrators');
42
- if (ok) return next();
43
- return res.status(403).json({ ok: false, error: 'not-authorized' });
44
- } catch (e) {
45
- return res.status(500).json({ ok: false, error: 'internal-error' });
46
- }
47
- }
48
-
49
- async function ensureValidatorGroup(req, res, next) {
50
- try {
51
- const uid = Number(req.uid) || 0;
52
- if (uid <= 0) return res.status(401).json({ ok: false, error: 'not-logged-in' });
53
- const settings = meta ? await meta.settings.get(PLUGIN_ID) : {};
54
- const groupsCsv = settings && settings.validatorGroups ? settings.validatorGroups : 'administrators';
55
- const ok = await isInAnyGroup(uid, groupsCsv) || await isInAnyGroup(uid, 'administrators');
56
- if (ok) return next();
57
- return res.status(403).json({ ok: false, error: 'not-validator' });
58
- } catch (e) {
59
- return res.status(500).json({ ok: false, error: 'internal-error' });
60
- }
61
- }
62
-
63
- module.exports = {
64
- safeMiddlewares,
65
- ensureLoggedIn,
66
- ensureAdminGroup,
67
- ensureValidatorGroup,
68
- isInAnyGroup,
69
- };
package/lib/perms.js DELETED
@@ -1,44 +0,0 @@
1
- 'use strict';
2
-
3
- const Groups = require.main.require('./src/groups');
4
- const settings = require('./settings');
5
-
6
- async function isInAnyGroup(uid, groups) {
7
- if (!uid || uid <= 0) return false;
8
- for (const g of groups) {
9
- try {
10
- const ok = await Groups.isMember(uid, g);
11
- if (ok) return true;
12
- } catch {}
13
- }
14
- return false;
15
- }
16
-
17
- async function canCreate(uid) {
18
- const s = await settings.get();
19
- const groups = settings.splitCsv(s.creatorGroups);
20
- if (!groups.length) return !!uid;
21
- return await isInAnyGroup(uid, groups);
22
- }
23
-
24
- async function canValidate(uid) {
25
- const s = await settings.get();
26
- const groups = settings.splitCsv(s.validatorGroups);
27
- if (!groups.length) return false;
28
- return await isInAnyGroup(uid, groups);
29
- }
30
-
31
- async function listNotifyUids() {
32
- const s = await settings.get();
33
- const groups = settings.splitCsv(s.notifyGroups);
34
- const uids = new Set();
35
- for (const g of groups) {
36
- try {
37
- const members = await Groups.getMembers(g, 0, -1);
38
- (members || []).forEach(uid => uids.add(parseInt(uid, 10)));
39
- } catch {}
40
- }
41
- return [...uids].filter(n => Number.isFinite(n) && n > 0);
42
- }
43
-
44
- module.exports = { canCreate, canValidate, listNotifyUids };
package/lib/settings.js DELETED
@@ -1,18 +0,0 @@
1
- 'use strict';
2
- const meta = require.main.require('./src/meta');
3
- const PLUGIN_ID = 'calendar-onekite';
4
-
5
- async function get() {
6
- const s = await meta.settings.get(PLUGIN_ID);
7
- return s || {};
8
- }
9
-
10
- function normalizeCsv(v) {
11
- return String(v || '').split(',').map(s => s.trim()).filter(Boolean).join(',');
12
- }
13
- function splitCsv(v) {
14
- const n = normalizeCsv(v);
15
- return n ? n.split(',') : [];
16
- }
17
-
18
- module.exports = { PLUGIN_ID, get, normalizeCsv, splitCsv };
package/lib/store.js DELETED
@@ -1,209 +0,0 @@
1
- 'use strict';
2
-
3
- const db = require.main.require('./src/database');
4
-
5
- const KEY_NEXT = 'onekite:cal:nextRid';
6
- const KEY_HASH = (rid) => `onekite:cal:r:${rid}`;
7
- const KEY_Z_START = 'onekite:cal:z:start';
8
- const KEY_Z_STATUS = (status) => `onekite:cal:z:status:${status}`;
9
- const KEY_Z_EXPIRES = 'onekite:cal:z:expires';
10
-
11
- function toDayString(ms) {
12
- return new Date(ms).toISOString().slice(0, 10);
13
- }
14
-
15
- async function nextRid() {
16
- const v = await db.incrObjectField('global', KEY_NEXT);
17
- return String(v);
18
- }
19
-
20
- async function setReservation(rid, data) {
21
- await db.setObject(KEY_HASH(rid), data);
22
- }
23
-
24
- async function getReservation(rid) {
25
- const obj = await db.getObject(KEY_HASH(rid));
26
- if (!obj || !obj.rid) return null;
27
- // parse
28
- obj.startMs = parseInt(obj.startMs, 10);
29
- obj.endMs = parseInt(obj.endMs, 10);
30
- obj.uid = parseInt(obj.uid, 10);
31
- obj.expiresAt = parseInt(obj.expiresAt || '0', 10);
32
- obj.days = parseInt(obj.days || '0', 10);
33
- obj.totalCents = parseInt(obj.totalCents || '0', 10);
34
- try { obj.itemIds = JSON.parse(obj.itemIds || '[]'); } catch (e) { obj.itemIds = []; }
35
- return obj;
36
- }
37
-
38
- async function indexReservation(rid, r) {
39
- await db.sortedSetAdd(KEY_Z_START, r.startMs, rid);
40
- await db.sortedSetAdd(KEY_Z_STATUS(r.status), r.startMs, rid);
41
- if (r.expiresAt && r.expiresAt > 0) {
42
- await db.sortedSetAdd(KEY_Z_EXPIRES, r.expiresAt, rid);
43
- }
44
- }
45
-
46
- async function deindexStatus(rid, oldStatus) {
47
- await db.sortedSetRemove(KEY_Z_STATUS(oldStatus), rid);
48
- }
49
-
50
- async function updateStatusIndex(rid, oldStatus, newStatus, startMs) {
51
- if (oldStatus) await deindexStatus(rid, oldStatus);
52
- await db.sortedSetAdd(KEY_Z_STATUS(newStatus), startMs, rid);
53
- }
54
-
55
- async function removeExpireIndex(rid) {
56
- await db.sortedSetRemove(KEY_Z_EXPIRES, rid);
57
- }
58
-
59
- async function createReservation(input) {
60
- const rid = await nextRid();
61
- const r = {
62
- rid,
63
- uid: input.uid,
64
- startMs: input.startMs,
65
- endMs: input.endMs,
66
- status: input.status,
67
- expiresAt: input.expiresAt || 0,
68
- days: input.days || 0,
69
- totalCents: input.totalCents || 0,
70
- itemIds: JSON.stringify(input.itemIds || []),
71
- };
72
- await setReservation(rid, r);
73
- await indexReservation(rid, r);
74
- return rid;
75
- }
76
-
77
- async function updateReservation(rid, patch) {
78
- const existing = await getReservation(rid);
79
- if (!existing) return null;
80
- const updated = Object.assign({}, existing, patch);
81
- // indexes
82
- if (patch.status && patch.status !== existing.status) {
83
- await updateStatusIndex(rid, existing.status, patch.status, existing.startMs);
84
- }
85
- if (typeof patch.expiresAt !== 'undefined') {
86
- await removeExpireIndex(rid);
87
- if (patch.expiresAt && patch.expiresAt > 0) {
88
- await db.sortedSetAdd(KEY_Z_EXPIRES, patch.expiresAt, rid);
89
- }
90
- }
91
- // persist
92
- const obj = Object.assign({}, updated, {
93
- itemIds: JSON.stringify(updated.itemIds || existing.itemIds || []),
94
- });
95
- await setReservation(rid, obj);
96
- return updated;
97
- }
98
-
99
- async function listByStatus(status, start, stop) {
100
- const ids = await db.getSortedSetRange(KEY_Z_STATUS(status), start, stop);
101
- const out = [];
102
- for (const rid of ids) {
103
- // eslint-disable-next-line no-await-in-loop
104
- const r = await getReservation(rid);
105
- if (r) out.push(r);
106
- }
107
- return out;
108
- }
109
-
110
- function overlaps(aStart, aEnd, bStart, bEnd) {
111
- return aStart < bEnd && bStart < aEnd;
112
- }
113
-
114
- async function checkConflicts(itemIds, startMs, endMs, blockingStatuses) {
115
- // candidates: reservations with start <= endMs
116
- const ids = await db.getSortedSetRangeByScore(KEY_Z_START, 0, 2000, 0, endMs);
117
- const conflicts = [];
118
- for (const rid of ids) {
119
- // eslint-disable-next-line no-await-in-loop
120
- const r = await getReservation(rid);
121
- if (!r) continue;
122
- if (!blockingStatuses.has(r.status)) continue;
123
- if (!overlaps(startMs, endMs, r.startMs, r.endMs)) continue;
124
- const set = new Set(r.itemIds || []);
125
- const hit = itemIds.filter(id => set.has(String(id)));
126
- if (hit.length) conflicts.push({ rid, status: r.status, itemIds: hit, start: toDayString(r.startMs), end: toDayString(r.endMs) });
127
- }
128
- return conflicts;
129
- }
130
-
131
- async function getEventsInRange(startMs, endMs) {
132
- const ids = await db.getSortedSetRangeByScore(KEY_Z_START, 0, 2000, startMs - 365*24*3600*1000, endMs);
133
- const events = [];
134
- for (const rid of ids) {
135
- // eslint-disable-next-line no-await-in-loop
136
- const r = await getReservation(rid);
137
- if (!r) continue;
138
- if (!overlaps(startMs, endMs, r.startMs, r.endMs)) continue;
139
- const icon = r.status === 'pending' ? '⏳' : (r.status === 'refused' ? '⛔' : (r.status === 'awaiting_payment' ? '💳' : '✅'));
140
- const title = `${icon} #${r.rid}`;
141
- events.push({
142
- id: r.rid,
143
- title,
144
- start: toDayString(r.startMs),
145
- end: toDayString(r.endMs),
146
- allDay: true,
147
- extendedProps: {
148
- status: r.status,
149
- itemIds: r.itemIds || [],
150
- uid: r.uid,
151
- days: r.days,
152
- totalCents: r.totalCents,
153
- },
154
- });
155
- }
156
- return events;
157
- }
158
-
159
- async function sweepExpired() {
160
- const now = Date.now();
161
- const ids = await db.getSortedSetRangeByScore(KEY_Z_EXPIRES, 0, 500, 0, now);
162
- for (const rid of ids) {
163
- // eslint-disable-next-line no-await-in-loop
164
- const r = await getReservation(rid);
165
- if (!r) {
166
- // eslint-disable-next-line no-await-in-loop
167
- await removeExpireIndex(rid);
168
- continue;
169
- }
170
- if (r.status === 'pending' && r.expiresAt && r.expiresAt <= now) {
171
- // eslint-disable-next-line no-await-in-loop
172
- await updateReservation(rid, { status: 'expired', expiresAt: 0 });
173
- } else {
174
- // eslint-disable-next-line no-await-in-loop
175
- await removeExpireIndex(rid);
176
- }
177
- }
178
- }
179
-
180
- async function purgeRange(startMs, endMs) {
181
- const ids = await db.getSortedSetRangeByScore(KEY_Z_START, 0, 100000, startMs, endMs);
182
- let count = 0;
183
- for (const rid of ids) {
184
- // eslint-disable-next-line no-await-in-loop
185
- const r = await getReservation(rid);
186
- if (!r) continue;
187
- // eslint-disable-next-line no-await-in-loop
188
- await db.delete(KEY_HASH(rid));
189
- // eslint-disable-next-line no-await-in-loop
190
- await db.sortedSetRemove(KEY_Z_START, rid);
191
- // eslint-disable-next-line no-await-in-loop
192
- await db.sortedSetRemove(KEY_Z_STATUS(r.status), rid);
193
- // eslint-disable-next-line no-await-in-loop
194
- await db.sortedSetRemove(KEY_Z_EXPIRES, rid);
195
- count++;
196
- }
197
- return count;
198
- }
199
-
200
- module.exports = {
201
- createReservation,
202
- getReservation,
203
- updateReservation,
204
- listByStatus,
205
- getEventsInRange,
206
- sweepExpired,
207
- purgeRange,
208
- checkConflicts,
209
- };
package/lib/sweeper.js DELETED
@@ -1,16 +0,0 @@
1
- 'use strict';
2
- const logic = require('./logic');
3
-
4
- let started = false;
5
- let timer = null;
6
-
7
- function start() {
8
- if (started) return;
9
- started = true;
10
- timer = setInterval(() => {
11
- logic.sweepExpired().catch(() => {});
12
- }, 60 * 1000);
13
- if (timer.unref) timer.unref();
14
- }
15
-
16
- module.exports = { start };
@@ -1 +0,0 @@
1
- Votre réservation #{rid} du {startDate} au {endDate} a été validée.
@@ -1 +0,0 @@
1
- Nouvelle demande de réservation #{rid} du {startDate} au {endDate} par {username}
@@ -1 +0,0 @@
1
- Votre réservation #{rid} du {startDate} au {endDate} a été refusée.