nodebb-plugin-calendar-onekite 11.1.32 → 11.1.33
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 +108 -222
- package/lib/api.js +184 -132
- package/lib/controllers.js +2 -3
- package/lib/db.js +33 -24
- package/lib/helloasso.js +68 -179
- package/lib/scheduler.js +14 -34
- package/lib/utils.js +25 -0
- package/library.js +8 -0
- package/package.json +3 -5
- package/plugin.json +14 -11
- package/public/admin.js +95 -206
- package/public/client.js +41 -0
- package/templates/admin/plugins/calendar-onekite.tpl +55 -78
- package/templates/calendar-onekite.tpl +6 -6
- package/templates/emails/calendar-onekite-approved.tpl +1 -0
- package/templates/emails/calendar-onekite-pending.tpl +1 -0
- package/templates/emails/calendar-onekite-refused.tpl +1 -0
package/lib/admin.js
CHANGED
|
@@ -1,249 +1,135 @@
|
|
|
1
|
+
|
|
1
2
|
'use strict';
|
|
2
3
|
|
|
3
4
|
const meta = require.main.require('./src/meta');
|
|
4
|
-
const
|
|
5
|
-
const emailer = require.main.require('./src/emailer');
|
|
6
|
-
|
|
7
|
-
const dbLayer = require('./db');
|
|
5
|
+
const User = require.main.require('./src/user');
|
|
8
6
|
const helloasso = require('./helloasso');
|
|
7
|
+
const reservationDb = require('./db');
|
|
8
|
+
const { csvToList, toDateOnlyISO } = require('./utils');
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
res.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
async function getSettings(req, res) {
|
|
11
|
+
const s = await meta.settings.get('calendar-onekite');
|
|
12
|
+
if (s && s.helloassoClientSecret) {
|
|
13
|
+
s.helloassoClientSecret = '***';
|
|
14
|
+
}
|
|
15
|
+
res.json({ ok:true, settings: s || {} });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function saveSettings(req, res) {
|
|
19
|
+
const payload = req.body || {};
|
|
20
|
+
const normalizeCsv = (v) => String(v || '').split(',').map(s => s.trim()).filter(Boolean).join(',');
|
|
21
|
+
|
|
22
|
+
const current = await meta.settings.get('calendar-onekite') || {};
|
|
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 ?? '',
|
|
29
|
+
|
|
30
|
+
creatorGroups: normalizeCsv(payload.creatorGroups ?? current.creatorGroups),
|
|
31
|
+
validatorGroups: normalizeCsv(payload.validatorGroups ?? current.validatorGroups),
|
|
32
|
+
notifyGroups: normalizeCsv(payload.notifyGroups ?? current.notifyGroups),
|
|
33
|
+
|
|
34
|
+
holdMinutes: Math.max(1, parseInt(payload.holdMinutes ?? current.holdMinutes, 10) || 5),
|
|
35
|
+
};
|
|
19
36
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
37
|
+
// secret only if provided and not masked
|
|
38
|
+
if (payload.helloassoClientSecret && payload.helloassoClientSecret !== '***') {
|
|
39
|
+
toSave.helloassoClientSecret = payload.helloassoClientSecret;
|
|
40
|
+
} else if (current.helloassoClientSecret) {
|
|
41
|
+
toSave.helloassoClientSecret = current.helloassoClientSecret;
|
|
42
|
+
}
|
|
24
43
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
44
|
+
await meta.settings.set('calendar-onekite', toSave);
|
|
45
|
+
res.json({ ok:true });
|
|
46
|
+
}
|
|
29
47
|
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
async function listPending(req, res) {
|
|
49
|
+
// Fetch by broad range (last 2 years) and filter pending
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
const ids = await reservationDb.listReservationIdsByStartRange(now - 86400000*365*2, now + 86400000*365*2);
|
|
32
52
|
const pending = [];
|
|
33
53
|
for (const rid of ids) {
|
|
34
|
-
const r = await
|
|
35
|
-
if (r
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const r = await dbLayer.getReservation(rid);
|
|
46
|
-
if (!r) return res.status(404).json({ error: 'not-found' });
|
|
47
|
-
|
|
48
|
-
r.status = 'approved';
|
|
49
|
-
|
|
50
|
-
// Create HelloAsso payment link if configured
|
|
51
|
-
const settings = await meta.settings.get('calendar-onekite');
|
|
52
|
-
const env = settings.helloassoEnv || 'prod';
|
|
53
|
-
const token = await helloasso.getAccessToken({
|
|
54
|
-
env,
|
|
55
|
-
clientId: settings.helloassoClientId,
|
|
56
|
-
clientSecret: settings.helloassoClientSecret,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
let paymentUrl = null;
|
|
60
|
-
if (token) {
|
|
61
|
-
const requester = await user.getUserData(r.uid);
|
|
62
|
-
// Determine amount from HelloAsso items (pricing comes from HelloAsso)
|
|
63
|
-
let totalAmount = 0;
|
|
64
|
-
try {
|
|
65
|
-
const items = await helloasso.listItems({
|
|
66
|
-
env,
|
|
67
|
-
token,
|
|
68
|
-
organizationSlug: settings.helloassoOrganizationSlug,
|
|
69
|
-
formType: settings.helloassoFormType,
|
|
70
|
-
formSlug: settings.helloassoFormSlug,
|
|
71
|
-
});
|
|
72
|
-
const normalized = (items || []).map((it) => ({
|
|
73
|
-
id: String(it.id || it.itemId || it.reference || it.name),
|
|
74
|
-
price: it.price || it.amount || it.unitPrice || 0,
|
|
75
|
-
})).filter(it => it.id);
|
|
76
|
-
const match = normalized.find(it => it.id === String(r.itemId));
|
|
77
|
-
totalAmount = match ? parseInt(match.price, 10) || 0 : 0;
|
|
78
|
-
} catch (e) {
|
|
79
|
-
totalAmount = 0;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!totalAmount) {
|
|
83
|
-
return res.status(400).json({ error: 'item-price-not-found' });
|
|
84
|
-
}
|
|
85
|
-
paymentUrl = await helloasso.createCheckoutIntent({
|
|
86
|
-
env,
|
|
87
|
-
token,
|
|
88
|
-
organizationSlug: settings.helloassoOrganizationSlug,
|
|
89
|
-
formType: settings.helloassoFormType,
|
|
90
|
-
formSlug: settings.helloassoFormSlug,
|
|
91
|
-
totalAmount,
|
|
92
|
-
payerEmail: requester && requester.email,
|
|
54
|
+
const r = await reservationDb.getReservation(rid);
|
|
55
|
+
if (!r) continue;
|
|
56
|
+
if (r.status !== 'pending') continue;
|
|
57
|
+
const user = await User.getUserFields(Number(r.uid), ['username']);
|
|
58
|
+
pending.push({
|
|
59
|
+
rid,
|
|
60
|
+
username: user?.username || '',
|
|
61
|
+
itemName: r.itemName,
|
|
62
|
+
startDate: toDateOnlyISO(Number(r.startTs)),
|
|
63
|
+
endDate: toDateOnlyISO(Number(r.endTs)),
|
|
64
|
+
status: r.status,
|
|
93
65
|
});
|
|
94
66
|
}
|
|
67
|
+
res.json({ ok:true, pending });
|
|
68
|
+
}
|
|
95
69
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
70
|
+
async function approveReservation(req, res) {
|
|
71
|
+
const api = require('./api');
|
|
72
|
+
return api.approveReservation(req, res);
|
|
73
|
+
}
|
|
99
74
|
|
|
100
|
-
|
|
75
|
+
async function refuseReservation(req, res) {
|
|
76
|
+
const api = require('./api');
|
|
77
|
+
return api.refuseReservation(req, res);
|
|
78
|
+
}
|
|
101
79
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const requester = await user.getUserData(r.uid);
|
|
105
|
-
if (requester && requester.email) {
|
|
106
|
-
await emailer.send('calendar-onekite_approved', requester.email, {
|
|
107
|
-
username: requester.username,
|
|
108
|
-
itemName: r.itemName,
|
|
109
|
-
start: new Date(parseInt(r.start, 10)).toISOString(),
|
|
110
|
-
end: new Date(parseInt(r.end, 10)).toISOString(),
|
|
111
|
-
paymentUrl: paymentUrl || '',
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
} catch (e) {}
|
|
115
|
-
|
|
116
|
-
res.json({ ok: true, paymentUrl: paymentUrl || null });
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
admin.refuseReservation = async function (req, res) {
|
|
120
|
-
const rid = req.params.rid;
|
|
121
|
-
const r = await dbLayer.getReservation(rid);
|
|
122
|
-
if (!r) return res.status(404).json({ error: 'not-found' });
|
|
123
|
-
|
|
124
|
-
r.status = 'refused';
|
|
125
|
-
await dbLayer.saveReservation(r);
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
const requester = await user.getUserData(r.uid);
|
|
129
|
-
if (requester && requester.email) {
|
|
130
|
-
await emailer.send('calendar-onekite_refused', requester.email, {
|
|
131
|
-
username: requester.username,
|
|
132
|
-
itemName: r.itemName,
|
|
133
|
-
start: new Date(parseInt(r.start, 10)).toISOString(),
|
|
134
|
-
end: new Date(parseInt(r.end, 10)).toISOString(),
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
} catch (e) {}
|
|
138
|
-
|
|
139
|
-
res.json({ ok: true });
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
admin.purgeByYear = async function (req, res) {
|
|
143
|
-
const year = (req.body && req.body.year ? String(req.body.year) : '').trim();
|
|
80
|
+
async function purgeByYear(req, res) {
|
|
81
|
+
const year = String(req.body.year || '').trim();
|
|
144
82
|
if (!/^\d{4}$/.test(year)) {
|
|
145
|
-
return res.status(400).json({ error:
|
|
83
|
+
return res.status(400).json({ ok:false, error:'invalid-year' });
|
|
146
84
|
}
|
|
147
85
|
const y = parseInt(year, 10);
|
|
148
|
-
const
|
|
149
|
-
const
|
|
86
|
+
const start = Date.UTC(y,0,1,0,0,0,0);
|
|
87
|
+
const end = Date.UTC(y+1,0,1,0,0,0,0);
|
|
150
88
|
|
|
151
|
-
const ids = await
|
|
152
|
-
let count = 0;
|
|
89
|
+
const ids = await reservationDb.listReservationIdsByStartRange(start, end, 100000);
|
|
153
90
|
for (const rid of ids) {
|
|
154
|
-
await
|
|
155
|
-
|
|
91
|
+
const r = await reservationDb.getReservation(rid);
|
|
92
|
+
if (!r) continue;
|
|
93
|
+
const st = Number(r.startTs);
|
|
94
|
+
if (st >= start && st < end) await reservationDb.deleteReservation(rid);
|
|
156
95
|
}
|
|
157
|
-
res.json({ ok:
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Debug endpoint to validate HelloAsso connectivity and item loading
|
|
161
|
-
admin.debugHelloAsso = async function (req, res) {
|
|
162
|
-
const settings = await meta.settings.get('calendar-onekite');
|
|
163
|
-
const env = (settings && settings.helloassoEnv) || 'prod';
|
|
164
|
-
|
|
165
|
-
// Never expose secrets in debug output
|
|
166
|
-
const safeSettings = {
|
|
167
|
-
helloassoEnv: env,
|
|
168
|
-
helloassoClientId: settings && settings.helloassoClientId ? String(settings.helloassoClientId) : '',
|
|
169
|
-
helloassoClientSecret: settings && settings.helloassoClientSecret ? '***' : '',
|
|
170
|
-
helloassoOrganizationSlug: settings && settings.helloassoOrganizationSlug ? String(settings.helloassoOrganizationSlug) : '',
|
|
171
|
-
helloassoFormType: settings && settings.helloassoFormType ? String(settings.helloassoFormType) : '',
|
|
172
|
-
helloassoFormSlug: settings && settings.helloassoFormSlug ? String(settings.helloassoFormSlug) : '',
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const out = {
|
|
176
|
-
ok: true,
|
|
177
|
-
settings: safeSettings,
|
|
178
|
-
token: { ok: false },
|
|
179
|
-
// Catalog = what you actually want for a shop (available products/material)
|
|
180
|
-
catalog: { ok: false, count: 0, sample: [], keys: [] },
|
|
181
|
-
// Sold items = items present in orders (can be 0 if no sales yet)
|
|
182
|
-
soldItems: { ok: false, count: 0, sample: [] },
|
|
183
|
-
};
|
|
96
|
+
res.json({ ok:true, purged: ids.length });
|
|
97
|
+
}
|
|
184
98
|
|
|
99
|
+
async function debugHelloAsso(req, res) {
|
|
100
|
+
const s = await meta.settings.get('calendar-onekite') || {};
|
|
101
|
+
const safe = { ...s, helloassoClientSecret: s.helloassoClientSecret ? '***' : '' };
|
|
185
102
|
try {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
103
|
+
const tokenOk = !!(await helloasso.getToken(s));
|
|
104
|
+
const cat = await helloasso.getCatalog(s);
|
|
105
|
+
res.json({
|
|
106
|
+
ok:true,
|
|
107
|
+
settings: safe,
|
|
108
|
+
token: { ok: tokenOk },
|
|
109
|
+
items: { ok:true, count: (cat.items||[]).length, sample: (cat.items||[]).slice(0,10) }
|
|
190
110
|
});
|
|
191
|
-
if (!token) {
|
|
192
|
-
out.token = { ok: false, error: 'token-null' };
|
|
193
|
-
return res.json(out);
|
|
194
|
-
}
|
|
195
|
-
out.token = { ok: true };
|
|
196
|
-
|
|
197
|
-
// Catalog items (via /public)
|
|
198
|
-
try {
|
|
199
|
-
const { publicForm, items } = await helloasso.listCatalogItems({
|
|
200
|
-
env,
|
|
201
|
-
token,
|
|
202
|
-
organizationSlug: settings.helloassoOrganizationSlug,
|
|
203
|
-
formType: settings.helloassoFormType,
|
|
204
|
-
formSlug: settings.helloassoFormSlug,
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
const arr = Array.isArray(items) ? items : [];
|
|
208
|
-
out.catalog.ok = true;
|
|
209
|
-
out.catalog.count = arr.length;
|
|
210
|
-
out.catalog.keys = publicForm && typeof publicForm === 'object' ? Object.keys(publicForm) : [];
|
|
211
|
-
out.catalog.sample = arr.slice(0, 10).map((it) => ({
|
|
212
|
-
id: it.id,
|
|
213
|
-
name: it.name,
|
|
214
|
-
price: it.price ?? null,
|
|
215
|
-
}));
|
|
216
|
-
} catch (e) {
|
|
217
|
-
out.catalog = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [], keys: [] };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Sold items
|
|
221
|
-
try {
|
|
222
|
-
const items = await helloasso.listItems({
|
|
223
|
-
env,
|
|
224
|
-
token,
|
|
225
|
-
organizationSlug: settings.helloassoOrganizationSlug,
|
|
226
|
-
formType: settings.helloassoFormType,
|
|
227
|
-
formSlug: settings.helloassoFormSlug,
|
|
228
|
-
});
|
|
229
|
-
const arr = Array.isArray(items) ? items : [];
|
|
230
|
-
out.soldItems.ok = true;
|
|
231
|
-
out.soldItems.count = arr.length;
|
|
232
|
-
out.soldItems.sample = arr.slice(0, 10).map((it) => ({
|
|
233
|
-
id: it.id || it.itemId || it.reference || it.name,
|
|
234
|
-
name: it.name || it.label || it.itemName,
|
|
235
|
-
price: it.price || it.amount || it.unitPrice || null,
|
|
236
|
-
}));
|
|
237
|
-
} catch (e) {
|
|
238
|
-
out.soldItems = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [] };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return res.json(out);
|
|
242
111
|
} catch (e) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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),
|
|
118
|
+
});
|
|
246
119
|
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function renderAdmin(req, res) {
|
|
123
|
+
res.render('admin/plugins/calendar-onekite', {});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
renderAdmin,
|
|
128
|
+
getSettings,
|
|
129
|
+
saveSettings,
|
|
130
|
+
listPending,
|
|
131
|
+
approveReservation,
|
|
132
|
+
refuseReservation,
|
|
133
|
+
purgeByYear,
|
|
134
|
+
debugHelloAsso,
|
|
247
135
|
};
|
|
248
|
-
|
|
249
|
-
module.exports = admin;
|