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 +137 -9
- package/lib/api.js +23 -21
- package/lib/checkout.js +4 -2
- package/lib/helloassoWebhook.js +44 -12
- package/lib/shared.js +8 -0
- package/library.js +1 -0
- 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 || ''),
|
|
@@ -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 (
|
|
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
|
-
|
|
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:
|
|
289
|
-
itemNames:
|
|
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: (
|
|
310
|
-
itemNames: (
|
|
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:
|
|
326
|
-
itemNames:
|
|
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
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
|
-
};
|