nodebb-plugin-onekite-calendar 2.0.91 → 2.0.93

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 || ''),
@@ -76,7 +76,11 @@ function getReservationIdFromPayload(payload) {
76
76
  data.order && (data.order.meta || data.order.metadata),
77
77
  ].filter(Boolean);
78
78
 
79
- for (const metaObj of metaCandidates) {
79
+ for (let metaObj of metaCandidates) {
80
+ // HelloAsso sometimes sends metadata as a JSON-encoded string — parse it first.
81
+ if (typeof metaObj === 'string') {
82
+ try { metaObj = JSON.parse(metaObj); } catch (e) { continue; }
83
+ }
80
84
  if (typeof metaObj === 'object' && !Array.isArray(metaObj) && metaObj.reservationId) {
81
85
  return String(metaObj.reservationId);
82
86
  }
@@ -113,10 +117,12 @@ function isConfirmedPayment(payload) {
113
117
  const okState = ['confirmed', 'authorized', 'paid', 'processed', 'succeeded', 'success'].includes(state);
114
118
 
115
119
  if (eventType === 'payment') return okState;
116
- if (eventType === 'order') {
117
- // 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.
118
122
  return !state || okState;
119
123
  }
124
+ // For any other event type, accept if state is clearly positive.
125
+ if (okState) return true;
120
126
  return false;
121
127
  } catch (e) {
122
128
  return false;
@@ -215,9 +221,35 @@ async function handler(req, res, next) {
215
221
  }
216
222
  }
217
223
 
218
- const payload = req.body;
224
+ // Parse body req.body is set by our bodyParser middleware, but if NodeBB's
225
+ // global body parser consumed the stream first, fall back to req.rawBody.
226
+ let payload = req.body;
227
+ if (!payload && req.rawBody) {
228
+ try {
229
+ payload = JSON.parse(req.rawBody.toString('utf8'));
230
+ } catch (e) {
231
+ // eslint-disable-next-line no-console
232
+ console.warn('[calendar-onekite] HelloAsso webhook: failed to parse rawBody', { err: e && e.message });
233
+ }
234
+ }
235
+
236
+ // eslint-disable-next-line no-console
237
+ console.info('[calendar-onekite] HelloAsso webhook received', {
238
+ eventType: payload && payload.eventType,
239
+ state: payload && payload.data && (payload.data.state || payload.data.status || payload.data.paymentState),
240
+ paymentId: payload && payload.data && (payload.data.id || payload.data.paymentId),
241
+ checkoutIntentId: payload && payload.data && (payload.data.checkoutIntentId || (payload.data.order && payload.data.order.checkoutIntentId)),
242
+ hasBody: !!payload,
243
+ contentType: req.headers && req.headers['content-type'],
244
+ });
219
245
 
220
246
  if (!isConfirmedPayment(payload)) {
247
+ // eslint-disable-next-line no-console
248
+ console.warn('[calendar-onekite] HelloAsso webhook: payment not confirmed — ignored', {
249
+ eventType: payload && payload.eventType,
250
+ state: payload && payload.data && (payload.data.state || payload.data.status || payload.data.paymentState),
251
+ hasData: !!(payload && payload.data),
252
+ });
221
253
  return res.json({ ok: true, ignored: true });
222
254
  }
223
255
 
@@ -285,8 +317,8 @@ async function handler(req, res, next) {
285
317
  targetId: String(r.rid),
286
318
  reservationUid: Number(r.uid) || 0,
287
319
  reservationUsername: String(r.username || ''),
288
- itemIds: r.itemIds || (r.itemId ? [r.itemId] : []),
289
- itemNames: r.itemNames || (r.itemName ? [r.itemName] : []),
320
+ itemIds: arrayifyIds(r),
321
+ itemNames: arrayifyNames(r),
290
322
  startDate: r.startDate || '',
291
323
  endDate: r.endDate || '',
292
324
  paymentId: r.paymentId,
@@ -306,8 +338,8 @@ async function handler(req, res, next) {
306
338
  await sendEmail('calendar-onekite_paid', requesterUid, 'Location matériel - Paiement reçu', {
307
339
  uid: requesterUid,
308
340
  username: requester && requester.username ? requester.username : '',
309
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
310
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
341
+ itemName: arrayifyNames(r).join(', '),
342
+ itemNames: arrayifyNames(r),
311
343
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
312
344
  paymentReceiptUrl: r.paymentReceiptUrl || '',
313
345
  pickupTime: r.pickupTime || '',
@@ -322,8 +354,8 @@ async function handler(req, res, next) {
322
354
  rid: r.rid,
323
355
  uid: r.uid,
324
356
  username: (requester && requester.username) ? requester.username : (r.username || ''),
325
- itemIds: r.itemIds || (r.itemId ? [r.itemId] : []),
326
- itemNames: r.itemNames || (r.itemName ? [r.itemName] : []),
357
+ itemIds: arrayifyIds(r),
358
+ itemNames: arrayifyNames(r),
327
359
  start: r.start,
328
360
  end: r.end,
329
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;
@@ -424,5 +431,6 @@ module.exports = {
424
431
 
425
432
  // Misc
426
433
  arrayifyNames,
434
+ arrayifyIds,
427
435
  getSetting,
428
436
  };
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.91",
3
+ "version": "2.0.93",
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
- };