nodebb-plugin-onekite-calendar 2.0.88 → 2.0.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/admin.js +2 -0
- package/lib/api.js +106 -0
- package/lib/scheduler.js +1 -0
- package/library.js +1 -0
- package/package.json +1 -1
- package/public/client.js +16 -3
- package/templates/emails/calendar-onekite_approved.tpl +10 -5
- package/templates/emails/calendar-onekite_reminder.tpl +10 -5
package/lib/admin.js
CHANGED
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
calendarDaysExclusiveYmd,
|
|
18
18
|
yearFromTs,
|
|
19
19
|
autoFormSlugForYear,
|
|
20
|
+
forumBaseUrl,
|
|
20
21
|
} = shared;
|
|
21
22
|
|
|
22
23
|
const dbLayer = require('./db');
|
|
@@ -213,6 +214,7 @@ admin.approveReservation = async function (req, res) {
|
|
|
213
214
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
214
215
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
215
216
|
paymentUrl: paymentUrl || '',
|
|
217
|
+
calendarUrl: `${forumBaseUrl()}/calendar`,
|
|
216
218
|
pickupAddress: r.pickupAddress || '',
|
|
217
219
|
notes: r.notes || '',
|
|
218
220
|
pickupTime: r.pickupTime || '',
|
package/lib/api.js
CHANGED
|
@@ -621,6 +621,110 @@ api.getReservationDetails = async function (req, res) {
|
|
|
621
621
|
return res.json(out);
|
|
622
622
|
};
|
|
623
623
|
|
|
624
|
+
/**
|
|
625
|
+
* Return a fresh HelloAsso payment URL for a reservation in awaiting_payment status.
|
|
626
|
+
* HelloAsso checkout intents expire after ~30 minutes, so stored URLs become stale.
|
|
627
|
+
* This endpoint regenerates a new intent on demand and persists the fresh URL.
|
|
628
|
+
*
|
|
629
|
+
* @route GET /api/v3/plugins/calendar-onekite/reservations/:rid/payment-url
|
|
630
|
+
*/
|
|
631
|
+
api.getFreshPaymentUrl = async function (req, res) {
|
|
632
|
+
const uid = req.uid;
|
|
633
|
+
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
634
|
+
|
|
635
|
+
const settings = await getSettings();
|
|
636
|
+
const rid = String(req.params.rid || '').trim();
|
|
637
|
+
if (!rid) return res.status(400).json({ error: 'missing-rid' });
|
|
638
|
+
|
|
639
|
+
const r = await dbLayer.getReservation(rid);
|
|
640
|
+
if (!r) return res.status(404).json({ error: 'not-found' });
|
|
641
|
+
|
|
642
|
+
const isOwner = String(r.uid) === String(uid);
|
|
643
|
+
const isMod = await canValidate(uid, settings);
|
|
644
|
+
if (!isOwner && !isMod) return res.status(403).json({ error: 'not-allowed' });
|
|
645
|
+
|
|
646
|
+
if (r.status !== 'awaiting_payment') {
|
|
647
|
+
return res.status(409).json({ error: 'wrong-status', status: r.status });
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Build a fresh checkout intent
|
|
651
|
+
const env = settings.helloassoEnv || 'prod';
|
|
652
|
+
const token = await helloasso.getAccessToken({
|
|
653
|
+
env,
|
|
654
|
+
clientId: settings.helloassoClientId,
|
|
655
|
+
clientSecret: settings.helloassoClientSecret,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
if (!token) {
|
|
659
|
+
return res.status(503).json({ error: 'helloasso-unavailable' });
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const requester = await user.getUserFields(r.uid, ['email']);
|
|
663
|
+
const year = yearFromTs(Number(r.start));
|
|
664
|
+
const formSlug = autoFormSlugForYear(year);
|
|
665
|
+
|
|
666
|
+
// Recompute total from HelloAsso catalog to always use up-to-date prices.
|
|
667
|
+
let recomputedTotalCents = null;
|
|
668
|
+
try {
|
|
669
|
+
const { items: catalog } = await helloasso.listCatalogItems({
|
|
670
|
+
env,
|
|
671
|
+
token,
|
|
672
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
673
|
+
formType: settings.helloassoFormType,
|
|
674
|
+
formSlug,
|
|
675
|
+
});
|
|
676
|
+
const byId = new Map((catalog || []).map((it) => [String(it.id), (typeof it.price === 'number' ? it.price : 0)]));
|
|
677
|
+
const ids = (Array.isArray(r.itemIds) ? r.itemIds : (r.itemId ? [r.itemId] : [])).map(String);
|
|
678
|
+
const days = calendarDaysExclusiveYmd(r.startDate, r.endDate) || 1;
|
|
679
|
+
const sumCentsPerDay = ids.reduce((acc, id) => acc + (byId.get(id) || 0), 0);
|
|
680
|
+
if (sumCentsPerDay > 0) {
|
|
681
|
+
recomputedTotalCents = Math.max(0, Math.round(sumCentsPerDay * days));
|
|
682
|
+
}
|
|
683
|
+
} catch (e) { /* fallback to stored total */ }
|
|
684
|
+
|
|
685
|
+
const totalAmount = (typeof recomputedTotalCents === 'number')
|
|
686
|
+
? recomputedTotalCents
|
|
687
|
+
: Math.max(0, Math.round((Number(r.total) || 0) * 100));
|
|
688
|
+
|
|
689
|
+
if (!totalAmount) {
|
|
690
|
+
return res.status(422).json({ error: 'zero-amount' });
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const intent = await helloasso.createCheckoutIntent({
|
|
694
|
+
env,
|
|
695
|
+
token,
|
|
696
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
697
|
+
formType: settings.helloassoFormType,
|
|
698
|
+
formSlug,
|
|
699
|
+
totalAmount,
|
|
700
|
+
payerEmail: requester && requester.email ? requester.email : '',
|
|
701
|
+
callbackUrl: normalizeReturnUrl(),
|
|
702
|
+
webhookUrl: normalizeCallbackUrl(settings.helloassoCallbackUrl),
|
|
703
|
+
itemName: buildHelloAssoItemName('Réservation matériel Onekite', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
|
|
704
|
+
containsDonation: false,
|
|
705
|
+
metadata: {
|
|
706
|
+
reservationId: String(rid),
|
|
707
|
+
items: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
|
|
708
|
+
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const paymentUrl = intent && (intent.paymentUrl || intent.redirectUrl)
|
|
713
|
+
? (intent.paymentUrl || intent.redirectUrl)
|
|
714
|
+
: (typeof intent === 'string' ? intent : null);
|
|
715
|
+
|
|
716
|
+
if (!paymentUrl) {
|
|
717
|
+
return res.status(502).json({ error: 'intent-creation-failed' });
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Persist the fresh URL so subsequent requests can also renew from DB.
|
|
721
|
+
r.paymentUrl = paymentUrl;
|
|
722
|
+
if (intent && intent.checkoutIntentId) r.checkoutIntentId = intent.checkoutIntentId;
|
|
723
|
+
await dbLayer.saveReservation(r);
|
|
724
|
+
|
|
725
|
+
return res.json({ ok: true, paymentUrl });
|
|
726
|
+
};
|
|
727
|
+
|
|
624
728
|
/**
|
|
625
729
|
* Get detailed information about a special event.
|
|
626
730
|
*
|
|
@@ -1549,6 +1653,7 @@ api.createReservation = async function (req, res) {
|
|
|
1549
1653
|
pickupLon: '',
|
|
1550
1654
|
mapUrl: '',
|
|
1551
1655
|
paymentUrl: resv.paymentUrl || '',
|
|
1656
|
+
calendarUrl: `${forumBaseUrl()}/calendar`,
|
|
1552
1657
|
validatedBy: creatorUsername || '',
|
|
1553
1658
|
validatedByUrl: creatorUsername ? `${forumBaseUrl()}/user/${encodeURIComponent(String(creatorUsername))}` : '',
|
|
1554
1659
|
...buildCalendarLinks({
|
|
@@ -1785,6 +1890,7 @@ api.approveReservation = async function (req, res) {
|
|
|
1785
1890
|
pickupLon: r.pickupLon || '',
|
|
1786
1891
|
mapUrl,
|
|
1787
1892
|
paymentUrl: r.paymentUrl || '',
|
|
1893
|
+
calendarUrl: `${forumBaseUrl()}/calendar`,
|
|
1788
1894
|
...buildCalendarLinks({
|
|
1789
1895
|
type: 'reservation',
|
|
1790
1896
|
id: String(r.rid),
|
package/lib/scheduler.js
CHANGED
|
@@ -253,6 +253,7 @@ async function processAwaitingPayment(preIds, preReservations) {
|
|
|
253
253
|
itemNames: arrayifyNames(r),
|
|
254
254
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
255
255
|
paymentUrl: r.paymentUrl || '',
|
|
256
|
+
calendarUrl: `${forumBaseUrl()}/calendar`,
|
|
256
257
|
delayMinutes: holdMins,
|
|
257
258
|
pickupLine: r.pickupTime ? (r.adminNote ? `${r.pickupTime} à ${r.adminNote}` : r.pickupTime) : '',
|
|
258
259
|
});
|
package/library.js
CHANGED
|
@@ -76,6 +76,7 @@ Plugin.init = async function (params) {
|
|
|
76
76
|
|
|
77
77
|
router.post('/api/v3/plugins/calendar-onekite/reservations', ...publicExpose, api.createReservation);
|
|
78
78
|
router.get('/api/v3/plugins/calendar-onekite/reservations/:rid', ...publicExpose, api.getReservationDetails);
|
|
79
|
+
router.get('/api/v3/plugins/calendar-onekite/reservations/:rid/payment-url', ...publicExpose, api.getFreshPaymentUrl);
|
|
79
80
|
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/approve', ...publicExpose, api.approveReservation);
|
|
80
81
|
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/refuse', ...publicExpose, api.refuseReservation);
|
|
81
82
|
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/cancel', ...publicExpose, api.cancelReservation);
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -2403,9 +2403,22 @@ function toDatetimeLocalValue(date) {
|
|
|
2403
2403
|
label: 'Payer maintenant',
|
|
2404
2404
|
className: 'btn-primary',
|
|
2405
2405
|
callback: () => {
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2406
|
+
(async () => {
|
|
2407
|
+
try {
|
|
2408
|
+
// Always fetch a fresh checkout intent — HelloAsso URLs expire after ~30 min.
|
|
2409
|
+
const resp = await fetchJson(
|
|
2410
|
+
`/api/v3/plugins/calendar-onekite/reservations/${encodeURIComponent(String(rid))}/payment-url`
|
|
2411
|
+
);
|
|
2412
|
+
const freshUrl = resp && resp.paymentUrl ? String(resp.paymentUrl) : '';
|
|
2413
|
+
if (freshUrl && /^https?:\/\//i.test(freshUrl)) {
|
|
2414
|
+
window.open(freshUrl, '_blank', 'noopener');
|
|
2415
|
+
} else {
|
|
2416
|
+
showAlert('error', 'Impossible de générer le lien de paiement. Veuillez réessayer.');
|
|
2417
|
+
}
|
|
2418
|
+
} catch (e) {
|
|
2419
|
+
showAlert('error', 'Erreur lors de la génération du lien de paiement.');
|
|
2420
|
+
}
|
|
2421
|
+
})();
|
|
2409
2422
|
return false;
|
|
2410
2423
|
},
|
|
2411
2424
|
};
|
|
@@ -40,11 +40,16 @@
|
|
|
40
40
|
<p><strong>Notes :</strong><br>{notes}</p>
|
|
41
41
|
<!-- ENDIF notes -->
|
|
42
42
|
|
|
43
|
-
<!-- IF
|
|
43
|
+
<!-- IF calendarUrl -->
|
|
44
44
|
<p>
|
|
45
|
-
<a href="{
|
|
46
|
-
|
|
45
|
+
<a href="{calendarUrl}" style="display:inline-block;padding:12px 18px;border-radius:6px;text-decoration:none;border:1px solid #333;">
|
|
46
|
+
Accéder au calendrier pour payer
|
|
47
47
|
</a>
|
|
48
48
|
</p>
|
|
49
|
-
<p style="font-size: 12px; color: #666;">
|
|
50
|
-
|
|
49
|
+
<p style="font-size: 12px; color: #666;">
|
|
50
|
+
Sur le calendrier, cliquez sur votre réservation puis sur le bouton <strong>« Payer maintenant »</strong>.
|
|
51
|
+
<!-- IF paymentUrl -->
|
|
52
|
+
<br>Vous pouvez aussi payer directement via ce lien (valable quelques heures) : <a href="{paymentUrl}">{paymentUrl}</a>
|
|
53
|
+
<!-- ENDIF paymentUrl -->
|
|
54
|
+
</p>
|
|
55
|
+
<!-- ENDIF calendarUrl -->
|
|
@@ -10,11 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
<p>{dateRange}</p>
|
|
12
12
|
|
|
13
|
-
<!-- IF
|
|
13
|
+
<!-- IF calendarUrl -->
|
|
14
14
|
<p>
|
|
15
|
-
<a href="{
|
|
16
|
-
|
|
15
|
+
<a href="{calendarUrl}" style="display:inline-block;padding:12px 18px;border-radius:6px;text-decoration:none;border:1px solid #333;">
|
|
16
|
+
Accéder au calendrier pour payer
|
|
17
17
|
</a>
|
|
18
18
|
</p>
|
|
19
|
-
<p style="font-size: 12px; color: #666;">
|
|
20
|
-
|
|
19
|
+
<p style="font-size: 12px; color: #666;">
|
|
20
|
+
Sur le calendrier, cliquez sur votre réservation puis sur le bouton <strong>« Payer maintenant »</strong>.
|
|
21
|
+
<!-- IF paymentUrl -->
|
|
22
|
+
<br>Vous pouvez aussi payer directement via ce lien (valable quelques heures) : <a href="{paymentUrl}">{paymentUrl}</a>
|
|
23
|
+
<!-- ENDIF paymentUrl -->
|
|
24
|
+
</p>
|
|
25
|
+
<!-- ENDIF calendarUrl -->
|