nodebb-plugin-onekite-calendar 2.0.88 → 2.0.90

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
@@ -17,6 +17,7 @@ const {
17
17
  calendarDaysExclusiveYmd,
18
18
  yearFromTs,
19
19
  autoFormSlugForYear,
20
+ forumBaseUrl,
20
21
  } = shared;
21
22
 
22
23
  const dbLayer = require('./db');
@@ -213,6 +214,7 @@ admin.approveReservation = async function (req, res) {
213
214
  itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
214
215
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
215
216
  paymentUrl: paymentUrl || '',
217
+ calendarUrl: `${forumBaseUrl()}/calendar`,
216
218
  pickupAddress: r.pickupAddress || '',
217
219
  notes: r.notes || '',
218
220
  pickupTime: r.pickupTime || '',
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
  *
@@ -1549,6 +1653,7 @@ api.createReservation = async function (req, res) {
1549
1653
  pickupLon: '',
1550
1654
  mapUrl: '',
1551
1655
  paymentUrl: resv.paymentUrl || '',
1656
+ calendarUrl: `${forumBaseUrl()}/calendar`,
1552
1657
  validatedBy: creatorUsername || '',
1553
1658
  validatedByUrl: creatorUsername ? `${forumBaseUrl()}/user/${encodeURIComponent(String(creatorUsername))}` : '',
1554
1659
  ...buildCalendarLinks({
@@ -1785,6 +1890,7 @@ api.approveReservation = async function (req, res) {
1785
1890
  pickupLon: r.pickupLon || '',
1786
1891
  mapUrl,
1787
1892
  paymentUrl: r.paymentUrl || '',
1893
+ calendarUrl: `${forumBaseUrl()}/calendar`,
1788
1894
  ...buildCalendarLinks({
1789
1895
  type: 'reservation',
1790
1896
  id: String(r.rid),
package/lib/scheduler.js CHANGED
@@ -253,6 +253,7 @@ async function processAwaitingPayment(preIds, preReservations) {
253
253
  itemNames: arrayifyNames(r),
254
254
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
255
255
  paymentUrl: r.paymentUrl || '',
256
+ calendarUrl: `${forumBaseUrl()}/calendar`,
256
257
  delayMinutes: holdMins,
257
258
  pickupLine: r.pickupTime ? (r.adminNote ? `${r.pickupTime} à ${r.adminNote}` : r.pickupTime) : '',
258
259
  });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.88",
3
+ "version": "2.0.90",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
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
- try {
2407
- window.open(paymentUrl, '_blank', 'noopener');
2408
- } catch (e) {}
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
  };
@@ -40,11 +40,16 @@
40
40
  <p><strong>Notes :</strong><br>{notes}</p>
41
41
  <!-- ENDIF notes -->
42
42
 
43
- <!-- IF paymentUrl -->
43
+ <!-- IF calendarUrl -->
44
44
  <p>
45
- <a href="{paymentUrl}" style="display:inline-block;padding:12px 18px;border-radius:6px;text-decoration:none;border:1px solid #333;">
46
- Payer maintenant
45
+ <a href="{calendarUrl}" style="display:inline-block;padding:12px 18px;border-radius:6px;text-decoration:none;border:1px solid #333;">
46
+ Accéder au calendrier pour payer
47
47
  </a>
48
48
  </p>
49
- <p style="font-size: 12px; color: #666;">Si le bouton ne fonctionne pas, utilisez ce lien : <a href="{paymentUrl}">{paymentUrl}</a></p>
50
- <!-- ENDIF paymentUrl -->
49
+ <p style="font-size: 12px; color: #666;">
50
+ Sur le calendrier, cliquez sur votre réservation puis sur le bouton <strong>« Payer maintenant »</strong>.
51
+ <!-- IF paymentUrl -->
52
+ <br>Vous pouvez aussi payer directement via ce lien (valable quelques heures) : <a href="{paymentUrl}">{paymentUrl}</a>
53
+ <!-- ENDIF paymentUrl -->
54
+ </p>
55
+ <!-- ENDIF calendarUrl -->
@@ -10,11 +10,16 @@
10
10
 
11
11
  <p>{dateRange}</p>
12
12
 
13
- <!-- IF paymentUrl -->
13
+ <!-- IF calendarUrl -->
14
14
  <p>
15
- <a href="{paymentUrl}" style="display:inline-block;padding:12px 18px;border-radius:6px;text-decoration:none;border:1px solid #333;">
16
- Payer maintenant
15
+ <a href="{calendarUrl}" style="display:inline-block;padding:12px 18px;border-radius:6px;text-decoration:none;border:1px solid #333;">
16
+ Accéder au calendrier pour payer
17
17
  </a>
18
18
  </p>
19
- <p style="font-size: 12px; color: #666;">Si le bouton ne fonctionne pas, utilisez ce lien : <a href="{paymentUrl}">{paymentUrl}</a></p>
20
- <!-- ENDIF paymentUrl -->
19
+ <p style="font-size: 12px; color: #666;">
20
+ Sur le calendrier, cliquez sur votre réservation puis sur le bouton <strong>« Payer maintenant »</strong>.
21
+ <!-- IF paymentUrl -->
22
+ <br>Vous pouvez aussi payer directement via ce lien (valable quelques heures) : <a href="{paymentUrl}">{paymentUrl}</a>
23
+ <!-- ENDIF paymentUrl -->
24
+ </p>
25
+ <!-- ENDIF calendarUrl -->