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 +137 -9
- package/lib/api.js +23 -21
- package/lib/checkout.js +4 -2
- package/lib/helloassoWebhook.js +12 -10
- package/lib/shared.js +8 -1
- package/library.js +3 -10
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/lib/nodebb-helpers.js +0 -16
- package/lib/utils.js +0 -15
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: (
|
|
155
|
-
itemNames: (
|
|
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: (
|
|
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 ?
|
|
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: (
|
|
222
|
-
itemNames: (
|
|
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:
|
|
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: (
|
|
276
|
-
itemNames: (
|
|
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 =
|
|
224
|
-
const itemNames =
|
|
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:
|
|
596
|
-
itemIds:
|
|
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 =
|
|
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:
|
|
1682
|
-
itemNames:
|
|
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: (
|
|
1701
|
-
itemNames: (
|
|
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 ?
|
|
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:
|
|
1763
|
-
itemNames:
|
|
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: (
|
|
1777
|
-
itemNames: (
|
|
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:
|
|
1835
|
-
itemNames:
|
|
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
|
|
1848
|
-
itemNames: r
|
|
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: (
|
|
1869
|
-
itemNames: (
|
|
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 =
|
|
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 = (
|
|
60
|
-
const itemNames =
|
|
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.
|
package/lib/helloassoWebhook.js
CHANGED
|
@@ -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 =
|
|
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:
|
|
319
|
-
itemNames:
|
|
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: (
|
|
340
|
-
itemNames: (
|
|
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:
|
|
356
|
-
itemNames:
|
|
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
|
|
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
package/plugin.json
CHANGED
package/lib/nodebb-helpers.js
DELETED
|
@@ -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
|
-
};
|