nodebb-plugin-onekite-calendar 2.0.92 → 2.0.94

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
@@ -15,10 +15,13 @@ const {
15
15
  yearFromTs,
16
16
  autoFormSlugForYear,
17
17
  forumBaseUrl,
18
+ arrayifyIds,
19
+ arrayifyNames,
18
20
  } = shared;
19
21
 
20
22
  const dbLayer = require('./db');
21
23
  const helloasso = require('./helloasso');
24
+ const discord = require('./discord');
22
25
  const { buildCheckoutIntent } = require('./checkout');
23
26
 
24
27
  const ADMIN_PRIV = 'admin:settings';
@@ -151,8 +154,8 @@ admin.approveReservation = async function (req, res) {
151
154
  await sendEmail('calendar-onekite_approved', requesterUid, 'Location matériel - Réservation validée', {
152
155
  uid: requesterUid,
153
156
  username: requester && requester.username ? requester.username : '',
154
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
155
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
157
+ itemName: arrayifyNames(r).join(', '),
158
+ itemNames: arrayifyNames(r),
156
159
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
157
160
  paymentUrl: paymentUrl || '',
158
161
  calendarUrl: `${forumBaseUrl()}/calendar`,
@@ -165,13 +168,13 @@ admin.approveReservation = async function (req, res) {
165
168
  ...(buildCalendarLinks({
166
169
  rid: String(r.rid),
167
170
  uid: requesterUid,
168
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
171
+ itemNames: arrayifyNames(r),
169
172
  pickupAddress: r.pickupAddress || '',
170
173
  startYmd: (r.startDate && /^\d{4}-\d{2}-\d{2}$/.test(String(r.startDate))) ? String(r.startDate) : new Date(parseInt(r.start, 10)).toISOString().slice(0, 10),
171
174
  endYmd: (r.endDate && /^\d{4}-\d{2}-\d{2}$/.test(String(r.endDate))) ? String(r.endDate) : new Date(parseInt(r.end, 10)).toISOString().slice(0, 10),
172
175
  })),
173
176
  validatedBy: r.approvedByUsername || '',
174
- validatedByUrl: (r.approvedByUsername ? `https://www.onekite.com/user/${encodeURIComponent(String(r.approvedByUsername))}` : ''),
177
+ validatedByUrl: (r.approvedByUsername ? `${forumBaseUrl()}/user/${encodeURIComponent(String(r.approvedByUsername))}` : ''),
175
178
  });
176
179
  }
177
180
  } catch (e) {}
@@ -218,8 +221,8 @@ admin.markReservationPaid = async function (req, res) {
218
221
  await sendEmail('calendar-onekite_paid', requesterUid, 'Location matériel - Paiement reçu', {
219
222
  uid: requesterUid,
220
223
  username: requester && requester.username ? requester.username : '',
221
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
222
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
224
+ itemName: arrayifyNames(r).join(', '),
225
+ itemNames: arrayifyNames(r),
223
226
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
224
227
  paymentReceiptUrl: '',
225
228
  pickupTime: r.pickupTime || '',
@@ -227,7 +230,7 @@ admin.markReservationPaid = async function (req, res) {
227
230
  mapUrl: '',
228
231
  ...(buildCalendarLinks({
229
232
  type: 'reservation', id: String(r.rid || ''), uid: Number(r.uid) || 0,
230
- itemNames: Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : []),
233
+ itemNames: arrayifyNames(r),
231
234
  pickupAddress: r.pickupAddress || '', allDay: true,
232
235
  startYmd: (r.startDate && /^\d{4}-\d{2}-\d{2}$/.test(String(r.startDate))) ? String(r.startDate) : new Date(parseInt(r.start, 10)).toISOString().slice(0, 10),
233
236
  endYmd: (r.endDate && /^\d{4}-\d{2}-\d{2}$/.test(String(r.endDate))) ? String(r.endDate) : new Date(parseInt(r.end, 10)).toISOString().slice(0, 10),
@@ -272,8 +275,8 @@ admin.refuseReservation = async function (req, res) {
272
275
  await sendEmail('calendar-onekite_refused', requesterUid, 'Location matériel - Réservation refusée', {
273
276
  uid: requesterUid,
274
277
  username: requester && requester.username ? requester.username : '',
275
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
276
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
278
+ itemName: arrayifyNames(r).join(', '),
279
+ itemNames: arrayifyNames(r),
277
280
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
278
281
  start: formatFR(r.start),
279
282
  end: formatFR(r.end),
@@ -684,6 +687,131 @@ admin.exportAccountingCsv = async function (req, res) {
684
687
  };
685
688
 
686
689
 
690
+ // Sync payment status from HelloAsso for a reservation stuck in awaiting_payment.
691
+ // Uses the stored checkoutIntentId to query HelloAsso's API directly.
692
+ admin.syncPaymentFromHelloAsso = async function (req, res) {
693
+ const rid = String(req.params.rid || '').trim();
694
+ if (!rid) return res.status(400).json({ error: 'missing-rid' });
695
+
696
+ const r = await dbLayer.getReservation(rid);
697
+ if (!r) return res.status(404).json({ error: 'not-found' });
698
+
699
+ if (r.status === 'paid') {
700
+ return res.json({ ok: true, alreadyPaid: true, status: r.status });
701
+ }
702
+
703
+ const checkoutIntentId = r.checkoutIntentId ? String(r.checkoutIntentId) : null;
704
+ if (!checkoutIntentId) {
705
+ return res.json({ ok: false, reason: 'no-checkout-intent-id', status: r.status });
706
+ }
707
+
708
+ const settings = await getSettings();
709
+ const token = await helloasso.getAccessToken({
710
+ env: settings.helloassoEnv || 'prod',
711
+ clientId: settings.helloassoClientId,
712
+ clientSecret: settings.helloassoClientSecret,
713
+ });
714
+ if (!token) {
715
+ return res.json({ ok: false, reason: 'helloasso-auth-failed' });
716
+ }
717
+
718
+ const details = await helloasso.getCheckoutIntentDetails({
719
+ env: settings.helloassoEnv || 'prod',
720
+ token,
721
+ organizationSlug: settings.helloassoOrganizationSlug,
722
+ checkoutIntentId,
723
+ });
724
+
725
+ if (!details) {
726
+ return res.json({ ok: false, reason: 'checkout-intent-not-found', checkoutIntentId });
727
+ }
728
+
729
+ // Determine payment state from checkout intent details
730
+ const order = (details && details.order) ? details.order : details;
731
+ const orderState = String((order && (order.state || order.status)) || '').toLowerCase();
732
+ const paymentsArr = (order && Array.isArray(order.payments)) ? order.payments : [];
733
+ const paymentState = paymentsArr.length
734
+ ? String((paymentsArr[0].state || paymentsArr[0].status) || '').toLowerCase()
735
+ : '';
736
+ const okStates = ['confirmed', 'authorized', 'paid', 'processed', 'succeeded', 'success'];
737
+ const isPaid = okStates.includes(orderState) || okStates.includes(paymentState);
738
+
739
+ if (!isPaid) {
740
+ return res.json({ ok: false, reason: 'not-paid-on-helloasso', orderState, paymentState });
741
+ }
742
+
743
+ // Mark as paid
744
+ const prevStatus = r.status;
745
+ r.status = 'paid';
746
+ r.paidAt = Date.now();
747
+ r.syncedFromHelloAsso = true;
748
+ r.syncedBy = req.uid;
749
+ const paymentId = paymentsArr.length ? String(paymentsArr[0].id || '') : '';
750
+ const paymentReceiptUrl = paymentsArr.length
751
+ ? String(paymentsArr[0].paymentReceiptUrl || paymentsArr[0].receiptUrl || '')
752
+ : '';
753
+ if (paymentId) r.paymentId = paymentId;
754
+ if (paymentReceiptUrl) r.paymentReceiptUrl = paymentReceiptUrl;
755
+ await dbLayer.saveReservation(r);
756
+
757
+ try {
758
+ await dbLayer.addAuditEntry({
759
+ ts: Date.now(), year: new Date().getFullYear(),
760
+ action: 'reservation_sync_paid',
761
+ targetType: 'reservation', targetId: String(r.rid),
762
+ reservationUid: Number(r.uid) || 0,
763
+ reservationUsername: String(r.username || ''),
764
+ actorUid: req.uid || 0,
765
+ prevStatus,
766
+ });
767
+ } catch (e) {}
768
+
769
+ realtime.emitCalendarUpdated({ kind: 'reservation', action: 'paid', rid: String(r.rid), status: 'paid' });
770
+
771
+ try {
772
+ const requesterUid = parseInt(r.uid, 10);
773
+ if (requesterUid) {
774
+ const requester = await user.getUserFields(requesterUid, ['username']);
775
+ const latNum = Number(r.pickupLat);
776
+ const lonNum = Number(r.pickupLon);
777
+ const mapUrl = (Number.isFinite(latNum) && Number.isFinite(lonNum))
778
+ ? `https://www.openstreetmap.org/?mlat=${encodeURIComponent(String(latNum))}&mlon=${encodeURIComponent(String(lonNum))}#map=18/${encodeURIComponent(String(latNum))}/${encodeURIComponent(String(lonNum))}`
779
+ : '';
780
+ await sendEmail('calendar-onekite_paid', requesterUid, 'Location matériel - Paiement reçu', {
781
+ uid: requesterUid,
782
+ username: requester && requester.username ? requester.username : '',
783
+ itemName: arrayifyNames(r).join(', '),
784
+ itemNames: arrayifyNames(r),
785
+ dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
786
+ paymentReceiptUrl: r.paymentReceiptUrl || '',
787
+ pickupTime: r.pickupTime || '',
788
+ pickupAddress: r.pickupAddress || '',
789
+ mapUrl,
790
+ ...(buildCalendarLinks({
791
+ type: 'reservation', id: String(r.rid || ''), uid: requesterUid,
792
+ itemNames: arrayifyNames(r),
793
+ pickupAddress: r.pickupAddress || '', allDay: true,
794
+ startYmd: (r.startDate && /^\d{4}-\d{2}-\d{2}$/.test(String(r.startDate))) ? String(r.startDate) : new Date(parseInt(r.start, 10)).toISOString().slice(0, 10),
795
+ endYmd: (r.endDate && /^\d{4}-\d{2}-\d{2}$/.test(String(r.endDate))) ? String(r.endDate) : new Date(parseInt(r.end, 10)).toISOString().slice(0, 10),
796
+ })),
797
+ });
798
+ }
799
+ } catch (e) {}
800
+
801
+ try {
802
+ const requester = await user.getUserFields(parseInt(r.uid, 10), ['username']);
803
+ await discord.notifyPaymentReceived(settings, {
804
+ rid: r.rid, uid: r.uid,
805
+ username: (requester && requester.username) ? requester.username : (r.username || ''),
806
+ itemIds: arrayifyIds(r),
807
+ itemNames: arrayifyNames(r),
808
+ start: r.start, end: r.end, status: r.status,
809
+ });
810
+ } catch (e) {}
811
+
812
+ return res.json({ ok: true, synced: true, prevStatus, paymentId: r.paymentId || '' });
813
+ };
814
+
687
815
  admin.purgeAccounting = async function (req, res) {
688
816
  const qFrom = String((req.query && req.query.from) || '').trim();
689
817
  const qTo = String((req.query && req.query.to) || '').trim();
package/lib/api.js CHANGED
@@ -28,6 +28,8 @@ const {
28
28
  signCalendarLink,
29
29
  ymdToCompact,
30
30
  getSetting,
31
+ arrayifyIds,
32
+ arrayifyNames,
31
33
  } = shared;
32
34
 
33
35
  const helloasso = require('./helloasso');
@@ -220,8 +222,8 @@ function eventsFor(resv) {
220
222
  ? String(resv.endDate)
221
223
  : new Date(parseInt(resv.end, 10)).toISOString().slice(0, 10);
222
224
 
223
- const itemIds = Array.isArray(resv.itemIds) ? resv.itemIds : (resv.itemId ? [resv.itemId] : []);
224
- const itemNames = Array.isArray(resv.itemNames) ? resv.itemNames : (resv.itemName ? [resv.itemName] : []);
225
+ const itemIds = arrayifyIds(resv);
226
+ const itemNames = arrayifyNames(resv);
225
227
 
226
228
  // Calendar export links (only really useful to the reservation creator, but
227
229
  // harmless to include for everyone; access is protected by signature).
@@ -592,8 +594,8 @@ api.getReservationDetails = async function (req, res) {
592
594
  status: r.status,
593
595
  uid: r.uid,
594
596
  username: r.username || '',
595
- itemNames: Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : []),
596
- itemIds: Array.isArray(r.itemIds) ? r.itemIds : (r.itemId ? [r.itemId] : []),
597
+ itemNames: arrayifyNames(r),
598
+ itemIds: arrayifyIds(r),
597
599
  start: r.start,
598
600
  end: r.end,
599
601
  approvedByUsername: r.approvedByUsername || '',
@@ -1326,7 +1328,7 @@ api.createReservation = async function (req, res) {
1326
1328
  const exEnd = parseInt(existing.end, 10);
1327
1329
  if (!(exStart < end && start < exEnd)) continue;
1328
1330
  }
1329
- const exItemIds = Array.isArray(existing.itemIds) ? existing.itemIds : (existing.itemId ? [existing.itemId] : []);
1331
+ const exItemIds = arrayifyIds(existing);
1330
1332
  const conflictingItemIds = exItemIds.filter(x => itemIds.includes(String(x)));
1331
1333
  if (conflictingItemIds.length) {
1332
1334
  conflicts.push({ rid: existing.rid, itemIds: conflictingItemIds, status: existing.status });
@@ -1678,8 +1680,8 @@ api.approveReservation = async function (req, res) {
1678
1680
  targetId: String(rid),
1679
1681
  reservationUid: Number(r.uid) || 0,
1680
1682
  reservationUsername: String(r.username || ''),
1681
- itemIds: r.itemIds || (r.itemId ? [r.itemId] : []),
1682
- itemNames: r.itemNames || (r.itemName ? [r.itemName] : []),
1683
+ itemIds: arrayifyIds(r),
1684
+ itemNames: arrayifyNames(r),
1683
1685
  startDate: r.startDate || '',
1684
1686
  endDate: r.endDate || '',
1685
1687
  status: r.status,
@@ -1697,8 +1699,8 @@ api.approveReservation = async function (req, res) {
1697
1699
  await sendEmail('calendar-onekite_approved', requesterUid, 'Location matériel - Réservation validée', {
1698
1700
  uid: requesterUid,
1699
1701
  username: requester && requester.username ? requester.username : '',
1700
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
1701
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
1702
+ itemName: arrayifyNames(r).join(', '),
1703
+ itemNames: arrayifyNames(r),
1702
1704
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
1703
1705
  start: formatFR(r.start),
1704
1706
  end: formatFR(r.end),
@@ -1722,7 +1724,7 @@ api.approveReservation = async function (req, res) {
1722
1724
  endYmd: (r.endDate && /^\d{4}-\d{2}-\d{2}$/.test(String(r.endDate))) ? String(r.endDate) : new Date(parseInt(r.end, 10)).toISOString().slice(0, 10),
1723
1725
  }),
1724
1726
  validatedBy: r.approvedByUsername || '',
1725
- validatedByUrl: (r.approvedByUsername ? `https://www.onekite.com/user/${encodeURIComponent(String(r.approvedByUsername))}` : ''),
1727
+ validatedByUrl: (r.approvedByUsername ? `${forumBaseUrl()}/user/${encodeURIComponent(String(r.approvedByUsername))}` : ''),
1726
1728
  });
1727
1729
  }
1728
1730
  return res.json({ ok: true });
@@ -1759,8 +1761,8 @@ api.refuseReservation = async function (req, res) {
1759
1761
  targetId: String(rid),
1760
1762
  reservationUid: Number(r.uid) || 0,
1761
1763
  reservationUsername: String(r.username || ''),
1762
- itemIds: r.itemIds || (r.itemId ? [r.itemId] : []),
1763
- itemNames: r.itemNames || (r.itemName ? [r.itemName] : []),
1764
+ itemIds: arrayifyIds(r),
1765
+ itemNames: arrayifyNames(r),
1764
1766
  startDate: r.startDate || '',
1765
1767
  endDate: r.endDate || '',
1766
1768
  reason: r.refusedReason || '',
@@ -1773,8 +1775,8 @@ api.refuseReservation = async function (req, res) {
1773
1775
  await sendEmail('calendar-onekite_refused', requesterUid2, 'Location matériel - Demande de réservation', {
1774
1776
  uid: requesterUid2,
1775
1777
  username: requester && requester.username ? requester.username : '',
1776
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
1777
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
1778
+ itemName: arrayifyNames(r).join(', '),
1779
+ itemNames: arrayifyNames(r),
1778
1780
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
1779
1781
  start: formatFR(r.start),
1780
1782
  end: formatFR(r.end),
@@ -1831,8 +1833,8 @@ api.cancelReservation = async function (req, res) {
1831
1833
  targetId: String(rid),
1832
1834
  reservationUid: Number(r.uid) || 0,
1833
1835
  reservationUsername: String(r.username || ''),
1834
- itemIds: r.itemIds || (r.itemId ? [r.itemId] : []),
1835
- itemNames: r.itemNames || (r.itemName ? [r.itemName] : []),
1836
+ itemIds: arrayifyIds(r),
1837
+ itemNames: arrayifyNames(r),
1836
1838
  startDate: r.startDate || '',
1837
1839
  endDate: r.endDate || '',
1838
1840
  status: r.status,
@@ -1844,8 +1846,8 @@ api.cancelReservation = async function (req, res) {
1844
1846
  rid: r.rid,
1845
1847
  uid: r.uid,
1846
1848
  username: r.username || '',
1847
- itemIds: r.itemIds || [],
1848
- itemNames: r.itemNames || [],
1849
+ itemIds: arrayifyIds(r),
1850
+ itemNames: arrayifyNames(r),
1849
1851
  start: r.start,
1850
1852
  end: r.end,
1851
1853
  status: r.status,
@@ -1865,8 +1867,8 @@ api.cancelReservation = async function (req, res) {
1865
1867
  await sendEmail('calendar-onekite_cancelled', requesterUid, 'Location matériel - Réservation annulée', {
1866
1868
  uid: requesterUid,
1867
1869
  username: requester && requester.username ? requester.username : '',
1868
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
1869
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
1870
+ itemName: arrayifyNames(r).join(', '),
1871
+ itemNames: arrayifyNames(r),
1870
1872
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
1871
1873
  cancelledBy: (r.cancelledByUsername || (canceller && canceller.username) || ''),
1872
1874
  cancelledByUrl: ((r.cancelledByUsername || (canceller && canceller.username)) ? `${forumBaseUrl()}/user/${encodeURIComponent(String(r.cancelledByUsername || canceller.username))}` : ''),
@@ -2065,7 +2067,7 @@ api.getIcs = async function (req, res) {
2065
2067
  ? String(r.endDate)
2066
2068
  : new Date(parseInt(r.end, 10)).toISOString().slice(0, 10);
2067
2069
 
2068
- const itemNames = Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : []);
2070
+ const itemNames = arrayifyNames(r);
2069
2071
  const summary = itemNames.length ? `Location - ${itemNames.join(', ')}` : 'Location matériel';
2070
2072
  const description = [
2071
2073
  itemNames.length ? `Matériel: ${itemNames.join(', ')}` : '',
package/lib/checkout.js CHANGED
@@ -23,6 +23,8 @@ const {
23
23
  normalizeCallbackUrl,
24
24
  normalizeReturnUrl,
25
25
  formatFR,
26
+ arrayifyIds,
27
+ arrayifyNames,
26
28
  } = require('./shared');
27
29
 
28
30
  /**
@@ -56,8 +58,8 @@ async function buildCheckoutIntent({ r, settings }) {
56
58
 
57
59
  const year = yearFromTs(Number(r.start));
58
60
  const formSlug = autoFormSlugForYear(year);
59
- const itemIds = (Array.isArray(r.itemIds) ? r.itemIds : (r.itemId ? [r.itemId] : [])).map(String);
60
- const itemNames = Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : []);
61
+ const itemIds = arrayifyIds(r).map(String);
62
+ const itemNames = arrayifyNames(r);
61
63
  const days = calendarDaysExclusiveYmd(r.startDate, r.endDate) || 1;
62
64
 
63
65
  // Recompute total from HelloAsso catalog to always use up-to-date prices.
@@ -9,7 +9,7 @@ const helloasso = require('./helloasso');
9
9
  const discord = require('./discord');
10
10
  const realtime = require('./realtime');
11
11
  const shared = require('./shared');
12
- const { formatFR, sendEmail, buildCalendarLinks } = shared;
12
+ const { formatFR, sendEmail, buildCalendarLinks, arrayifyIds, arrayifyNames } = shared;
13
13
 
14
14
  const SETTINGS_KEY = 'calendar-onekite';
15
15
  const PROCESSED_KEY = 'calendar-onekite:helloasso:processedPayments';
@@ -25,7 +25,7 @@ function buildReservationCalendarLinks(r) {
25
25
  const endYmd = (r.endDate && /^\d{4}-\d{2}-\d{2}$/.test(String(r.endDate)))
26
26
  ? String(r.endDate)
27
27
  : new Date(parseInt(r.end, 10)).toISOString().slice(0, 10);
28
- const itemNames = Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : []);
28
+ const itemNames = arrayifyNames(r);
29
29
  return buildCalendarLinks({
30
30
  type: 'reservation',
31
31
  id: String(r.rid || ''),
@@ -117,10 +117,12 @@ function isConfirmedPayment(payload) {
117
117
  const okState = ['confirmed', 'authorized', 'paid', 'processed', 'succeeded', 'success'].includes(state);
118
118
 
119
119
  if (eventType === 'payment') return okState;
120
- if (eventType === 'order') {
121
- // Order payloads sometimes omit the state field; accept when missing.
120
+ if (eventType === 'order' || eventType === 'checkout') {
121
+ // Order/Checkout payloads sometimes omit the state field; accept when missing.
122
122
  return !state || okState;
123
123
  }
124
+ // For any other event type, accept if state is clearly positive.
125
+ if (okState) return true;
124
126
  return false;
125
127
  } catch (e) {
126
128
  return false;
@@ -315,8 +317,8 @@ async function handler(req, res, next) {
315
317
  targetId: String(r.rid),
316
318
  reservationUid: Number(r.uid) || 0,
317
319
  reservationUsername: String(r.username || ''),
318
- itemIds: r.itemIds || (r.itemId ? [r.itemId] : []),
319
- itemNames: r.itemNames || (r.itemName ? [r.itemName] : []),
320
+ itemIds: arrayifyIds(r),
321
+ itemNames: arrayifyNames(r),
320
322
  startDate: r.startDate || '',
321
323
  endDate: r.endDate || '',
322
324
  paymentId: r.paymentId,
@@ -336,8 +338,8 @@ async function handler(req, res, next) {
336
338
  await sendEmail('calendar-onekite_paid', requesterUid, 'Location matériel - Paiement reçu', {
337
339
  uid: requesterUid,
338
340
  username: requester && requester.username ? requester.username : '',
339
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
340
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
341
+ itemName: arrayifyNames(r).join(', '),
342
+ itemNames: arrayifyNames(r),
341
343
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
342
344
  paymentReceiptUrl: r.paymentReceiptUrl || '',
343
345
  pickupTime: r.pickupTime || '',
@@ -352,8 +354,8 @@ async function handler(req, res, next) {
352
354
  rid: r.rid,
353
355
  uid: r.uid,
354
356
  username: (requester && requester.username) ? requester.username : (r.username || ''),
355
- itemIds: r.itemIds || (r.itemId ? [r.itemId] : []),
356
- itemNames: r.itemNames || (r.itemName ? [r.itemName] : []),
357
+ itemIds: arrayifyIds(r),
358
+ itemNames: arrayifyNames(r),
357
359
  start: r.start,
358
360
  end: r.end,
359
361
  status: r.status,
package/lib/shared.js CHANGED
@@ -374,6 +374,13 @@ function arrayifyNames(obj) {
374
374
  ).filter(Boolean);
375
375
  }
376
376
 
377
+ function arrayifyIds(obj) {
378
+ return (Array.isArray(obj && obj.itemIds)
379
+ ? obj.itemIds
380
+ : (obj && obj.itemId ? [obj.itemId] : [])
381
+ ).filter(Boolean);
382
+ }
383
+
377
384
  function getSetting(settings, key, fallback) {
378
385
  const v = settings && Object.prototype.hasOwnProperty.call(settings, key) ? settings[key] : undefined;
379
386
  if (v == null || v === '') return fallback;
@@ -392,7 +399,6 @@ module.exports = {
392
399
  autoCreatorGroupForYear,
393
400
 
394
401
  // HMAC / signing
395
- hmacSecret,
396
402
  signCalendarLink,
397
403
 
398
404
  // Dates
@@ -424,5 +430,6 @@ module.exports = {
424
430
 
425
431
  // Misc
426
432
  arrayifyNames,
433
+ arrayifyIds,
427
434
  getSetting,
428
435
  };
package/library.js CHANGED
@@ -113,6 +113,7 @@ Plugin.init = async function (params) {
113
113
  router.put(`${adminBase}/reservations/:rid/approve`, ...adminMws, admin.approveReservation);
114
114
  router.put(`${adminBase}/reservations/:rid/refuse`, ...adminMws, admin.refuseReservation);
115
115
  router.put(`${adminBase}/reservations/:rid/mark-paid`, ...adminMws, admin.markReservationPaid);
116
+ router.post(`${adminBase}/reservations/:rid/sync-payment`, ...adminMws, admin.syncPaymentFromHelloAsso);
116
117
 
117
118
  router.post(`${adminBase}/purge`, ...adminMws, admin.purgeByYear);
118
119
  router.get(`${adminBase}/debug`, ...adminMws, admin.debugHelloAsso);
@@ -126,24 +127,16 @@ Plugin.init = async function (params) {
126
127
  // Purge outings by year
127
128
  router.post(`${adminBase}/outings/purge`, ...adminMws, admin.purgeOutingsByYear);
128
129
 
129
- // HelloAsso callback endpoint (hardened)
130
- // - Only accepts POST
130
+ // HelloAsso webhook endpoint (hardened)
131
131
  // - Verifies x-ha-signature (HMAC SHA-256) using the configured client secret
132
- // - Basic replay protection
133
- // NOTE: we capture the raw body for signature verification.
132
+ // - Basic replay protection; raw body captured for signature verification
134
133
  const helloassoJson = bodyParser.json({
135
134
  verify: (req, _res, buf) => {
136
135
  req.rawBody = buf;
137
136
  },
138
137
  type: ['application/json', 'application/*+json'],
139
138
  });
140
- // Accept webhook on both legacy root path and namespaced plugin path.
141
- // Some reverse proxies block unknown root paths, so /plugins/... is recommended.
142
- router.post('/helloasso', helloassoJson, helloassoWebhook.handler);
143
139
  router.post('/plugins/calendar-onekite/helloasso', helloassoJson, helloassoWebhook.handler);
144
-
145
- // Optional: health checks
146
- router.get('/helloasso', (req, res) => res.json({ ok: true }));
147
140
  router.get('/plugins/calendar-onekite/helloasso', (req, res) => res.json({ ok: true }));
148
141
 
149
142
  scheduler.start();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.92",
3
+ "version": "2.0.94",
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/plugin.json CHANGED
@@ -39,5 +39,5 @@
39
39
  "acpScripts": [
40
40
  "public/admin.js"
41
41
  ],
42
- "version": "2.0.70"
42
+ "version": "2.0.93"
43
43
  }
@@ -1,16 +0,0 @@
1
- 'use strict';
2
-
3
- // Thin re-export layer for backward compatibility.
4
- // All helpers now live in ./shared.js.
5
- const shared = require('./shared');
6
-
7
- module.exports = {
8
- normalizeAllowedGroups: shared.normalizeAllowedGroups,
9
- normalizeUids: shared.normalizeUids,
10
- getMembersByGroupIdentifier: shared.getMembersByGroupIdentifier,
11
- userInAnyGroup: shared.userInAnyGroup,
12
- forumBaseUrl: shared.forumBaseUrl,
13
- sendEmail: shared.sendEmail,
14
- normalizeUidList: shared.normalizeUidList,
15
- usernamesByUids: shared.usernamesByUids,
16
- };
package/lib/utils.js DELETED
@@ -1,15 +0,0 @@
1
- 'use strict';
2
-
3
- // Thin re-export layer for backward compatibility.
4
- // All helpers now live in ./shared.js.
5
- const shared = require('./shared');
6
-
7
- module.exports = {
8
- getSetting: shared.getSetting,
9
- formatFR: shared.formatFR,
10
- formatFRShort: shared.formatFRShort,
11
- forumBaseUrl: shared.forumBaseUrl,
12
- normalizeAllowedGroups: shared.normalizeAllowedGroups,
13
- normalizeUids: shared.normalizeUids,
14
- arrayifyNames: shared.arrayifyNames,
15
- };