nodebb-plugin-onekite-calendar 2.0.88 → 2.0.89
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/api.js +104 -0
- package/library.js +1 -0
- package/package.json +1 -1
- package/public/client.js +16 -3
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
|
*
|
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
|
};
|