nodebb-plugin-calendar-onekite 11.1.21 → 11.1.22

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 CHANGED
@@ -176,7 +176,10 @@ admin.debugHelloAsso = async function (req, res) {
176
176
  ok: true,
177
177
  settings: safeSettings,
178
178
  token: { ok: false },
179
- items: { ok: false, count: 0, sample: [] },
179
+ // Catalog = what you actually want for a shop (available products/material)
180
+ catalog: { ok: false, count: 0, sample: [], keys: [] },
181
+ // Sold items = items present in orders (can be 0 if no sales yet)
182
+ soldItems: { ok: false, count: 0, sample: [] },
180
183
  };
181
184
 
182
185
  try {
@@ -191,6 +194,30 @@ admin.debugHelloAsso = async function (req, res) {
191
194
  }
192
195
  out.token = { ok: true };
193
196
 
197
+ // Catalog items (via /public)
198
+ try {
199
+ const { publicForm, items } = await helloasso.listCatalogItems({
200
+ env,
201
+ token,
202
+ organizationSlug: settings.helloassoOrganizationSlug,
203
+ formType: settings.helloassoFormType,
204
+ formSlug: settings.helloassoFormSlug,
205
+ });
206
+
207
+ const arr = Array.isArray(items) ? items : [];
208
+ out.catalog.ok = true;
209
+ out.catalog.count = arr.length;
210
+ out.catalog.keys = publicForm && typeof publicForm === 'object' ? Object.keys(publicForm) : [];
211
+ out.catalog.sample = arr.slice(0, 10).map((it) => ({
212
+ id: it.id,
213
+ name: it.name,
214
+ price: it.price ?? null,
215
+ }));
216
+ } catch (e) {
217
+ out.catalog = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [], keys: [] };
218
+ }
219
+
220
+ // Sold items
194
221
  try {
195
222
  const items = await helloasso.listItems({
196
223
  env,
@@ -200,15 +227,15 @@ admin.debugHelloAsso = async function (req, res) {
200
227
  formSlug: settings.helloassoFormSlug,
201
228
  });
202
229
  const arr = Array.isArray(items) ? items : [];
203
- out.items.ok = true;
204
- out.items.count = arr.length;
205
- out.items.sample = arr.slice(0, 10).map((it) => ({
230
+ out.soldItems.ok = true;
231
+ out.soldItems.count = arr.length;
232
+ out.soldItems.sample = arr.slice(0, 10).map((it) => ({
206
233
  id: it.id || it.itemId || it.reference || it.name,
207
234
  name: it.name || it.label || it.itemName,
208
235
  price: it.price || it.amount || it.unitPrice || null,
209
236
  }));
210
237
  } catch (e) {
211
- out.items = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [] };
238
+ out.soldItems = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [] };
212
239
  }
213
240
 
214
241
  return res.json(out);
package/lib/api.js CHANGED
@@ -72,7 +72,9 @@ api.getItems = async function (req, res) {
72
72
  return res.json([]);
73
73
  }
74
74
 
75
- const items = await helloasso.listItems({
75
+ // Important: the /items endpoint on HelloAsso lists *sold items*.
76
+ // For a shop catalog, use the /public form endpoint and extract the catalog.
77
+ const { items: catalog } = await helloasso.listCatalogItems({
76
78
  env,
77
79
  token,
78
80
  organizationSlug: settings.helloassoOrganizationSlug,
@@ -80,11 +82,10 @@ api.getItems = async function (req, res) {
80
82
  formSlug: settings.helloassoFormSlug,
81
83
  });
82
84
 
83
- // Normalize minimal fields for client
84
- const normalized = (items || []).map((it) => ({
85
- id: it.id || it.itemId || it.reference || it.name,
86
- name: it.name || it.label || `Item ${it.id || ''}`,
87
- price: it.price || it.amount || it.unitPrice || 0,
85
+ const normalized = (catalog || []).map((it) => ({
86
+ id: it.id,
87
+ name: it.name,
88
+ price: typeof it.price === 'number' ? it.price : 0,
88
89
  })).filter(it => it.id && it.name);
89
90
 
90
91
  res.json(normalized);
package/lib/helloasso.js CHANGED
@@ -95,18 +95,89 @@ async function getAccessToken({ env, clientId, clientSecret }) {
95
95
 
96
96
  async function listItems({ env, token, organizationSlug, formType, formSlug }) {
97
97
  if (!token || !organizationSlug || !formType || !formSlug) return [];
98
- const url = `${baseUrl(env)}/v5/organizations/${encodeURIComponent(organizationSlug)}/${encodeURIComponent(formType)}/${encodeURIComponent(formSlug)}/items?pageIndex=1&pageSize=200`;
98
+ // This endpoint returns *sold items* (i.e., items present in orders). If your shop has
99
+ // no sales yet, it will legitimately return an empty list.
100
+ const url = `${baseUrl(env)}/v5/organizations/${encodeURIComponent(organizationSlug)}/forms/${encodeURIComponent(formType)}/${encodeURIComponent(formSlug)}/items?pageIndex=1&pageSize=200`;
99
101
  const { status, json } = await requestJson('GET', url, { Authorization: `Bearer ${token}` });
100
102
  if (status >= 200 && status < 300 && json) {
101
- // HelloAsso returns { data: [...] } in many endpoints
102
- return json.data || json.items || json || [];
103
+ return json.data || json.items || [];
103
104
  }
104
105
  return [];
105
106
  }
106
107
 
107
- async function createCheckoutIntent({ env, token, organizationSlug, formType, formSlug, totalAmount, payerEmail }) {
108
+ async function getFormPublic({ env, token, organizationSlug, formType, formSlug }) {
108
109
  if (!token || !organizationSlug || !formType || !formSlug) return null;
109
- const url = `${baseUrl(env)}/v5/organizations/${encodeURIComponent(organizationSlug)}/${encodeURIComponent(formType)}/${encodeURIComponent(formSlug)}/checkout-intents`;
110
+ // Public form details contains extraOptions/customFields and (for Shop) usually the catalog structure.
111
+ const url = `${baseUrl(env)}/v5/organizations/${encodeURIComponent(organizationSlug)}/forms/${encodeURIComponent(formType)}/${encodeURIComponent(formSlug)}/public`;
112
+ const { status, json } = await requestJson('GET', url, { Authorization: `Bearer ${token}` });
113
+ if (status >= 200 && status < 300) {
114
+ return json || null;
115
+ }
116
+ return null;
117
+ }
118
+
119
+ function extractCatalogItems(publicFormJson) {
120
+ if (!publicFormJson || typeof publicFormJson !== 'object') return [];
121
+
122
+ // Try a few common shapes used in HelloAsso "public" form responses.
123
+ const candidates = [];
124
+ const pushArr = (arr) => {
125
+ if (Array.isArray(arr)) candidates.push(...arr);
126
+ };
127
+
128
+ pushArr(publicFormJson.items);
129
+ pushArr(publicFormJson.tiers);
130
+ pushArr(publicFormJson.products);
131
+ pushArr(publicFormJson.data);
132
+ if (publicFormJson.form) {
133
+ pushArr(publicFormJson.form.items);
134
+ pushArr(publicFormJson.form.tiers);
135
+ pushArr(publicFormJson.form.products);
136
+ }
137
+
138
+ // Some responses nest in "campaign" or "publicForm".
139
+ if (publicFormJson.publicForm) {
140
+ pushArr(publicFormJson.publicForm.items);
141
+ pushArr(publicFormJson.publicForm.tiers);
142
+ pushArr(publicFormJson.publicForm.products);
143
+ }
144
+ if (publicFormJson.campaign) {
145
+ pushArr(publicFormJson.campaign.items);
146
+ pushArr(publicFormJson.campaign.tiers);
147
+ pushArr(publicFormJson.campaign.products);
148
+ }
149
+
150
+ // Normalize to { id, name, price }
151
+ return candidates
152
+ .map((it) => {
153
+ if (!it || typeof it !== 'object') return null;
154
+ const id = it.id ?? it.itemId ?? it.tierId;
155
+ const name = it.name ?? it.label ?? it.title;
156
+ const price =
157
+ (it.amount && (it.amount.total ?? it.amount.value)) ??
158
+ it.price ??
159
+ it.unitPrice ??
160
+ it.totalAmount ??
161
+ it.initialAmount;
162
+ if (!id || !name) return null;
163
+ return { id, name, price: typeof price === 'number' ? price : 0, raw: it };
164
+ })
165
+ .filter(Boolean);
166
+ }
167
+
168
+ async function listCatalogItems({ env, token, organizationSlug, formType, formSlug }) {
169
+ const publicForm = await getFormPublic({ env, token, organizationSlug, formType, formSlug });
170
+ const extracted = extractCatalogItems(publicForm);
171
+ return {
172
+ publicForm,
173
+ items: extracted.map(({ id, name, price, raw }) => ({ id, name, price, raw })),
174
+ };
175
+ }
176
+
177
+ async function createCheckoutIntent({ env, token, organizationSlug, formType, formSlug, totalAmount, payerEmail }) {
178
+ if (!token || !organizationSlug) return null;
179
+ // Checkout intents are created at organization level.
180
+ const url = `${baseUrl(env)}/v5/organizations/${encodeURIComponent(organizationSlug)}/checkout-intents`;
110
181
  const payload = {
111
182
  totalAmount: totalAmount,
112
183
  initialAmount: totalAmount,
@@ -125,5 +196,8 @@ async function createCheckoutIntent({ env, token, organizationSlug, formType, fo
125
196
  module.exports = {
126
197
  getAccessToken,
127
198
  listItems,
199
+ getFormPublic,
200
+ extractCatalogItems,
201
+ listCatalogItems,
128
202
  createCheckoutIntent,
129
203
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "11.1.21",
3
+ "version": "11.1.22",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",