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 +245 -103
- package/lib/api.js +285 -174
- package/lib/controllers.js +3 -2
- package/lib/db.js +24 -33
- package/lib/helloasso.js +179 -68
- package/lib/scheduler.js +34 -14
- package/library.js +16 -1
- package/package.json +5 -3
- package/plugin.json +11 -14
- package/public/admin.js +258 -95
- package/public/client.js +210 -57
- package/templates/admin/plugins/calendar-onekite.tpl +93 -48
- package/templates/calendar-onekite.tpl +6 -6
- package/templates/emails/calendar-onekite_approved.tpl +6 -0
- package/lib/utils.js +0 -25
- package/templates/emails/calendar-onekite-approved.tpl +0 -1
- package/templates/emails/calendar-onekite-pending.tpl +0 -1
- package/templates/emails/calendar-onekite-refused.tpl +0 -1
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
|
|
6
|
-
const
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
validatorGroups: normalizeCsv(payload.validatorGroups ?? current.validatorGroups),
|
|
32
|
-
notifyGroups: normalizeCsv(payload.notifyGroups ?? current.notifyGroups),
|
|
35
|
+
const admin = {};
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
admin.renderAdmin = async function (req, res) {
|
|
38
|
+
res.render('admin/plugins/calendar-onekite', {
|
|
39
|
+
title: 'Calendar OneKite',
|
|
40
|
+
});
|
|
41
|
+
};
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
49
|
-
|
|
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
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
122
|
+
if (paymentUrl) {
|
|
123
|
+
r.paymentUrl = paymentUrl;
|
|
124
|
+
}
|
|
74
125
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
81
|
-
const year = String(req.body.year
|
|
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({
|
|
173
|
+
return res.status(400).json({ error: 'invalid-year' });
|
|
84
174
|
}
|
|
85
175
|
const y = parseInt(year, 10);
|
|
86
|
-
const
|
|
87
|
-
const
|
|
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
|
|
179
|
+
const ids = await dbLayer.listReservationIdsByStartRange(startTs, endTs, 100000);
|
|
180
|
+
let count = 0;
|
|
90
181
|
for (const rid of ids) {
|
|
91
|
-
|
|
92
|
-
|
|
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,
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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;
|