nodebb-plugin-onekite-calendar 2.0.89 → 2.0.91
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 +21 -116
- package/lib/api.js +28 -207
- package/lib/checkout.js +126 -0
- package/lib/helloasso.js +0 -9
- package/lib/scheduler.js +1 -0
- package/package.json +1 -1
- package/public/admin.js +45 -10
- package/templates/emails/calendar-onekite_approved.tpl +10 -5
- package/templates/emails/calendar-onekite_reminder.tpl +10 -5
package/lib/admin.js
CHANGED
|
@@ -11,16 +11,15 @@ const {
|
|
|
11
11
|
formatFR,
|
|
12
12
|
sendEmail,
|
|
13
13
|
buildCalendarLinks,
|
|
14
|
-
buildHelloAssoItemName,
|
|
15
|
-
normalizeCallbackUrl,
|
|
16
|
-
normalizeReturnUrl,
|
|
17
14
|
calendarDaysExclusiveYmd,
|
|
18
15
|
yearFromTs,
|
|
19
16
|
autoFormSlugForYear,
|
|
17
|
+
forumBaseUrl,
|
|
20
18
|
} = shared;
|
|
21
19
|
|
|
22
20
|
const dbLayer = require('./db');
|
|
23
21
|
const helloasso = require('./helloasso');
|
|
22
|
+
const { buildCheckoutIntent } = require('./checkout');
|
|
24
23
|
|
|
25
24
|
const ADMIN_PRIV = 'admin:settings';
|
|
26
25
|
|
|
@@ -44,10 +43,16 @@ admin.saveSettings = async function (req, res) {
|
|
|
44
43
|
};
|
|
45
44
|
|
|
46
45
|
admin.listPending = async function (req, res) {
|
|
46
|
+
// Accept ?status=pending or ?status=pending,awaiting_payment (default: both)
|
|
47
|
+
const rawStatus = (req.query && req.query.status) ? String(req.query.status) : '';
|
|
48
|
+
const statusFilter = rawStatus
|
|
49
|
+
? rawStatus.split(',').map((s) => s.trim()).filter(Boolean)
|
|
50
|
+
: ['pending', 'awaiting_payment'];
|
|
51
|
+
|
|
47
52
|
const ids = await dbLayer.listAllReservationIds(5000);
|
|
48
53
|
// Batch fetch to avoid N DB round-trips.
|
|
49
54
|
const rows = await dbLayer.getReservations(ids);
|
|
50
|
-
const pending = (rows || []).filter(r => r && r.status
|
|
55
|
+
const pending = (rows || []).filter(r => r && statusFilter.includes(r.status));
|
|
51
56
|
pending.sort((a, b) => parseInt(a.start, 10) - parseInt(b.start, 10));
|
|
52
57
|
|
|
53
58
|
// Enrich with user info for ACP display (username + userslug)
|
|
@@ -98,84 +103,21 @@ admin.approveReservation = async function (req, res) {
|
|
|
98
103
|
r.approvedByUsername = '';
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
// Create HelloAsso payment link
|
|
106
|
+
// Create HelloAsso payment link
|
|
102
107
|
const settings = await getSettings();
|
|
103
|
-
const env = settings.helloassoEnv || 'prod';
|
|
104
|
-
const token = await helloasso.getAccessToken({
|
|
105
|
-
env,
|
|
106
|
-
clientId: settings.helloassoClientId,
|
|
107
|
-
clientSecret: settings.helloassoClientSecret,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
108
|
let paymentUrl = null;
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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 });
|
|
109
|
+
try {
|
|
110
|
+
const checkoutResult = await buildCheckoutIntent({ r, settings });
|
|
111
|
+
if (checkoutResult) {
|
|
112
|
+
paymentUrl = checkoutResult.paymentUrl;
|
|
113
|
+
r.paymentUrl = checkoutResult.paymentUrl;
|
|
114
|
+
if (checkoutResult.checkoutIntentId) r.checkoutIntentId = checkoutResult.checkoutIntentId;
|
|
115
|
+
if (checkoutResult.catalogTotalCents !== null) r.total = checkoutResult.catalogTotalCents / 100;
|
|
145
116
|
} else {
|
|
146
|
-
|
|
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
|
-
}
|
|
117
|
+
console.warn('[calendar-onekite] HelloAsso payment link not created (approve ACP)', { rid });
|
|
172
118
|
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (paymentUrl) {
|
|
176
|
-
r.paymentUrl = paymentUrl;
|
|
177
|
-
} else {
|
|
178
|
-
console.warn('[calendar-onekite] HelloAsso payment link not created (approve ACP)', { rid });
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.warn('[calendar-onekite] HelloAsso payment link error (approve ACP)', e && e.message ? e.message : e);
|
|
179
121
|
}
|
|
180
122
|
|
|
181
123
|
await dbLayer.saveReservation(r);
|
|
@@ -213,6 +155,7 @@ admin.approveReservation = async function (req, res) {
|
|
|
213
155
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
214
156
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
215
157
|
paymentUrl: paymentUrl || '',
|
|
158
|
+
calendarUrl: `${forumBaseUrl()}/calendar`,
|
|
216
159
|
pickupAddress: r.pickupAddress || '',
|
|
217
160
|
notes: r.notes || '',
|
|
218
161
|
pickupTime: r.pickupTime || '',
|
|
@@ -506,44 +449,6 @@ admin.debugHelloAsso = async function (req, res) {
|
|
|
506
449
|
out.soldItems = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [] };
|
|
507
450
|
}
|
|
508
451
|
|
|
509
|
-
// Test checkout intent creation with a 1-cent amount to validate URL format.
|
|
510
|
-
// This creates a real (but minimal) intent — it will expire quickly and can be ignored.
|
|
511
|
-
out.checkoutIntentTest = { ok: false };
|
|
512
|
-
try {
|
|
513
|
-
const callbackUrl = normalizeReturnUrl();
|
|
514
|
-
const webhookUrl = normalizeCallbackUrl(settings.helloassoCallbackUrl);
|
|
515
|
-
if (!callbackUrl) {
|
|
516
|
-
out.checkoutIntentTest = { ok: false, error: 'callbackUrl empty — forum base URL not configured' };
|
|
517
|
-
} else {
|
|
518
|
-
const intent = await helloasso.createCheckoutIntent({
|
|
519
|
-
env,
|
|
520
|
-
token,
|
|
521
|
-
organizationSlug: settings.helloassoOrganizationSlug,
|
|
522
|
-
formType: settings.helloassoFormType,
|
|
523
|
-
formSlug: `locations-materiel-${new Date().getFullYear()}`,
|
|
524
|
-
totalAmount: 100,
|
|
525
|
-
payerEmail: '',
|
|
526
|
-
callbackUrl,
|
|
527
|
-
webhookUrl,
|
|
528
|
-
itemName: '[TEST] Vérification lien paiement',
|
|
529
|
-
containsDonation: false,
|
|
530
|
-
metadata: { debug: 'true' },
|
|
531
|
-
});
|
|
532
|
-
if (intent && intent.paymentUrl) {
|
|
533
|
-
out.checkoutIntentTest = {
|
|
534
|
-
ok: true,
|
|
535
|
-
checkoutIntentId: intent.checkoutIntentId,
|
|
536
|
-
paymentUrl: intent.paymentUrl,
|
|
537
|
-
note: 'Cet intent de test (1 cent) peut être ignoré — il expirera automatiquement',
|
|
538
|
-
};
|
|
539
|
-
} else {
|
|
540
|
-
out.checkoutIntentTest = { ok: false, error: 'no paymentUrl in response', raw: intent };
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
} catch (e) {
|
|
544
|
-
out.checkoutIntentTest = { ok: false, error: String(e && e.message ? e.message : e) };
|
|
545
|
-
}
|
|
546
|
-
|
|
547
452
|
return res.json(out);
|
|
548
453
|
} catch (e) {
|
|
549
454
|
out.ok = false;
|
package/lib/api.js
CHANGED
|
@@ -24,21 +24,16 @@ const {
|
|
|
24
24
|
yearFromTs,
|
|
25
25
|
autoCreatorGroupForYear,
|
|
26
26
|
autoFormSlugForYear,
|
|
27
|
-
normalizeCallbackUrl,
|
|
28
|
-
normalizeReturnUrl,
|
|
29
|
-
buildHelloAssoItemName,
|
|
30
27
|
buildCalendarLinks,
|
|
31
28
|
signCalendarLink,
|
|
32
29
|
ymdToCompact,
|
|
33
|
-
dtToGCalUtc,
|
|
34
30
|
getSetting,
|
|
35
31
|
} = shared;
|
|
36
32
|
|
|
37
33
|
const helloasso = require('./helloasso');
|
|
38
34
|
const discord = require('./discord');
|
|
39
35
|
const realtime = require('./realtime');
|
|
40
|
-
|
|
41
|
-
// normalizeCallbackUrl and normalizeReturnUrl are imported from shared.js
|
|
36
|
+
const { buildCheckoutIntent } = require('./checkout');
|
|
42
37
|
|
|
43
38
|
|
|
44
39
|
function overlap(aStart, aEnd, bStart, bEnd) {
|
|
@@ -46,8 +41,6 @@ function overlap(aStart, aEnd, bStart, bEnd) {
|
|
|
46
41
|
}
|
|
47
42
|
|
|
48
43
|
|
|
49
|
-
// buildHelloAssoItemName is imported from shared.js
|
|
50
|
-
|
|
51
44
|
function toTs(v) {
|
|
52
45
|
if (v === undefined || v === null || v === '') return NaN;
|
|
53
46
|
// Accept milliseconds timestamps passed as strings or numbers.
|
|
@@ -647,82 +640,18 @@ api.getFreshPaymentUrl = async function (req, res) {
|
|
|
647
640
|
return res.status(409).json({ error: 'wrong-status', status: r.status });
|
|
648
641
|
}
|
|
649
642
|
|
|
650
|
-
// Build a fresh checkout intent
|
|
651
|
-
const
|
|
652
|
-
|
|
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) {
|
|
643
|
+
// Build a fresh checkout intent (handles token, catalog recompute, intent creation)
|
|
644
|
+
const checkoutResult = await buildCheckoutIntent({ r, settings });
|
|
645
|
+
if (!checkoutResult) {
|
|
717
646
|
return res.status(502).json({ error: 'intent-creation-failed' });
|
|
718
647
|
}
|
|
719
648
|
|
|
720
|
-
|
|
721
|
-
r.
|
|
722
|
-
if (
|
|
649
|
+
r.paymentUrl = checkoutResult.paymentUrl;
|
|
650
|
+
if (checkoutResult.checkoutIntentId) r.checkoutIntentId = checkoutResult.checkoutIntentId;
|
|
651
|
+
if (checkoutResult.catalogTotalCents !== null) r.total = checkoutResult.catalogTotalCents / 100;
|
|
723
652
|
await dbLayer.saveReservation(r);
|
|
724
653
|
|
|
725
|
-
return res.json({ ok: true, paymentUrl });
|
|
654
|
+
return res.json({ ok: true, paymentUrl: checkoutResult.paymentUrl });
|
|
726
655
|
};
|
|
727
656
|
|
|
728
657
|
/**
|
|
@@ -1398,9 +1327,9 @@ api.createReservation = async function (req, res) {
|
|
|
1398
1327
|
if (!(exStart < end && start < exEnd)) continue;
|
|
1399
1328
|
}
|
|
1400
1329
|
const exItemIds = Array.isArray(existing.itemIds) ? existing.itemIds : (existing.itemId ? [existing.itemId] : []);
|
|
1401
|
-
const
|
|
1402
|
-
if (
|
|
1403
|
-
conflicts.push({ rid: existing.rid, itemIds:
|
|
1330
|
+
const conflictingItemIds = exItemIds.filter(x => itemIds.includes(String(x)));
|
|
1331
|
+
if (conflictingItemIds.length) {
|
|
1332
|
+
conflicts.push({ rid: existing.rid, itemIds: conflictingItemIds, status: existing.status });
|
|
1404
1333
|
}
|
|
1405
1334
|
}
|
|
1406
1335
|
if (conflicts.length) {
|
|
@@ -1491,63 +1420,16 @@ api.createReservation = async function (req, res) {
|
|
|
1491
1420
|
// create the HelloAsso checkout intent immediately so the user has a payment link right away.
|
|
1492
1421
|
if (isRegularizationOther) {
|
|
1493
1422
|
try {
|
|
1494
|
-
const
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
let recomputedTotalCents = null;
|
|
1500
|
-
try {
|
|
1501
|
-
const { items: catalog } = await helloasso.listCatalogItems({
|
|
1502
|
-
env: settings.helloassoEnv,
|
|
1503
|
-
token,
|
|
1504
|
-
organizationSlug: settings.helloassoOrganizationSlug,
|
|
1505
|
-
formType: settings.helloassoFormType,
|
|
1506
|
-
formSlug: autoFormSlugForYear(year),
|
|
1507
|
-
});
|
|
1508
|
-
const byId = new Map((catalog || []).map((it) => [String(it.id), (typeof it.price === 'number' ? it.price : 0)]));
|
|
1509
|
-
const ids = itemIds.map(String);
|
|
1510
|
-
const sumCentsPerDay = ids.reduce((acc, id) => acc + (byId.get(id) || 0), 0);
|
|
1511
|
-
if (sumCentsPerDay > 0) {
|
|
1512
|
-
recomputedTotalCents = Math.max(0, Math.round(sumCentsPerDay * days));
|
|
1513
|
-
resv.total = recomputedTotalCents / 100;
|
|
1514
|
-
}
|
|
1515
|
-
} catch (e) {}
|
|
1516
|
-
|
|
1517
|
-
const intent = await helloasso.createCheckoutIntent({
|
|
1518
|
-
env: settings.helloassoEnv,
|
|
1519
|
-
token,
|
|
1520
|
-
organizationSlug: settings.helloassoOrganizationSlug,
|
|
1521
|
-
formType: settings.helloassoFormType,
|
|
1522
|
-
formSlug: autoFormSlugForYear(year),
|
|
1523
|
-
totalAmount: (() => {
|
|
1524
|
-
const cents = (typeof recomputedTotalCents === 'number')
|
|
1525
|
-
? recomputedTotalCents
|
|
1526
|
-
: Math.max(0, Math.round((Number(resv.total) || 0) * 100));
|
|
1527
|
-
if (!cents) console.warn('[calendar-onekite] HelloAsso totalAmount is 0 (regularizationOther)', { rid });
|
|
1528
|
-
return cents;
|
|
1529
|
-
})(),
|
|
1530
|
-
payerEmail: payer && payer.email ? payer.email : '',
|
|
1531
|
-
callbackUrl: normalizeReturnUrl(),
|
|
1532
|
-
webhookUrl: normalizeCallbackUrl(settings.helloassoCallbackUrl),
|
|
1533
|
-
itemName: buildHelloAssoItemName('', resv.itemNames || [], resv.start, resv.end),
|
|
1534
|
-
containsDonation: false,
|
|
1535
|
-
metadata: {
|
|
1536
|
-
reservationId: String(rid),
|
|
1537
|
-
items: resv.itemNames || [],
|
|
1538
|
-
dateRange: `Du ${formatFR(resv.start)} au ${formatFR(resv.end)}`,
|
|
1539
|
-
},
|
|
1540
|
-
});
|
|
1541
|
-
const intentPaymentUrl = intent && (intent.paymentUrl || intent.redirectUrl) ? (intent.paymentUrl || intent.redirectUrl) : (typeof intent === 'string' ? intent : null);
|
|
1542
|
-
const intentCheckoutId = intent && intent.checkoutIntentId ? String(intent.checkoutIntentId) : null;
|
|
1543
|
-
if (intentPaymentUrl) {
|
|
1544
|
-
resv.paymentUrl = intentPaymentUrl;
|
|
1545
|
-
if (intentCheckoutId) resv.checkoutIntentId = intentCheckoutId;
|
|
1423
|
+
const checkoutResult = await buildCheckoutIntent({ r: resv, settings });
|
|
1424
|
+
if (checkoutResult) {
|
|
1425
|
+
resv.paymentUrl = checkoutResult.paymentUrl;
|
|
1426
|
+
if (checkoutResult.checkoutIntentId) resv.checkoutIntentId = checkoutResult.checkoutIntentId;
|
|
1427
|
+
if (checkoutResult.catalogTotalCents !== null) resv.total = checkoutResult.catalogTotalCents / 100;
|
|
1546
1428
|
} else {
|
|
1547
1429
|
console.warn('[calendar-onekite] HelloAsso payment link not created (regularizationOther)', { rid });
|
|
1548
1430
|
}
|
|
1549
1431
|
} catch (e) {
|
|
1550
|
-
console.warn('[calendar-onekite] Failed to create HelloAsso checkout
|
|
1432
|
+
console.warn('[calendar-onekite] Failed to create HelloAsso checkout (regularizationOther)', e && e.message ? e.message : e);
|
|
1551
1433
|
}
|
|
1552
1434
|
}
|
|
1553
1435
|
|
|
@@ -1653,6 +1535,7 @@ api.createReservation = async function (req, res) {
|
|
|
1653
1535
|
pickupLon: '',
|
|
1654
1536
|
mapUrl: '',
|
|
1655
1537
|
paymentUrl: resv.paymentUrl || '',
|
|
1538
|
+
calendarUrl: `${forumBaseUrl()}/calendar`,
|
|
1656
1539
|
validatedBy: creatorUsername || '',
|
|
1657
1540
|
validatedByUrl: creatorUsername ? `${forumBaseUrl()}/user/${encodeURIComponent(String(creatorUsername))}` : '',
|
|
1658
1541
|
...buildCalendarLinks({
|
|
@@ -1773,74 +1656,11 @@ api.approveReservation = async function (req, res) {
|
|
|
1773
1656
|
}
|
|
1774
1657
|
// Create HelloAsso payment link on validation
|
|
1775
1658
|
try {
|
|
1776
|
-
const
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
// Reliable calendar-day count (end is EXCLUSIVE, FullCalendar rule)
|
|
1782
|
-
const days = calendarDaysExclusiveYmd(r.startDate, r.endDate) || 1;
|
|
1783
|
-
|
|
1784
|
-
// Recompute total from HelloAsso catalog to avoid any dependency on hours/DST
|
|
1785
|
-
// and to ensure checkout amount is always consistent.
|
|
1786
|
-
let recomputedTotalCents = null;
|
|
1787
|
-
try {
|
|
1788
|
-
const { items: catalog } = await helloasso.listCatalogItems({
|
|
1789
|
-
env: settings2.helloassoEnv,
|
|
1790
|
-
token,
|
|
1791
|
-
organizationSlug: settings2.helloassoOrganizationSlug,
|
|
1792
|
-
formType: settings2.helloassoFormType,
|
|
1793
|
-
formSlug: autoFormSlugForYear(year),
|
|
1794
|
-
});
|
|
1795
|
-
const byId = new Map((catalog || []).map((it) => [String(it.id), (typeof it.price === 'number' ? it.price : 0)]));
|
|
1796
|
-
const ids = (Array.isArray(r.itemIds) ? r.itemIds : (r.itemId ? [r.itemId] : [])).map(String);
|
|
1797
|
-
const sumCentsPerDay = ids.reduce((acc, id) => acc + (byId.get(String(id)) || 0), 0);
|
|
1798
|
-
if (sumCentsPerDay > 0) {
|
|
1799
|
-
recomputedTotalCents = Math.max(0, Math.round(sumCentsPerDay * days));
|
|
1800
|
-
// Keep stored total in sync (euros) for emails/UX.
|
|
1801
|
-
r.total = recomputedTotalCents / 100;
|
|
1802
|
-
}
|
|
1803
|
-
} catch (e) {
|
|
1804
|
-
// ignore recompute failures; fallback to stored total
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
const intent = await helloasso.createCheckoutIntent({
|
|
1808
|
-
env: settings2.helloassoEnv,
|
|
1809
|
-
token,
|
|
1810
|
-
organizationSlug: settings2.helloassoOrganizationSlug,
|
|
1811
|
-
formType: settings2.helloassoFormType,
|
|
1812
|
-
// Form slug is derived from the year of the reservation start date
|
|
1813
|
-
formSlug: autoFormSlugForYear(year),
|
|
1814
|
-
// r.total is stored as an estimated total in euros; HelloAsso expects cents.
|
|
1815
|
-
totalAmount: (() => {
|
|
1816
|
-
const cents = (typeof recomputedTotalCents === 'number')
|
|
1817
|
-
? recomputedTotalCents
|
|
1818
|
-
: Math.max(0, Math.round((Number(r.total) || 0) * 100));
|
|
1819
|
-
if (!cents) {
|
|
1820
|
-
console.warn('[calendar-onekite] HelloAsso totalAmount is 0 (approve API)', { rid, total: r.total });
|
|
1821
|
-
}
|
|
1822
|
-
return cents;
|
|
1823
|
-
})(),
|
|
1824
|
-
payerEmail: payer && payer.email ? payer.email : '',
|
|
1825
|
-
// By default, point to the forum base url so the webhook hits this NodeBB instance.
|
|
1826
|
-
// Can be overridden via ACP setting `helloassoCallbackUrl`.
|
|
1827
|
-
callbackUrl: normalizeReturnUrl(),
|
|
1828
|
-
webhookUrl: normalizeCallbackUrl(settings2.helloassoCallbackUrl),
|
|
1829
|
-
itemName: buildHelloAssoItemName('', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
|
|
1830
|
-
containsDonation: false,
|
|
1831
|
-
metadata: {
|
|
1832
|
-
reservationId: String(rid),
|
|
1833
|
-
items: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
|
|
1834
|
-
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
1835
|
-
},
|
|
1836
|
-
});
|
|
1837
|
-
const paymentUrl = intent && (intent.paymentUrl || intent.redirectUrl) ? (intent.paymentUrl || intent.redirectUrl) : (typeof intent === 'string' ? intent : null);
|
|
1838
|
-
const checkoutIntentId = intent && intent.checkoutIntentId ? String(intent.checkoutIntentId) : null;
|
|
1839
|
-
if (paymentUrl) {
|
|
1840
|
-
r.paymentUrl = paymentUrl;
|
|
1841
|
-
if (checkoutIntentId) {
|
|
1842
|
-
r.checkoutIntentId = checkoutIntentId;
|
|
1843
|
-
}
|
|
1659
|
+
const checkoutResult = await buildCheckoutIntent({ r, settings });
|
|
1660
|
+
if (checkoutResult) {
|
|
1661
|
+
r.paymentUrl = checkoutResult.paymentUrl;
|
|
1662
|
+
if (checkoutResult.checkoutIntentId) r.checkoutIntentId = checkoutResult.checkoutIntentId;
|
|
1663
|
+
if (checkoutResult.catalogTotalCents !== null) r.total = checkoutResult.catalogTotalCents / 100;
|
|
1844
1664
|
} else {
|
|
1845
1665
|
console.warn('[calendar-onekite] HelloAsso payment link not created (approve API)', { rid });
|
|
1846
1666
|
}
|
|
@@ -1878,8 +1698,8 @@ api.approveReservation = async function (req, res) {
|
|
|
1878
1698
|
uid: requesterUid,
|
|
1879
1699
|
username: requester && requester.username ? requester.username : '',
|
|
1880
1700
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
1881
|
-
|
|
1882
|
-
|
|
1701
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
1702
|
+
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
1883
1703
|
start: formatFR(r.start),
|
|
1884
1704
|
end: formatFR(r.end),
|
|
1885
1705
|
pickupAddress: r.pickupAddress || '',
|
|
@@ -1889,6 +1709,7 @@ api.approveReservation = async function (req, res) {
|
|
|
1889
1709
|
pickupLon: r.pickupLon || '',
|
|
1890
1710
|
mapUrl,
|
|
1891
1711
|
paymentUrl: r.paymentUrl || '',
|
|
1712
|
+
calendarUrl: `${forumBaseUrl()}/calendar`,
|
|
1892
1713
|
...buildCalendarLinks({
|
|
1893
1714
|
type: 'reservation',
|
|
1894
1715
|
id: String(r.rid),
|
|
@@ -1953,8 +1774,8 @@ api.refuseReservation = async function (req, res) {
|
|
|
1953
1774
|
uid: requesterUid2,
|
|
1954
1775
|
username: requester && requester.username ? requester.username : '',
|
|
1955
1776
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
1956
|
-
|
|
1957
|
-
|
|
1777
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
1778
|
+
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
1958
1779
|
start: formatFR(r.start),
|
|
1959
1780
|
end: formatFR(r.end),
|
|
1960
1781
|
refusedReason: r.refusedReason || '',
|
package/lib/checkout.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared checkout intent builder for HelloAsso reservations.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from api.js and admin.js to eliminate the four identical copies
|
|
7
|
+
* of the same pattern:
|
|
8
|
+
* - api.approveReservation
|
|
9
|
+
* - api.getFreshPaymentUrl
|
|
10
|
+
* - api.createReservation (regularizationOther path)
|
|
11
|
+
* - admin.approveReservation
|
|
12
|
+
*
|
|
13
|
+
* Does NOT persist anything — callers are responsible for saving the reservation.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const user = require.main.require('./src/user');
|
|
17
|
+
const helloasso = require('./helloasso');
|
|
18
|
+
const {
|
|
19
|
+
yearFromTs,
|
|
20
|
+
autoFormSlugForYear,
|
|
21
|
+
calendarDaysExclusiveYmd,
|
|
22
|
+
buildHelloAssoItemName,
|
|
23
|
+
normalizeCallbackUrl,
|
|
24
|
+
normalizeReturnUrl,
|
|
25
|
+
formatFR,
|
|
26
|
+
} = require('./shared');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a HelloAsso checkout intent for a reservation.
|
|
30
|
+
*
|
|
31
|
+
* @param {object} opts
|
|
32
|
+
* @param {object} opts.r Reservation object (not mutated here)
|
|
33
|
+
* @param {object} opts.settings Plugin settings (from getSettings())
|
|
34
|
+
* @returns {Promise<{
|
|
35
|
+
* paymentUrl: string,
|
|
36
|
+
* checkoutIntentId: string | null,
|
|
37
|
+
* totalCents: number,
|
|
38
|
+
* catalogTotalCents: number | null,
|
|
39
|
+
* } | null>}
|
|
40
|
+
* null when: token unavailable, amount is 0, or intent creation failed.
|
|
41
|
+
* catalogTotalCents is non-null only when the catalog lookup succeeded with
|
|
42
|
+
* positive prices; callers should update r.total only in that case.
|
|
43
|
+
*/
|
|
44
|
+
async function buildCheckoutIntent({ r, settings }) {
|
|
45
|
+
const env = settings.helloassoEnv || 'prod';
|
|
46
|
+
|
|
47
|
+
const token = await helloasso.getAccessToken({
|
|
48
|
+
env,
|
|
49
|
+
clientId: settings.helloassoClientId,
|
|
50
|
+
clientSecret: settings.helloassoClientSecret,
|
|
51
|
+
});
|
|
52
|
+
if (!token) return null;
|
|
53
|
+
|
|
54
|
+
const payerData = await user.getUserFields(r.uid, ['email']).catch(() => null);
|
|
55
|
+
const payerEmail = (payerData && payerData.email) ? String(payerData.email) : '';
|
|
56
|
+
|
|
57
|
+
const year = yearFromTs(Number(r.start));
|
|
58
|
+
const formSlug = autoFormSlugForYear(year);
|
|
59
|
+
const itemIds = (Array.isArray(r.itemIds) ? r.itemIds : (r.itemId ? [r.itemId] : [])).map(String);
|
|
60
|
+
const itemNames = Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : []);
|
|
61
|
+
const days = calendarDaysExclusiveYmd(r.startDate, r.endDate) || 1;
|
|
62
|
+
|
|
63
|
+
// Recompute total from HelloAsso catalog to always use up-to-date prices.
|
|
64
|
+
let catalogTotalCents = null;
|
|
65
|
+
try {
|
|
66
|
+
const { items: catalog } = await helloasso.listCatalogItems({
|
|
67
|
+
env,
|
|
68
|
+
token,
|
|
69
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
70
|
+
formType: settings.helloassoFormType,
|
|
71
|
+
formSlug,
|
|
72
|
+
});
|
|
73
|
+
const byId = new Map(
|
|
74
|
+
(catalog || []).map((it) => [String(it.id), (typeof it.price === 'number' ? it.price : 0)])
|
|
75
|
+
);
|
|
76
|
+
const sumCentsPerDay = itemIds.reduce((acc, id) => acc + (byId.get(id) || 0), 0);
|
|
77
|
+
if (sumCentsPerDay > 0) {
|
|
78
|
+
catalogTotalCents = Math.max(0, Math.round(sumCentsPerDay * days));
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// Catalog unavailable — fall back to stored total below.
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const totalCents = (catalogTotalCents !== null)
|
|
85
|
+
? catalogTotalCents
|
|
86
|
+
: Math.max(0, Math.round((Number(r.total) || 0) * 100));
|
|
87
|
+
|
|
88
|
+
if (!totalCents) {
|
|
89
|
+
console.warn('[calendar-onekite] checkout intent skipped: totalAmount is 0', { rid: r.rid });
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const intent = await helloasso.createCheckoutIntent({
|
|
94
|
+
env,
|
|
95
|
+
token,
|
|
96
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
97
|
+
formType: settings.helloassoFormType,
|
|
98
|
+
formSlug,
|
|
99
|
+
totalAmount: totalCents,
|
|
100
|
+
payerEmail,
|
|
101
|
+
callbackUrl: normalizeReturnUrl(),
|
|
102
|
+
webhookUrl: normalizeCallbackUrl(settings.helloassoCallbackUrl),
|
|
103
|
+
itemName: buildHelloAssoItemName('Réservation matériel Onekite', itemNames, r.start, r.end),
|
|
104
|
+
containsDonation: false,
|
|
105
|
+
metadata: {
|
|
106
|
+
reservationId: String(r.rid),
|
|
107
|
+
items: itemNames.filter(Boolean),
|
|
108
|
+
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const paymentUrl = (intent && (intent.paymentUrl || intent.redirectUrl))
|
|
113
|
+
? (intent.paymentUrl || intent.redirectUrl)
|
|
114
|
+
: (typeof intent === 'string' ? intent : null);
|
|
115
|
+
|
|
116
|
+
if (!paymentUrl) return null;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
paymentUrl,
|
|
120
|
+
checkoutIntentId: (intent && intent.checkoutIntentId) ? String(intent.checkoutIntentId) : null,
|
|
121
|
+
totalCents,
|
|
122
|
+
catalogTotalCents,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { buildCheckoutIntent };
|
package/lib/helloasso.js
CHANGED
|
@@ -307,15 +307,6 @@ async function createCheckoutIntent({ env, token, organizationSlug, formType, fo
|
|
|
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
310
|
return { paymentUrl: redirectUrl, checkoutIntentId: (json.id || json.checkoutIntentId || null), raw: json };
|
|
320
311
|
}
|
|
321
312
|
// Log the error payload to help diagnose configuration issues (slug, env, urls, amount, etc.)
|
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/package.json
CHANGED
package/public/admin.js
CHANGED
|
@@ -572,6 +572,22 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
572
572
|
: '';
|
|
573
573
|
const itemNames = Array.isArray(r.itemNames) && r.itemNames.length ? r.itemNames : [r.itemName || r.itemId].filter(Boolean);
|
|
574
574
|
const itemsHtml = `<ul style="margin: 0 0 10px 18px;">${itemNames.map(n => `<li>${escapeHtml(String(n))}</li>`).join('')}</ul>`;
|
|
575
|
+
|
|
576
|
+
const isAwaitingPayment = r.status === 'awaiting_payment';
|
|
577
|
+
const statusBadge = isAwaitingPayment
|
|
578
|
+
? '<span class="badge bg-warning text-dark ms-1" style="font-size:10px;">En attente de paiement</span>'
|
|
579
|
+
: '<span class="badge bg-secondary ms-1" style="font-size:10px;">En attente de validation</span>';
|
|
580
|
+
|
|
581
|
+
// Buttons differ per status:
|
|
582
|
+
// - pending → "Valider" + "Refuser"
|
|
583
|
+
// - awaiting_payment → "Marquer payée" + "Renouveler lien" + "Refuser"
|
|
584
|
+
const actionButtons = isAwaitingPayment
|
|
585
|
+
? `<button type="button" class="btn btn-outline-danger btn-sm" data-action="refuse" data-rid="${escapeHtml(String(r.rid || ''))}">Refuser</button>
|
|
586
|
+
<button type="button" class="btn btn-outline-primary btn-sm" data-action="approve" data-rid="${escapeHtml(String(r.rid || ''))}">Renouveler lien</button>
|
|
587
|
+
<button type="button" class="btn btn-success btn-sm" data-action="mark-paid" data-rid="${escapeHtml(String(r.rid || ''))}">Marquer payée</button>`
|
|
588
|
+
: `<button type="button" class="btn btn-outline-danger btn-sm" data-action="refuse" data-rid="${escapeHtml(String(r.rid || ''))}">Refuser</button>
|
|
589
|
+
<button type="button" class="btn btn-success btn-sm" data-action="approve" data-rid="${escapeHtml(String(r.rid || ''))}">Valider</button>`;
|
|
590
|
+
|
|
575
591
|
const div = document.createElement('div');
|
|
576
592
|
div.className = 'list-group-item onekite-pending-row';
|
|
577
593
|
div.innerHTML = `
|
|
@@ -581,16 +597,15 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
581
597
|
<input type="checkbox" class="form-check-input onekite-pending-select" data-rid="${escapeHtml(String(r.rid || ''))}" />
|
|
582
598
|
</div>
|
|
583
599
|
<div style="min-width:0;">
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
600
|
+
<div><strong>${itemsHtml || escapeHtml(r.itemName || '')}</strong>${statusBadge}</div>
|
|
601
|
+
<div class="text-muted" style="font-size: 12px;">Créée: ${escapeHtml(created)}</div>
|
|
602
|
+
${userLink ? `<div class="text-muted" style="font-size: 12px;">Réservée par: ${userLink}</div>` : ''}
|
|
603
|
+
<div class="text-muted" style="font-size: 12px;">Période: ${escapeHtml(new Date(parseInt(r.start, 10)).toLocaleDateString('fr-FR'))} → ${escapeHtml(new Date(parseInt(r.end, 10)).toLocaleDateString('fr-FR'))}</div>
|
|
588
604
|
</div>
|
|
589
605
|
</div>
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
<button type="button" class="btn btn-success btn-sm" data-action="approve" data-rid="${escapeHtml(String(r.rid || ''))}">Valider</button>
|
|
606
|
+
<!-- IMPORTANT: type="button" to avoid submitting the settings form and resetting ACP tabs -->
|
|
607
|
+
<div class="d-flex gap-2 flex-wrap justify-content-end">
|
|
608
|
+
${actionButtons}
|
|
594
609
|
</div>
|
|
595
610
|
</div>
|
|
596
611
|
`;
|
|
@@ -862,7 +877,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
862
877
|
const rid = btn.getAttribute('data-rid');
|
|
863
878
|
|
|
864
879
|
// Prevent accidental double-open of modals on mobile/trackpad double taps
|
|
865
|
-
if (rid && (action === 'approve' || action === 'refuse')) {
|
|
880
|
+
if (rid && (action === 'approve' || action === 'refuse' || action === 'mark-paid')) {
|
|
866
881
|
const openKey = `open:${action}:${rid}`;
|
|
867
882
|
if (actionLocks.has(openKey)) return;
|
|
868
883
|
actionLocks.add(openKey);
|
|
@@ -1044,9 +1059,29 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
1044
1059
|
|
|
1045
1060
|
// Remove the row immediately on success for a snappier UX
|
|
1046
1061
|
const rowEl = btn.closest('tr') || btn.closest('.onekite-pending-row');
|
|
1047
|
-
const rowBtns = rowEl ? Array.from(rowEl.querySelectorAll('button[data-action
|
|
1062
|
+
const rowBtns = rowEl ? Array.from(rowEl.querySelectorAll('button[data-action]')) : [btn];
|
|
1048
1063
|
|
|
1049
1064
|
try {
|
|
1065
|
+
if (action === 'mark-paid') {
|
|
1066
|
+
bootbox.confirm(
|
|
1067
|
+
`Marquer la réservation <strong>${escapeHtml(rid)}</strong> comme payée manuellement ?<br><small class="text-muted">Un email de confirmation sera envoyé à l'adhérent.</small>`,
|
|
1068
|
+
async (ok) => {
|
|
1069
|
+
if (!ok) return;
|
|
1070
|
+
await withLock(`mark-paid:${rid}`, rowBtns, async () => {
|
|
1071
|
+
try {
|
|
1072
|
+
await markPaid(rid, { note: 'Paiement validé manuellement depuis ACP' });
|
|
1073
|
+
if (rowEl && rowEl.parentNode) rowEl.parentNode.removeChild(rowEl);
|
|
1074
|
+
showAlert('success', `Réservation ${rid} marquée comme payée.`);
|
|
1075
|
+
await refreshPending();
|
|
1076
|
+
} catch (e) {
|
|
1077
|
+
showAlert('error', `Impossible de marquer comme payée : ${e && e.message ? e.message : e}`);
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1050
1085
|
if (action === 'refuse') {
|
|
1051
1086
|
const html = `
|
|
1052
1087
|
<div class="mb-3">
|
|
@@ -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 -->
|