nodebb-plugin-calendar-onekite 11.1.5 → 11.1.6

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.
Files changed (2) hide show
  1. package/lib/helloasso.js +145 -37
  2. package/package.json +1 -1
package/lib/helloasso.js CHANGED
@@ -1,62 +1,170 @@
1
1
  'use strict';
2
2
 
3
- // Minimal HelloAsso wrapper placeholder.
4
- // Returns null payment url if not configured; does not throw to keep plugin stable.
3
+ /**
4
+ * HelloAsso API helper (NodeBB v4 compatible, no external deps).
5
+ * Uses native https to avoid optional dependencies.
6
+ */
5
7
 
6
- const request = require('request-promise-native');
8
+ const https = require('https');
9
+ const { URL } = require('url');
7
10
  const Settings = require('./settings');
8
11
 
9
12
  function baseUrl(env) {
10
13
  return env === 'production' ? 'https://api.helloasso.com' : 'https://api.helloasso-sandbox.com';
11
14
  }
12
15
 
16
+ function httpsRequest(method, urlString, headers = {}, body = undefined) {
17
+ return new Promise((resolve, reject) => {
18
+ const url = new URL(urlString);
19
+
20
+ const opts = {
21
+ method,
22
+ protocol: url.protocol,
23
+ hostname: url.hostname,
24
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
25
+ path: url.pathname + (url.search || ''),
26
+ headers: headers || {},
27
+ };
28
+
29
+ const req = https.request(opts, (res) => {
30
+ let data = '';
31
+ res.setEncoding('utf8');
32
+ res.on('data', (chunk) => { data += chunk; });
33
+ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: data }));
34
+ });
35
+
36
+ req.on('error', reject);
37
+
38
+ if (body !== undefined && body !== null) {
39
+ req.write(body);
40
+ }
41
+ req.end();
42
+ });
43
+ }
44
+
13
45
  async function getAccessToken() {
14
46
  const s = await Settings.get();
15
- if (!s.helloassoClientId || !s.helloassoClientSecret) return null;
16
- const url = baseUrl(s.helloassoEnv) + '/oauth2/token';
47
+ if (!s.helloassoClientId || !s.helloassoClientSecret) {
48
+ return null;
49
+ }
50
+
51
+ const tokenUrl = `${baseUrl(s.helloassoEnv)}/oauth2/token`;
52
+ const form = new URLSearchParams({
53
+ grant_type: 'client_credentials',
54
+ client_id: String(s.helloassoClientId),
55
+ client_secret: String(s.helloassoClientSecret),
56
+ }).toString();
57
+
58
+ const res = await httpsRequest('POST', tokenUrl, {
59
+ 'Content-Type': 'application/x-www-form-urlencoded',
60
+ 'Content-Length': Buffer.byteLength(form),
61
+ }, form);
62
+
63
+ if (res.status < 200 || res.status >= 300) {
64
+ // Keep plugin stable: return null token and let caller show a helpful message
65
+ return null;
66
+ }
67
+
17
68
  try {
18
- const resp = await request({
19
- method: 'POST',
20
- url,
21
- form: {
22
- grant_type: 'client_credentials',
23
- client_id: s.helloassoClientId,
24
- client_secret: s.helloassoClientSecret,
25
- },
26
- json: true,
27
- });
28
- return resp && resp.access_token;
29
- } catch (e) {
69
+ const json = JSON.parse(res.body || '{}');
70
+ return json.access_token || null;
71
+ } catch {
30
72
  return null;
31
73
  }
32
74
  }
33
75
 
34
- async function createCheckoutIntent(amountCents, metadata) {
35
- // This is a simplified implementation; returns null if token missing.
76
+ async function apiJson(method, path, { query, body } = {}) {
36
77
  const s = await Settings.get();
37
78
  const token = await getAccessToken();
38
- if (!token || !s.helloassoOrganizationSlug || !s.helloassoFormType || !s.helloassoFormSlug) return null;
79
+ if (!token) {
80
+ return null;
81
+ }
82
+
83
+ const url = new URL(`${baseUrl(s.helloassoEnv)}${path}`);
84
+ if (query && typeof query === 'object') {
85
+ Object.entries(query).forEach(([k, v]) => {
86
+ if (v !== undefined && v !== null && v !== '') {
87
+ url.searchParams.set(k, String(v));
88
+ }
89
+ });
90
+ }
39
91
 
40
- const url = baseUrl(s.helloassoEnv) + `/v5/organizations/${encodeURIComponent(s.helloassoOrganizationSlug)}/forms/${encodeURIComponent(s.helloassoFormType)}/${encodeURIComponent(s.helloassoFormSlug)}/checkout-intents`;
92
+ const payload = body ? JSON.stringify(body) : undefined;
93
+ const headers = {
94
+ Authorization: `Bearer ${token}`,
95
+ Accept: 'application/json',
96
+ };
97
+ if (payload) {
98
+ headers['Content-Type'] = 'application/json';
99
+ headers['Content-Length'] = Buffer.byteLength(payload);
100
+ }
101
+
102
+ const res = await httpsRequest(method, url.toString(), headers, payload);
41
103
 
104
+ if (res.status < 200 || res.status >= 300) {
105
+ return null;
106
+ }
42
107
  try {
43
- const resp = await request({
44
- method: 'POST',
45
- url,
46
- headers: { Authorization: `Bearer ${token}` },
47
- body: {
48
- // NOTE: Real payload depends on HelloAsso form; this is a safe placeholder.
49
- // If it fails, admin can still manually handle payment.
50
- totalAmount: Math.round(amountCents) / 100,
51
- metadata: metadata || {},
52
- returnUrl: metadata && metadata.returnUrl ? metadata.returnUrl : undefined,
53
- },
54
- json: true,
55
- });
56
- return resp && (resp.redirectUrl || resp.checkoutUrl || resp.url);
57
- } catch (e) {
108
+ return JSON.parse(res.body || '{}');
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ async function listItems() {
115
+ const s = await Settings.get();
116
+ if (!s.helloassoOrganizationSlug || !s.helloassoFormType || !s.helloassoFormSlug) {
117
+ return [];
118
+ }
119
+
120
+ // Endpoint used by HelloAsso to list form items
121
+ const data = await apiJson('GET', `/v5/organizations/${encodeURIComponent(s.helloassoOrganizationSlug)}/${encodeURIComponent(s.helloassoFormType)}/${encodeURIComponent(s.helloassoFormSlug)}/items`);
122
+ if (!data) {
123
+ return [];
124
+ }
125
+
126
+ // Normalize (HelloAsso returns {items:[...]} in many responses; be defensive)
127
+ const items = Array.isArray(data.items) ? data.items : (Array.isArray(data.data) ? data.data : []);
128
+ return items.map((it) => ({
129
+ id: it.id ?? it.itemId ?? it.slug ?? it.name,
130
+ name: it.name ?? it.label ?? 'Item',
131
+ price: it.amount ?? it.price ?? it.unitPrice ?? 0,
132
+ raw: it,
133
+ }));
134
+ }
135
+
136
+ async function createCheckoutIntent({ itemId, quantity = 1, payer = {} }) {
137
+ const s = await Settings.get();
138
+ if (!s.helloassoOrganizationSlug) {
139
+ return null;
140
+ }
141
+
142
+ // Minimal payload; depending on your HelloAsso form, you might need to add more fields.
143
+ const payload = {
144
+ totalAmount: undefined, // let HelloAsso compute if supported
145
+ initialAmount: undefined,
146
+ items: [{
147
+ itemId,
148
+ quantity,
149
+ }],
150
+ payer: {
151
+ firstName: payer.firstName || '',
152
+ lastName: payer.lastName || '',
153
+ email: payer.email || '',
154
+ },
155
+ backUrl: s.helloassoBackUrl || '',
156
+ errorUrl: s.helloassoErrorUrl || '',
157
+ returnUrl: s.helloassoReturnUrl || '',
158
+ };
159
+
160
+ const data = await apiJson('POST', `/v5/organizations/${encodeURIComponent(s.helloassoOrganizationSlug)}/checkout-intents`, { body: payload });
161
+ if (!data) {
58
162
  return null;
59
163
  }
164
+ return data.redirectUrl || data.checkoutUrl || data.url || null;
60
165
  }
61
166
 
62
- module.exports = { createCheckoutIntent };
167
+ module.exports = {
168
+ listItems,
169
+ createCheckoutIntent,
170
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "11.1.5",
3
+ "version": "11.1.6",
4
4
  "description": "Reservation calendar with validation workflow and HelloAsso payment (OneKite)",
5
5
  "main": "library.js",
6
6
  "author": "OneKite",