nodebb-plugin-onekite-calendar 2.0.83 → 2.0.84
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 +96 -31
- package/lib/helloasso.js +11 -1
- package/package.json +1 -1
package/lib/admin.js
CHANGED
|
@@ -8,7 +8,6 @@ const nconf = require.main.require('nconf');
|
|
|
8
8
|
|
|
9
9
|
const shared = require('./shared');
|
|
10
10
|
const {
|
|
11
|
-
forumBaseUrl,
|
|
12
11
|
formatFR,
|
|
13
12
|
sendEmail,
|
|
14
13
|
buildCalendarLinks,
|
|
@@ -111,39 +110,67 @@ admin.approveReservation = async function (req, res) {
|
|
|
111
110
|
let paymentUrl = null;
|
|
112
111
|
if (token) {
|
|
113
112
|
const requester = await user.getUserFields(r.uid, ['username', 'email']);
|
|
114
|
-
// r.total is stored as an estimated total in euros; HelloAsso expects cents.
|
|
115
|
-
const totalAmount = Math.max(0, Math.round((Number(r.total) || 0) * 100));
|
|
116
|
-
const base = forumBaseUrl();
|
|
117
|
-
const returnUrl = base ? `${base}/calendar` : '';
|
|
118
|
-
const webhookUrl = base ? `${base}/plugins/calendar-onekite/helloasso` : '';
|
|
119
113
|
const year = new Date(Number(r.start)).getFullYear();
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (intent && intent.checkoutIntentId) {
|
|
144
|
-
r.checkoutIntentId = intent.checkoutIntentId;
|
|
114
|
+
const formSlug = autoFormSlugForYear(year);
|
|
115
|
+
|
|
116
|
+
// Recompute total from HelloAsso catalog (same logic as api.js approve path)
|
|
117
|
+
// to ensure the checkout amount is always up-to-date with actual item prices.
|
|
118
|
+
let recomputedTotalCents = null;
|
|
119
|
+
try {
|
|
120
|
+
const { items: catalog } = await helloasso.listCatalogItems({
|
|
121
|
+
env,
|
|
122
|
+
token,
|
|
123
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
124
|
+
formType: settings.helloassoFormType,
|
|
125
|
+
formSlug,
|
|
126
|
+
});
|
|
127
|
+
const byId = new Map((catalog || []).map((it) => [String(it.id), (typeof it.price === 'number' ? it.price : 0)]));
|
|
128
|
+
const ids = (Array.isArray(r.itemIds) ? r.itemIds : (r.itemId ? [r.itemId] : [])).map(String);
|
|
129
|
+
const days = calendarDaysExclusiveYmd(r.startDate, r.endDate) || 1;
|
|
130
|
+
const sumCentsPerDay = ids.reduce((acc, id) => acc + (byId.get(id) || 0), 0);
|
|
131
|
+
if (sumCentsPerDay > 0) {
|
|
132
|
+
recomputedTotalCents = Math.max(0, Math.round(sumCentsPerDay * days));
|
|
133
|
+
r.total = recomputedTotalCents / 100;
|
|
134
|
+
}
|
|
135
|
+
} catch (e) {
|
|
136
|
+
// ignore recompute failures; fallback to stored total
|
|
145
137
|
}
|
|
138
|
+
|
|
139
|
+
const totalAmount = (typeof recomputedTotalCents === 'number')
|
|
140
|
+
? recomputedTotalCents
|
|
141
|
+
: Math.max(0, Math.round((Number(r.total) || 0) * 100));
|
|
142
|
+
|
|
143
|
+
if (!totalAmount) {
|
|
144
|
+
console.warn('[calendar-onekite] HelloAsso totalAmount is 0 (approve ACP) — skipping checkout intent', { rid, total: r.total });
|
|
145
|
+
} else {
|
|
146
|
+
const returnUrl = normalizeReturnUrl();
|
|
147
|
+
const webhookUrl = normalizeCallbackUrl(settings.helloassoCallbackUrl);
|
|
148
|
+
const intent = await helloasso.createCheckoutIntent({
|
|
149
|
+
env,
|
|
150
|
+
token,
|
|
151
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
152
|
+
formType: settings.helloassoFormType,
|
|
153
|
+
formSlug,
|
|
154
|
+
totalAmount,
|
|
155
|
+
payerEmail: requester && requester.email,
|
|
156
|
+
callbackUrl: returnUrl,
|
|
157
|
+
webhookUrl: webhookUrl,
|
|
158
|
+
itemName: buildHelloAssoItemName('Réservation matériel Onekite', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
|
|
159
|
+
containsDonation: false,
|
|
160
|
+
metadata: {
|
|
161
|
+
reservationId: String(rid),
|
|
162
|
+
items: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
|
|
163
|
+
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
paymentUrl = intent && (intent.paymentUrl || intent.redirectUrl)
|
|
167
|
+
? (intent.paymentUrl || intent.redirectUrl)
|
|
168
|
+
: (typeof intent === 'string' ? intent : null);
|
|
169
|
+
if (intent && intent.checkoutIntentId) {
|
|
170
|
+
r.checkoutIntentId = intent.checkoutIntentId;
|
|
171
|
+
}
|
|
146
172
|
}
|
|
173
|
+
}
|
|
147
174
|
|
|
148
175
|
if (paymentUrl) {
|
|
149
176
|
r.paymentUrl = paymentUrl;
|
|
@@ -441,6 +468,44 @@ admin.debugHelloAsso = async function (req, res) {
|
|
|
441
468
|
out.soldItems = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [] };
|
|
442
469
|
}
|
|
443
470
|
|
|
471
|
+
// Test checkout intent creation with a 1-cent amount to validate URL format.
|
|
472
|
+
// This creates a real (but minimal) intent — it will expire quickly and can be ignored.
|
|
473
|
+
out.checkoutIntentTest = { ok: false };
|
|
474
|
+
try {
|
|
475
|
+
const callbackUrl = normalizeReturnUrl();
|
|
476
|
+
const webhookUrl = normalizeCallbackUrl(settings.helloassoCallbackUrl);
|
|
477
|
+
if (!callbackUrl) {
|
|
478
|
+
out.checkoutIntentTest = { ok: false, error: 'callbackUrl empty — forum base URL not configured' };
|
|
479
|
+
} else {
|
|
480
|
+
const intent = await helloasso.createCheckoutIntent({
|
|
481
|
+
env,
|
|
482
|
+
token,
|
|
483
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
484
|
+
formType: settings.helloassoFormType,
|
|
485
|
+
formSlug: `locations-materiel-${new Date().getFullYear()}`,
|
|
486
|
+
totalAmount: 100,
|
|
487
|
+
payerEmail: '',
|
|
488
|
+
callbackUrl,
|
|
489
|
+
webhookUrl,
|
|
490
|
+
itemName: '[TEST] Vérification lien paiement',
|
|
491
|
+
containsDonation: false,
|
|
492
|
+
metadata: { debug: 'true' },
|
|
493
|
+
});
|
|
494
|
+
if (intent && intent.paymentUrl) {
|
|
495
|
+
out.checkoutIntentTest = {
|
|
496
|
+
ok: true,
|
|
497
|
+
checkoutIntentId: intent.checkoutIntentId,
|
|
498
|
+
paymentUrl: intent.paymentUrl,
|
|
499
|
+
note: 'Cet intent de test (1 cent) peut être ignoré — il expirera automatiquement',
|
|
500
|
+
};
|
|
501
|
+
} else {
|
|
502
|
+
out.checkoutIntentTest = { ok: false, error: 'no paymentUrl in response', raw: intent };
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
} catch (e) {
|
|
506
|
+
out.checkoutIntentTest = { ok: false, error: String(e && e.message ? e.message : e) };
|
|
507
|
+
}
|
|
508
|
+
|
|
444
509
|
return res.json(out);
|
|
445
510
|
} catch (e) {
|
|
446
511
|
out.ok = false;
|
package/lib/helloasso.js
CHANGED
|
@@ -306,7 +306,17 @@ async function createCheckoutIntent({ env, token, organizationSlug, formType, fo
|
|
|
306
306
|
};
|
|
307
307
|
const { status, json } = await requestJson('POST', url, { Authorization: `Bearer ${token}` }, payload);
|
|
308
308
|
if (status >= 200 && status < 300 && json) {
|
|
309
|
-
|
|
309
|
+
const redirectUrl = json.redirectUrl || json.checkoutUrl || json.url || null;
|
|
310
|
+
// Always log so the URL format is visible in NodeBB logs (helps diagnose 404s).
|
|
311
|
+
// eslint-disable-next-line no-console
|
|
312
|
+
console.log('[calendar-onekite] HelloAsso checkout-intent created', {
|
|
313
|
+
status,
|
|
314
|
+
checkoutIntentId: json.id || json.checkoutIntentId || null,
|
|
315
|
+
redirectUrl,
|
|
316
|
+
env,
|
|
317
|
+
organizationSlug,
|
|
318
|
+
});
|
|
319
|
+
return { paymentUrl: redirectUrl, checkoutIntentId: (json.id || json.checkoutIntentId || null), raw: json };
|
|
310
320
|
}
|
|
311
321
|
// Log the error payload to help diagnose configuration issues (slug, env, urls, amount, etc.)
|
|
312
322
|
try {
|
package/package.json
CHANGED