nodebb-plugin-onekite-calendar 2.0.87 → 2.0.89
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 +30 -6
- package/lib/api.js +104 -0
- package/library.js +1 -0
- package/package.json +1 -1
- package/public/client.js +16 -3
package/lib/admin.js
CHANGED
|
@@ -180,6 +180,19 @@ admin.approveReservation = async function (req, res) {
|
|
|
180
180
|
|
|
181
181
|
await dbLayer.saveReservation(r);
|
|
182
182
|
|
|
183
|
+
// Audit
|
|
184
|
+
try {
|
|
185
|
+
const year = new Date().getFullYear();
|
|
186
|
+
await dbLayer.addAuditEntry({
|
|
187
|
+
ts: Date.now(), year, action: 'reservation_approved',
|
|
188
|
+
targetType: 'reservation', targetId: String(rid),
|
|
189
|
+
reservationUid: Number(r.uid) || 0,
|
|
190
|
+
reservationUsername: String(r.username || ''),
|
|
191
|
+
actorUid: req.uid || 0,
|
|
192
|
+
actorUsername: r.approvedByUsername || '',
|
|
193
|
+
});
|
|
194
|
+
} catch (e) {}
|
|
195
|
+
|
|
183
196
|
// Real-time refresh for all viewers
|
|
184
197
|
realtime.emitCalendarUpdated({ kind: 'reservation', action: 'approved', rid: String(rid), status: r.status });
|
|
185
198
|
|
|
@@ -259,7 +272,6 @@ admin.markReservationPaid = async function (req, res) {
|
|
|
259
272
|
const requesterUid = parseInt(r.uid, 10);
|
|
260
273
|
const requester = await user.getUserFields(requesterUid, ['username']);
|
|
261
274
|
if (requesterUid) {
|
|
262
|
-
const { sendEmail, formatFR, buildCalendarLinks } = shared;
|
|
263
275
|
await sendEmail('calendar-onekite_paid', requesterUid, 'Location matériel - Paiement reçu', {
|
|
264
276
|
uid: requesterUid,
|
|
265
277
|
username: requester && requester.username ? requester.username : '',
|
|
@@ -294,6 +306,19 @@ admin.refuseReservation = async function (req, res) {
|
|
|
294
306
|
r.refusedReason = String((req.body && (req.body.reason || req.body.refusedReason || req.body.refuseReason)) || '').trim();
|
|
295
307
|
await dbLayer.saveReservation(r);
|
|
296
308
|
|
|
309
|
+
// Audit
|
|
310
|
+
try {
|
|
311
|
+
const year = new Date().getFullYear();
|
|
312
|
+
await dbLayer.addAuditEntry({
|
|
313
|
+
ts: Date.now(), year, action: 'reservation_refused',
|
|
314
|
+
targetType: 'reservation', targetId: String(rid),
|
|
315
|
+
reservationUid: Number(r.uid) || 0,
|
|
316
|
+
reservationUsername: String(r.username || ''),
|
|
317
|
+
actorUid: req.uid || 0,
|
|
318
|
+
refusedReason: r.refusedReason || '',
|
|
319
|
+
});
|
|
320
|
+
} catch (e) {}
|
|
321
|
+
|
|
297
322
|
// Real-time refresh for all viewers
|
|
298
323
|
realtime.emitCalendarUpdated({ kind: 'reservation', action: 'refused', rid: String(rid), status: r.status });
|
|
299
324
|
|
|
@@ -327,12 +352,11 @@ admin.purgeByYear = async function (req, res) {
|
|
|
327
352
|
const endTs = new Date(Date.UTC(y + 1, 0, 1)).getTime() - 1;
|
|
328
353
|
|
|
329
354
|
const ids = await dbLayer.listReservationIdsByStartRange(startTs, endTs, 100000);
|
|
355
|
+
const rows = ids && ids.length ? await dbLayer.getReservations(ids) : [];
|
|
330
356
|
let removed = 0;
|
|
331
|
-
for (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
await dbLayer.removeReservation(rid);
|
|
357
|
+
for (let i = 0; i < ids.length; i++) {
|
|
358
|
+
if (!rows[i]) continue;
|
|
359
|
+
await dbLayer.removeReservation(ids[i]);
|
|
336
360
|
removed++;
|
|
337
361
|
}
|
|
338
362
|
try {
|
package/lib/api.js
CHANGED
|
@@ -621,6 +621,110 @@ api.getReservationDetails = async function (req, res) {
|
|
|
621
621
|
return res.json(out);
|
|
622
622
|
};
|
|
623
623
|
|
|
624
|
+
/**
|
|
625
|
+
* Return a fresh HelloAsso payment URL for a reservation in awaiting_payment status.
|
|
626
|
+
* HelloAsso checkout intents expire after ~30 minutes, so stored URLs become stale.
|
|
627
|
+
* This endpoint regenerates a new intent on demand and persists the fresh URL.
|
|
628
|
+
*
|
|
629
|
+
* @route GET /api/v3/plugins/calendar-onekite/reservations/:rid/payment-url
|
|
630
|
+
*/
|
|
631
|
+
api.getFreshPaymentUrl = async function (req, res) {
|
|
632
|
+
const uid = req.uid;
|
|
633
|
+
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
634
|
+
|
|
635
|
+
const settings = await getSettings();
|
|
636
|
+
const rid = String(req.params.rid || '').trim();
|
|
637
|
+
if (!rid) return res.status(400).json({ error: 'missing-rid' });
|
|
638
|
+
|
|
639
|
+
const r = await dbLayer.getReservation(rid);
|
|
640
|
+
if (!r) return res.status(404).json({ error: 'not-found' });
|
|
641
|
+
|
|
642
|
+
const isOwner = String(r.uid) === String(uid);
|
|
643
|
+
const isMod = await canValidate(uid, settings);
|
|
644
|
+
if (!isOwner && !isMod) return res.status(403).json({ error: 'not-allowed' });
|
|
645
|
+
|
|
646
|
+
if (r.status !== 'awaiting_payment') {
|
|
647
|
+
return res.status(409).json({ error: 'wrong-status', status: r.status });
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Build a fresh checkout intent
|
|
651
|
+
const env = settings.helloassoEnv || 'prod';
|
|
652
|
+
const token = await helloasso.getAccessToken({
|
|
653
|
+
env,
|
|
654
|
+
clientId: settings.helloassoClientId,
|
|
655
|
+
clientSecret: settings.helloassoClientSecret,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
if (!token) {
|
|
659
|
+
return res.status(503).json({ error: 'helloasso-unavailable' });
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const requester = await user.getUserFields(r.uid, ['email']);
|
|
663
|
+
const year = yearFromTs(Number(r.start));
|
|
664
|
+
const formSlug = autoFormSlugForYear(year);
|
|
665
|
+
|
|
666
|
+
// Recompute total from HelloAsso catalog to always use up-to-date prices.
|
|
667
|
+
let recomputedTotalCents = null;
|
|
668
|
+
try {
|
|
669
|
+
const { items: catalog } = await helloasso.listCatalogItems({
|
|
670
|
+
env,
|
|
671
|
+
token,
|
|
672
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
673
|
+
formType: settings.helloassoFormType,
|
|
674
|
+
formSlug,
|
|
675
|
+
});
|
|
676
|
+
const byId = new Map((catalog || []).map((it) => [String(it.id), (typeof it.price === 'number' ? it.price : 0)]));
|
|
677
|
+
const ids = (Array.isArray(r.itemIds) ? r.itemIds : (r.itemId ? [r.itemId] : [])).map(String);
|
|
678
|
+
const days = calendarDaysExclusiveYmd(r.startDate, r.endDate) || 1;
|
|
679
|
+
const sumCentsPerDay = ids.reduce((acc, id) => acc + (byId.get(id) || 0), 0);
|
|
680
|
+
if (sumCentsPerDay > 0) {
|
|
681
|
+
recomputedTotalCents = Math.max(0, Math.round(sumCentsPerDay * days));
|
|
682
|
+
}
|
|
683
|
+
} catch (e) { /* fallback to stored total */ }
|
|
684
|
+
|
|
685
|
+
const totalAmount = (typeof recomputedTotalCents === 'number')
|
|
686
|
+
? recomputedTotalCents
|
|
687
|
+
: Math.max(0, Math.round((Number(r.total) || 0) * 100));
|
|
688
|
+
|
|
689
|
+
if (!totalAmount) {
|
|
690
|
+
return res.status(422).json({ error: 'zero-amount' });
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const intent = await helloasso.createCheckoutIntent({
|
|
694
|
+
env,
|
|
695
|
+
token,
|
|
696
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
697
|
+
formType: settings.helloassoFormType,
|
|
698
|
+
formSlug,
|
|
699
|
+
totalAmount,
|
|
700
|
+
payerEmail: requester && requester.email ? requester.email : '',
|
|
701
|
+
callbackUrl: normalizeReturnUrl(),
|
|
702
|
+
webhookUrl: normalizeCallbackUrl(settings.helloassoCallbackUrl),
|
|
703
|
+
itemName: buildHelloAssoItemName('Réservation matériel Onekite', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
|
|
704
|
+
containsDonation: false,
|
|
705
|
+
metadata: {
|
|
706
|
+
reservationId: String(rid),
|
|
707
|
+
items: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
|
|
708
|
+
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const paymentUrl = intent && (intent.paymentUrl || intent.redirectUrl)
|
|
713
|
+
? (intent.paymentUrl || intent.redirectUrl)
|
|
714
|
+
: (typeof intent === 'string' ? intent : null);
|
|
715
|
+
|
|
716
|
+
if (!paymentUrl) {
|
|
717
|
+
return res.status(502).json({ error: 'intent-creation-failed' });
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Persist the fresh URL so subsequent requests can also renew from DB.
|
|
721
|
+
r.paymentUrl = paymentUrl;
|
|
722
|
+
if (intent && intent.checkoutIntentId) r.checkoutIntentId = intent.checkoutIntentId;
|
|
723
|
+
await dbLayer.saveReservation(r);
|
|
724
|
+
|
|
725
|
+
return res.json({ ok: true, paymentUrl });
|
|
726
|
+
};
|
|
727
|
+
|
|
624
728
|
/**
|
|
625
729
|
* Get detailed information about a special event.
|
|
626
730
|
*
|
package/library.js
CHANGED
|
@@ -76,6 +76,7 @@ Plugin.init = async function (params) {
|
|
|
76
76
|
|
|
77
77
|
router.post('/api/v3/plugins/calendar-onekite/reservations', ...publicExpose, api.createReservation);
|
|
78
78
|
router.get('/api/v3/plugins/calendar-onekite/reservations/:rid', ...publicExpose, api.getReservationDetails);
|
|
79
|
+
router.get('/api/v3/plugins/calendar-onekite/reservations/:rid/payment-url', ...publicExpose, api.getFreshPaymentUrl);
|
|
79
80
|
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/approve', ...publicExpose, api.approveReservation);
|
|
80
81
|
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/refuse', ...publicExpose, api.refuseReservation);
|
|
81
82
|
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/cancel', ...publicExpose, api.cancelReservation);
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -2403,9 +2403,22 @@ function toDatetimeLocalValue(date) {
|
|
|
2403
2403
|
label: 'Payer maintenant',
|
|
2404
2404
|
className: 'btn-primary',
|
|
2405
2405
|
callback: () => {
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2406
|
+
(async () => {
|
|
2407
|
+
try {
|
|
2408
|
+
// Always fetch a fresh checkout intent — HelloAsso URLs expire after ~30 min.
|
|
2409
|
+
const resp = await fetchJson(
|
|
2410
|
+
`/api/v3/plugins/calendar-onekite/reservations/${encodeURIComponent(String(rid))}/payment-url`
|
|
2411
|
+
);
|
|
2412
|
+
const freshUrl = resp && resp.paymentUrl ? String(resp.paymentUrl) : '';
|
|
2413
|
+
if (freshUrl && /^https?:\/\//i.test(freshUrl)) {
|
|
2414
|
+
window.open(freshUrl, '_blank', 'noopener');
|
|
2415
|
+
} else {
|
|
2416
|
+
showAlert('error', 'Impossible de générer le lien de paiement. Veuillez réessayer.');
|
|
2417
|
+
}
|
|
2418
|
+
} catch (e) {
|
|
2419
|
+
showAlert('error', 'Erreur lors de la génération du lien de paiement.');
|
|
2420
|
+
}
|
|
2421
|
+
})();
|
|
2409
2422
|
return false;
|
|
2410
2423
|
},
|
|
2411
2424
|
};
|