nodebb-plugin-calendar-onekite 11.1.33 → 11.1.34

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 CHANGED
@@ -1,135 +1,249 @@
1
-
2
1
  'use strict';
3
2
 
4
3
  const meta = require.main.require('./src/meta');
5
- const User = require.main.require('./src/user');
4
+ const user = require.main.require('./src/user');
5
+ const emailer = require.main.require('./src/emailer');
6
+
7
+ const dbLayer = require('./db');
6
8
  const helloasso = require('./helloasso');
7
- const reservationDb = require('./db');
8
- const { csvToList, toDateOnlyISO } = require('./utils');
9
9
 
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
- };
10
+ const ADMIN_PRIV = 'admin:settings';
36
11
 
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
- }
12
+ const admin = {};
13
+
14
+ admin.renderAdmin = async function (req, res) {
15
+ res.render('admin/plugins/calendar-onekite', {
16
+ title: 'Calendar OneKite',
17
+ });
18
+ };
43
19
 
44
- await meta.settings.set('calendar-onekite', toSave);
45
- res.json({ ok:true });
46
- }
20
+ admin.getSettings = async function (req, res) {
21
+ const settings = await meta.settings.get('calendar-onekite');
22
+ res.json(settings || {});
23
+ };
47
24
 
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);
25
+ admin.saveSettings = async function (req, res) {
26
+ await meta.settings.set('calendar-onekite', req.body || {});
27
+ res.json({ ok: true });
28
+ };
29
+
30
+ admin.listPending = async function (req, res) {
31
+ const ids = await dbLayer.listAllReservationIds(5000);
52
32
  const pending = [];
53
33
  for (const rid of ids) {
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,
34
+ const r = await dbLayer.getReservation(rid);
35
+ if (r && r.status === 'pending') {
36
+ pending.push(r);
37
+ }
38
+ }
39
+ pending.sort((a, b) => parseInt(a.start, 10) - parseInt(b.start, 10));
40
+ res.json(pending);
41
+ };
42
+
43
+ admin.approveReservation = async function (req, res) {
44
+ const rid = req.params.rid;
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,
65
93
  });
66
94
  }
67
- res.json({ ok:true, pending });
68
- }
69
95
 
70
- async function approveReservation(req, res) {
71
- const api = require('./api');
72
- return api.approveReservation(req, res);
73
- }
96
+ if (paymentUrl) {
97
+ r.paymentUrl = paymentUrl;
98
+ }
74
99
 
75
- async function refuseReservation(req, res) {
76
- const api = require('./api');
77
- return api.refuseReservation(req, res);
78
- }
100
+ await dbLayer.saveReservation(r);
79
101
 
80
- async function purgeByYear(req, res) {
81
- const year = String(req.body.year || '').trim();
102
+ // Email requester
103
+ try {
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();
82
144
  if (!/^\d{4}$/.test(year)) {
83
- return res.status(400).json({ ok:false, error:'invalid-year' });
145
+ return res.status(400).json({ error: 'invalid-year' });
84
146
  }
85
147
  const y = parseInt(year, 10);
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);
148
+ const startTs = new Date(Date.UTC(y, 0, 1)).getTime();
149
+ const endTs = new Date(Date.UTC(y + 1, 0, 1)).getTime() - 1;
88
150
 
89
- const ids = await reservationDb.listReservationIdsByStartRange(start, end, 100000);
151
+ const ids = await dbLayer.listReservationIdsByStartRange(startTs, endTs, 100000);
152
+ let count = 0;
90
153
  for (const rid of ids) {
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);
154
+ await dbLayer.removeReservation(rid);
155
+ count++;
95
156
  }
96
- res.json({ ok:true, purged: ids.length });
97
- }
157
+ res.json({ ok: true, removed: count });
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
+ };
98
184
 
99
- async function debugHelloAsso(req, res) {
100
- const s = await meta.settings.get('calendar-onekite') || {};
101
- const safe = { ...s, helloassoClientSecret: s.helloassoClientSecret ? '***' : '' };
102
185
  try {
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) }
186
+ const token = await helloasso.getAccessToken({
187
+ env,
188
+ clientId: settings.helloassoClientId,
189
+ clientSecret: settings.helloassoClientSecret,
110
190
  });
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);
111
242
  } 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),
118
- });
243
+ out.ok = false;
244
+ out.token = { ok: false, error: String(e && e.message ? e.message : e) };
245
+ return res.json(out);
119
246
  }
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,
135
247
  };
248
+
249
+ module.exports = admin;