nodebb-plugin-onekite-calendar 2.0.97 → 2.0.98
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/helloassoWebhook.js +149 -0
- package/package.json +1 -1
package/lib/helloassoWebhook.js
CHANGED
|
@@ -133,6 +133,27 @@ function getCheckoutIntentIdFromPayload(payload) {
|
|
|
133
133
|
return null;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
// Returns true for event types and states that represent a HelloAsso refund.
|
|
137
|
+
function isRefundEvent(payload) {
|
|
138
|
+
try {
|
|
139
|
+
if (!payload) return false;
|
|
140
|
+
const eventType = String(payload.eventType || '').toLowerCase();
|
|
141
|
+
if (eventType === 'refund') return true;
|
|
142
|
+
const data = _payloadData(payload);
|
|
143
|
+
if (!data) return false;
|
|
144
|
+
const orderPayments = (data.order && Array.isArray(data.order.payments)) ? data.order.payments : [];
|
|
145
|
+
const state = String(
|
|
146
|
+
data.state || data.status || data.paymentState ||
|
|
147
|
+
(orderPayments.length ? (orderPayments[0].state || orderPayments[0].status) : '') ||
|
|
148
|
+
(data.order && (data.order.state || data.order.status)) ||
|
|
149
|
+
''
|
|
150
|
+
).toLowerCase();
|
|
151
|
+
return ['refunded', 'refund', 'refundpending'].includes(state);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
136
157
|
// Returns true for event types and states that represent a completed payment.
|
|
137
158
|
function isConfirmedPayment(payload) {
|
|
138
159
|
try {
|
|
@@ -235,6 +256,129 @@ async function tryRecoverRidFromPayment(settings, paymentId) {
|
|
|
235
256
|
}
|
|
236
257
|
}
|
|
237
258
|
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Refund handler (called from main handler when isRefundEvent() is true)
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
async function handleRefund(req, res, payload, settings, _d) {
|
|
264
|
+
try {
|
|
265
|
+
const rid = getReservationIdFromPayload(payload);
|
|
266
|
+
const checkoutIntentId = getCheckoutIntentIdFromPayload(payload);
|
|
267
|
+
|
|
268
|
+
// Refund idempotency key — prefix with "refund_" to avoid collision with payment IDs.
|
|
269
|
+
const _payments = (_d && _d.order && Array.isArray(_d.order.payments)) ? _d.order.payments : [];
|
|
270
|
+
const refundId = _d && _d.id != null ? String(_d.id) : null;
|
|
271
|
+
const idempotencyKey = refundId
|
|
272
|
+
? `refund_${refundId}`
|
|
273
|
+
: (!payload.data && checkoutIntentId ? `refund_ci_${checkoutIntentId}` : null);
|
|
274
|
+
|
|
275
|
+
if (!idempotencyKey) {
|
|
276
|
+
// eslint-disable-next-line no-console
|
|
277
|
+
console.warn('[calendar-onekite] HelloAsso webhook refund: missing refund id', { dataKeys: _d ? Object.keys(_d) : [] });
|
|
278
|
+
return res.json({ ok: true, ignored: true, missingRefundId: true });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (await alreadyProcessed(idempotencyKey)) {
|
|
282
|
+
return res.json({ ok: true, duplicate: true });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Resolve reservation ID
|
|
286
|
+
let resolvedRid = rid;
|
|
287
|
+
if (!resolvedRid && checkoutIntentId) {
|
|
288
|
+
resolvedRid = await tryRecoverRidFromCheckoutIntent(settings, checkoutIntentId);
|
|
289
|
+
}
|
|
290
|
+
const paymentIdForRecovery = payload.data
|
|
291
|
+
? (_d && (_d.id || _d.paymentId))
|
|
292
|
+
: (_payments.length ? _payments[0].id : null);
|
|
293
|
+
if (!resolvedRid && paymentIdForRecovery) {
|
|
294
|
+
resolvedRid = await tryRecoverRidFromPayment(settings, paymentIdForRecovery);
|
|
295
|
+
}
|
|
296
|
+
if (!resolvedRid) {
|
|
297
|
+
// eslint-disable-next-line no-console
|
|
298
|
+
console.warn('[calendar-onekite] HelloAsso webhook refund: could not resolve reservationId', { refundId, checkoutIntentId });
|
|
299
|
+
return res.json({ ok: true, processed: false, missingReservationId: true });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// eslint-disable-next-line no-console
|
|
303
|
+
console.info('[calendar-onekite] HelloAsso webhook refund received', { rid: resolvedRid, refundId });
|
|
304
|
+
|
|
305
|
+
const r = await dbLayer.getReservation(resolvedRid);
|
|
306
|
+
if (!r) {
|
|
307
|
+
await markProcessed(idempotencyKey);
|
|
308
|
+
return res.json({ ok: true, processed: true, reservationNotFound: true });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (r.status === 'cancelled') {
|
|
312
|
+
await markProcessed(idempotencyKey);
|
|
313
|
+
return res.json({ ok: true, processed: true, alreadyCancelled: true });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
r.status = 'cancelled';
|
|
317
|
+
r.cancelledAt = Date.now();
|
|
318
|
+
r.cancelledBy = 0;
|
|
319
|
+
r.cancelledByUsername = 'HelloAsso (remboursement)';
|
|
320
|
+
r.refundedAt = r.cancelledAt;
|
|
321
|
+
if (refundId) r.refundId = refundId;
|
|
322
|
+
|
|
323
|
+
await dbLayer.saveReservation(r);
|
|
324
|
+
|
|
325
|
+
await auditLog('reservation_refunded', 0, {
|
|
326
|
+
targetType: 'reservation',
|
|
327
|
+
targetId: String(r.rid),
|
|
328
|
+
reservationUid: Number(r.uid) || 0,
|
|
329
|
+
reservationUsername: String(r.username || ''),
|
|
330
|
+
itemIds: arrayifyIds(r),
|
|
331
|
+
itemNames: arrayifyNames(r),
|
|
332
|
+
startDate: r.startDate || '',
|
|
333
|
+
endDate: r.endDate || '',
|
|
334
|
+
refundId: refundId || '',
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
realtime.emitCalendarUpdated({ kind: 'reservation', action: 'cancelled', rid: String(r.rid), status: r.status });
|
|
338
|
+
|
|
339
|
+
// Email requester
|
|
340
|
+
try {
|
|
341
|
+
const requesterUid = parseInt(r.uid, 10);
|
|
342
|
+
const requester = await user.getUserFields(requesterUid, ['username']);
|
|
343
|
+
if (requesterUid) {
|
|
344
|
+
await sendEmail('calendar-onekite_cancelled', requesterUid, 'Location matériel - Réservation annulée (remboursement)', {
|
|
345
|
+
uid: requesterUid,
|
|
346
|
+
username: requester && requester.username ? requester.username : '',
|
|
347
|
+
itemName: arrayifyNames(r).join(', '),
|
|
348
|
+
itemNames: arrayifyNames(r),
|
|
349
|
+
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
350
|
+
cancelledBy: 'HelloAsso (remboursement)',
|
|
351
|
+
cancelledByUrl: '',
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
} catch (e) {}
|
|
355
|
+
|
|
356
|
+
// Discord notification
|
|
357
|
+
try {
|
|
358
|
+
await discord.notifyReservationCancelled(settings, {
|
|
359
|
+
rid: r.rid,
|
|
360
|
+
uid: r.uid,
|
|
361
|
+
username: r.username || '',
|
|
362
|
+
itemIds: arrayifyIds(r),
|
|
363
|
+
itemNames: arrayifyNames(r),
|
|
364
|
+
start: r.start,
|
|
365
|
+
end: r.end,
|
|
366
|
+
status: r.status,
|
|
367
|
+
cancelledAt: r.cancelledAt,
|
|
368
|
+
cancelledBy: 0,
|
|
369
|
+
cancelledByUsername: r.cancelledByUsername,
|
|
370
|
+
});
|
|
371
|
+
} catch (e) {}
|
|
372
|
+
|
|
373
|
+
await markProcessed(idempotencyKey);
|
|
374
|
+
return res.json({ ok: true, processed: true, refunded: true });
|
|
375
|
+
} catch (err) {
|
|
376
|
+
// eslint-disable-next-line no-console
|
|
377
|
+
console.error('[calendar-onekite] HelloAsso webhook refund: unhandled error', err && err.message ? err.message : String(err));
|
|
378
|
+
return res.status(500).json({ ok: false, error: 'internal-error' });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
238
382
|
// ---------------------------------------------------------------------------
|
|
239
383
|
// Webhook handler
|
|
240
384
|
// ---------------------------------------------------------------------------
|
|
@@ -308,6 +452,11 @@ async function handler(req, res, next) {
|
|
|
308
452
|
contentType: req.headers && req.headers['content-type'],
|
|
309
453
|
});
|
|
310
454
|
|
|
455
|
+
// Handle refund events before the confirmed-payment check.
|
|
456
|
+
if (isRefundEvent(payload)) {
|
|
457
|
+
return await handleRefund(req, res, payload, settings, _d);
|
|
458
|
+
}
|
|
459
|
+
|
|
311
460
|
if (!isConfirmedPayment(payload)) {
|
|
312
461
|
// eslint-disable-next-line no-console
|
|
313
462
|
console.warn('[calendar-onekite] HelloAsso webhook: payment not confirmed — ignored', {
|
package/package.json
CHANGED