nodebb-plugin-onekite-calendar 2.0.98 → 2.1.0
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 +0 -1
- package/lib/helloasso.js +58 -7
- package/lib/helloassoWebhook.js +3 -2
- package/lib/syncPayment.js +41 -2
- package/package.json +1 -1
- package/public/client.js +16 -1
- package/templates/emails/calendar-onekite_pending.tpl +1 -1
- package/templates/emails/calendar-onekite_validator_reminder.tpl +1 -1
package/lib/api.js
CHANGED
package/lib/helloasso.js
CHANGED
|
@@ -26,15 +26,16 @@ function requestJson(method, url, headers = {}, bodyObj = null) {
|
|
|
26
26
|
resp.on('data', (chunk) => { data += chunk; });
|
|
27
27
|
resp.on('end', () => {
|
|
28
28
|
const status = resp.statusCode || 0;
|
|
29
|
+
const headers = resp.headers || {};
|
|
29
30
|
|
|
30
31
|
// Try JSON first, but never throw from here (avoid crashing NodeBB).
|
|
31
32
|
try {
|
|
32
33
|
const json = data ? JSON.parse(data) : null;
|
|
33
|
-
return resolve({ status, json });
|
|
34
|
+
return resolve({ status, json, headers });
|
|
34
35
|
} catch (e) {
|
|
35
36
|
// Non-JSON response (HTML/proxy error/etc.)
|
|
36
37
|
const snippet = String(data || '').slice(0, 500);
|
|
37
|
-
return resolve({ status, json: null, raw: snippet });
|
|
38
|
+
return resolve({ status, json: null, raw: snippet, headers });
|
|
38
39
|
}
|
|
39
40
|
});
|
|
40
41
|
}
|
|
@@ -177,6 +178,14 @@ async function _fetchAccessTokenWithRetry(params) {
|
|
|
177
178
|
return { token: null, expiresAt: 0 };
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
// Force the next getAccessToken() call to fetch a fresh token instead of reusing
|
|
182
|
+
// the in-memory cache. Callers should invoke this after a downstream API call
|
|
183
|
+
// comes back 401/403 with a cached token, since a stale/rotated-secret token
|
|
184
|
+
// would otherwise keep failing silently for up to ~1h (the normal cache TTL).
|
|
185
|
+
function invalidateTokenCache() {
|
|
186
|
+
_tokenCache = { token: null, expiresAt: 0 };
|
|
187
|
+
}
|
|
188
|
+
|
|
180
189
|
async function getAccessToken({ env, clientId, clientSecret }) {
|
|
181
190
|
if (!clientId || !clientSecret) return null;
|
|
182
191
|
|
|
@@ -331,18 +340,60 @@ async function getPaymentDetails({ env, token, paymentId }) {
|
|
|
331
340
|
|
|
332
341
|
|
|
333
342
|
|
|
343
|
+
// Returns { ok, status, reason, json }. On success, ok=true and json is the checkout
|
|
344
|
+
// intent payload. On failure, ok=false and `reason` distinguishes a genuine 404 from
|
|
345
|
+
// transient errors (rate-limit, auth, network, 5xx) so callers don't misreport
|
|
346
|
+
// "does not exist" for issues that are actually retryable.
|
|
334
347
|
async function getCheckoutIntentDetails({ env, token, organizationSlug, checkoutIntentId }) {
|
|
335
|
-
if (!token || !organizationSlug || !checkoutIntentId)
|
|
348
|
+
if (!token || !organizationSlug || !checkoutIntentId) {
|
|
349
|
+
return { ok: false, status: 0, reason: 'missing-params', json: null };
|
|
350
|
+
}
|
|
336
351
|
const url = `${baseUrl(env)}/v5/organizations/${encodeURIComponent(String(organizationSlug))}/checkout-intents/${encodeURIComponent(String(checkoutIntentId))}`;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
352
|
+
|
|
353
|
+
let last = { status: 0, json: null };
|
|
354
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
355
|
+
// eslint-disable-next-line no-await-in-loop
|
|
356
|
+
const result = await requestJson('GET', url, { Authorization: `Bearer ${token}` });
|
|
357
|
+
last = result;
|
|
358
|
+
const { status, json, raw, headers } = result;
|
|
359
|
+
|
|
360
|
+
if (status >= 200 && status < 300) {
|
|
361
|
+
return { ok: true, status, reason: null, json: json || null };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const rateLimited = _isRateLimited(status, json, raw);
|
|
365
|
+
const networkish = status === 0;
|
|
366
|
+
|
|
367
|
+
if ((rateLimited || networkish) && attempt < 2) {
|
|
368
|
+
const base = 1000 * (2 ** attempt);
|
|
369
|
+
const ra = _retryAfterMs(headers);
|
|
370
|
+
const jitter = Math.floor(Math.random() * 200);
|
|
371
|
+
// eslint-disable-next-line no-await-in-loop
|
|
372
|
+
await _sleep(Math.min(15_000, Math.max(base, ra) + jitter));
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
break;
|
|
340
376
|
}
|
|
341
|
-
|
|
377
|
+
|
|
378
|
+
const { status, json, raw, error } = last;
|
|
379
|
+
let reason = 'http-error';
|
|
380
|
+
if (status === 0) reason = 'network-error';
|
|
381
|
+
else if (status === 401 || status === 403) reason = 'unauthorized';
|
|
382
|
+
else if (status === 404) reason = 'not-found';
|
|
383
|
+
else if (status === 429) reason = 'rate-limited';
|
|
384
|
+
else if (status >= 500) reason = 'server-error';
|
|
385
|
+
|
|
386
|
+
// eslint-disable-next-line no-console
|
|
387
|
+
console.warn('[calendar-onekite] HelloAsso getCheckoutIntentDetails failed', {
|
|
388
|
+
checkoutIntentId, status, reason, detail: raw || error || (json ? JSON.stringify(json).slice(0, 300) : ''),
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return { ok: false, status, reason, json: json || null };
|
|
342
392
|
}
|
|
343
393
|
|
|
344
394
|
module.exports = {
|
|
345
395
|
getAccessToken,
|
|
396
|
+
invalidateTokenCache,
|
|
346
397
|
listItems,
|
|
347
398
|
getFormPublic,
|
|
348
399
|
extractCatalogItems,
|
package/lib/helloassoWebhook.js
CHANGED
|
@@ -218,13 +218,14 @@ async function tryRecoverRidFromCheckoutIntent(settings, checkoutIntentId) {
|
|
|
218
218
|
clientSecret: settings.helloassoClientSecret,
|
|
219
219
|
});
|
|
220
220
|
if (!token) return null;
|
|
221
|
-
const
|
|
221
|
+
const detailsResult = await helloasso.getCheckoutIntentDetails({
|
|
222
222
|
env: settings.helloassoEnv || 'prod',
|
|
223
223
|
token,
|
|
224
224
|
organizationSlug: settings.helloassoOrganizationSlug,
|
|
225
225
|
checkoutIntentId,
|
|
226
226
|
});
|
|
227
|
-
if (!
|
|
227
|
+
if (!detailsResult.ok || !detailsResult.json) return null;
|
|
228
|
+
const details = detailsResult.json;
|
|
228
229
|
const rid = getReservationIdFromPayload({ data: details });
|
|
229
230
|
if (rid) {
|
|
230
231
|
try { await db.setObjectField(dbLayer.KEY_CHECKOUT_INTENT_TO_RID, String(checkoutIntentId), String(rid)); } catch (e) {}
|
package/lib/syncPayment.js
CHANGED
|
@@ -42,13 +42,52 @@ async function syncPayment({ r, settings, actorUid }) {
|
|
|
42
42
|
});
|
|
43
43
|
if (!token) return { ok: false, reason: 'helloasso-auth-failed' };
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
let detailsResult = await helloasso.getCheckoutIntentDetails({
|
|
46
46
|
env: settings.helloassoEnv || 'prod',
|
|
47
47
|
token,
|
|
48
48
|
organizationSlug: settings.helloassoOrganizationSlug,
|
|
49
49
|
checkoutIntentId,
|
|
50
50
|
});
|
|
51
|
-
|
|
51
|
+
|
|
52
|
+
// A cached token can go stale (e.g. HelloAsso secret rotated in ACP) and would
|
|
53
|
+
// otherwise keep failing every retry for up to ~1h. Force one fresh-token retry.
|
|
54
|
+
if (!detailsResult.ok && detailsResult.reason === 'unauthorized') {
|
|
55
|
+
helloasso.invalidateTokenCache();
|
|
56
|
+
const freshToken = await helloasso.getAccessToken({
|
|
57
|
+
env: settings.helloassoEnv || 'prod',
|
|
58
|
+
clientId: settings.helloassoClientId,
|
|
59
|
+
clientSecret: settings.helloassoClientSecret,
|
|
60
|
+
});
|
|
61
|
+
if (freshToken) {
|
|
62
|
+
detailsResult = await helloasso.getCheckoutIntentDetails({
|
|
63
|
+
env: settings.helloassoEnv || 'prod',
|
|
64
|
+
token: freshToken,
|
|
65
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
66
|
+
checkoutIntentId,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!detailsResult.ok) {
|
|
72
|
+
// Only a genuine HTTP 404 means "does not exist on HelloAsso". Everything else
|
|
73
|
+
// (rate-limit, auth, network, 5xx) is transient — surface it distinctly so the
|
|
74
|
+
// admin knows to retry rather than assuming the payment is missing.
|
|
75
|
+
const reasonMap = {
|
|
76
|
+
'not-found': 'checkout-intent-not-found',
|
|
77
|
+
'rate-limited': 'helloasso-rate-limited-try-again',
|
|
78
|
+
'unauthorized': 'helloasso-auth-failed',
|
|
79
|
+
'network-error': 'helloasso-network-error-try-again',
|
|
80
|
+
'server-error': 'helloasso-server-error-try-again',
|
|
81
|
+
'missing-params': 'no-checkout-intent-id',
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
reason: reasonMap[detailsResult.reason] || `helloasso-error-${detailsResult.status || 'unknown'}`,
|
|
86
|
+
httpStatus: detailsResult.status,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const details = detailsResult.json;
|
|
90
|
+
if (!details) return { ok: false, reason: 'checkout-intent-empty-response' };
|
|
52
91
|
|
|
53
92
|
const order = details.order ? details.order : details;
|
|
54
93
|
const orderState = String((order.state || order.status) || '').toLowerCase();
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -2433,7 +2433,22 @@ function toDatetimeLocalValue(date) {
|
|
|
2433
2433
|
try { bootbox.hideAll(); } catch (e) {}
|
|
2434
2434
|
} else {
|
|
2435
2435
|
const reason = (resp && resp.reason) || 'inconnu';
|
|
2436
|
-
|
|
2436
|
+
const retryReasons = [
|
|
2437
|
+
'helloasso-rate-limited-try-again',
|
|
2438
|
+
'helloasso-network-error-try-again',
|
|
2439
|
+
'helloasso-server-error-try-again',
|
|
2440
|
+
];
|
|
2441
|
+
if (retryReasons.includes(reason)) {
|
|
2442
|
+
showAlert('warning', 'HelloAsso est momentanément indisponible (limite de requêtes ou erreur réseau) — réessayez dans quelques secondes.');
|
|
2443
|
+
} else if (reason === 'helloasso-auth-failed') {
|
|
2444
|
+
showAlert('warning', 'Connexion à HelloAsso impossible (identifiants API) — vérifiez la configuration dans les réglages du plugin.');
|
|
2445
|
+
} else if (reason === 'checkout-intent-not-found') {
|
|
2446
|
+
showAlert('warning', 'HelloAsso ne trouve plus ce paiement (checkout intent introuvable) — vérifiez manuellement dans le back-office HelloAsso.');
|
|
2447
|
+
} else if (reason === 'not-paid-on-helloasso') {
|
|
2448
|
+
showAlert('warning', 'Paiement pas encore confirmé chez HelloAsso.');
|
|
2449
|
+
} else {
|
|
2450
|
+
showAlert('warning', `Paiement non confirmé chez HelloAsso (${reason}).`);
|
|
2451
|
+
}
|
|
2437
2452
|
}
|
|
2438
2453
|
} catch (e) {
|
|
2439
2454
|
showAlert('error', 'Erreur lors de la vérification du paiement.');
|