@blackcode_sa/metaestetics-api 1.15.17-staging.0 → 1.15.17-staging.10

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.15.17-staging.0",
4
+ "version": "1.15.17-staging.10",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -59,7 +59,7 @@ export class ClinicAggregationService {
59
59
  try {
60
60
  await groupRef.update({
61
61
  clinicsInfo: admin.firestore.FieldValue.arrayUnion(clinicInfo),
62
- clinicIds: admin.firestore.FieldValue.arrayUnion(clinicId),
62
+ clinics: admin.firestore.FieldValue.arrayUnion(clinicId),
63
63
  updatedAt: admin.firestore.FieldValue.serverTimestamp(),
64
64
  });
65
65
 
@@ -12,6 +12,9 @@ export {
12
12
  PERMISSION_KEYS,
13
13
  TIER_CONFIG,
14
14
  DEFAULT_ROLE_PERMISSIONS,
15
+ DEFAULT_PLAN_CONFIG,
16
+ PERMISSION_LABELS,
17
+ PERMISSION_CATEGORIES,
15
18
  resolveEffectiveTier,
16
19
  } from "./tiers.config";
17
20
  export type { PermissionKey } from "./tiers.config";
@@ -1,21 +1,37 @@
1
1
  import { ClinicRole } from '../types/clinic/rbac.types';
2
2
  import type { TierConfig } from '../types/clinic/rbac.types';
3
+ import type { PlanConfigDocument } from '../types/system/planConfig.types';
3
4
 
4
5
  /**
5
6
  * Permission keys used for role-based access control.
6
7
  * Every feature is available on every tier — these only control
7
- * what a given role (owner/admin/doctor/etc.) can do.
8
+ * what a given role (owner/admin/receptionist/assistant) can do.
9
+ * New roles can be added by extending the ClinicRole enum and adding
10
+ * a corresponding entry here.
11
+ *
12
+ * v2 (2026-03-31): Expanded from 23 to 40 permissions.
13
+ * - providers.manage → providers.create, providers.invite, providers.edit
14
+ * - staff.manage → staff.view, staff.edit, staff.invite, staff.delete, staff.viewTokens, staff.deleteTokens
15
+ * - Added: clinic.create, calendar.addEvent, calendar.editEvent, calendar.deleteEvent,
16
+ * appointments.reschedule, patients.viewDetails, patients.create, patients.manageTokens, billing.view
8
17
  */
9
18
  export const PERMISSION_KEYS = {
10
- // Listing & profile
19
+ // Clinic
11
20
  'clinic.view': true,
12
21
  'clinic.edit': true,
22
+ 'clinic.create': true,
13
23
  'reviews.view': true,
14
24
 
15
- // Calendar & Appointments
25
+ // Calendar
16
26
  'calendar.view': true,
27
+ 'calendar.addEvent': true,
28
+ 'calendar.editEvent': true,
29
+ 'calendar.deleteEvent': true,
30
+
31
+ // Appointments
17
32
  'appointments.view': true,
18
33
  'appointments.confirm': true,
34
+ 'appointments.reschedule': true,
19
35
  'appointments.cancel': true,
20
36
 
21
37
  // Messaging
@@ -35,20 +51,31 @@ export const PERMISSION_KEYS = {
35
51
 
36
52
  // Patients
37
53
  'patients.view': true,
54
+ 'patients.viewDetails': true,
55
+ 'patients.create': true,
38
56
  'patients.edit': true,
57
+ 'patients.manageTokens': true,
39
58
 
40
59
  // Providers (doctors, nurses, laser assistants, etc.)
41
60
  'providers.view': true,
42
- 'providers.manage': true,
61
+ 'providers.create': true,
62
+ 'providers.invite': true,
63
+ 'providers.edit': true,
43
64
 
44
65
  // Analytics
45
66
  'analytics.view': true,
46
67
 
47
68
  // Staff management
48
- 'staff.manage': true,
69
+ 'staff.view': true,
70
+ 'staff.edit': true,
71
+ 'staff.invite': true,
72
+ 'staff.delete': true,
73
+ 'staff.viewTokens': true,
74
+ 'staff.deleteTokens': true,
49
75
 
50
76
  // Settings / Admin
51
77
  'settings.manage': true,
78
+ 'billing.view': true,
52
79
  'billing.manage': true,
53
80
  } as const;
54
81
 
@@ -107,10 +134,15 @@ export const DEFAULT_ROLE_PERMISSIONS: Record<ClinicRole, Record<string, boolean
107
134
  [ClinicRole.OWNER]: {
108
135
  'clinic.view': true,
109
136
  'clinic.edit': true,
137
+ 'clinic.create': true,
110
138
  'reviews.view': true,
111
139
  'calendar.view': true,
140
+ 'calendar.addEvent': true,
141
+ 'calendar.editEvent': true,
142
+ 'calendar.deleteEvent': true,
112
143
  'appointments.view': true,
113
144
  'appointments.confirm': true,
145
+ 'appointments.reschedule': true,
114
146
  'appointments.cancel': true,
115
147
  'messaging': true,
116
148
  'procedures.view': true,
@@ -122,21 +154,37 @@ export const DEFAULT_ROLE_PERMISSIONS: Record<ClinicRole, Record<string, boolean
122
154
  'resources.edit': true,
123
155
  'resources.delete': true,
124
156
  'patients.view': true,
157
+ 'patients.viewDetails': true,
158
+ 'patients.create': true,
125
159
  'patients.edit': true,
160
+ 'patients.manageTokens': true,
126
161
  'providers.view': true,
127
- 'providers.manage': true,
162
+ 'providers.create': true,
163
+ 'providers.invite': true,
164
+ 'providers.edit': true,
128
165
  'analytics.view': true,
129
- 'staff.manage': true,
166
+ 'staff.view': true,
167
+ 'staff.edit': true,
168
+ 'staff.invite': true,
169
+ 'staff.delete': true,
170
+ 'staff.viewTokens': true,
171
+ 'staff.deleteTokens': true,
130
172
  'settings.manage': true,
173
+ 'billing.view': true,
131
174
  'billing.manage': true,
132
175
  },
133
176
  [ClinicRole.ADMIN]: {
134
177
  'clinic.view': true,
135
178
  'clinic.edit': true,
179
+ 'clinic.create': false,
136
180
  'reviews.view': true,
137
181
  'calendar.view': true,
182
+ 'calendar.addEvent': true,
183
+ 'calendar.editEvent': true,
184
+ 'calendar.deleteEvent': true,
138
185
  'appointments.view': true,
139
186
  'appointments.confirm': true,
187
+ 'appointments.reschedule': true,
140
188
  'appointments.cancel': true,
141
189
  'messaging': true,
142
190
  'procedures.view': true,
@@ -148,47 +196,37 @@ export const DEFAULT_ROLE_PERMISSIONS: Record<ClinicRole, Record<string, boolean
148
196
  'resources.edit': true,
149
197
  'resources.delete': true,
150
198
  'patients.view': true,
199
+ 'patients.viewDetails': true,
200
+ 'patients.create': true,
151
201
  'patients.edit': true,
202
+ 'patients.manageTokens': true,
152
203
  'providers.view': true,
153
- 'providers.manage': true,
204
+ 'providers.create': true,
205
+ 'providers.invite': true,
206
+ 'providers.edit': true,
154
207
  'analytics.view': true,
155
- 'staff.manage': false,
208
+ 'staff.view': true,
209
+ 'staff.edit': false,
210
+ 'staff.invite': false,
211
+ 'staff.delete': false,
212
+ 'staff.viewTokens': false,
213
+ 'staff.deleteTokens': false,
156
214
  'settings.manage': true,
157
- 'billing.manage': false,
158
- },
159
- [ClinicRole.DOCTOR]: {
160
- 'clinic.view': true,
161
- 'clinic.edit': false,
162
- 'reviews.view': true,
163
- 'calendar.view': true,
164
- 'appointments.view': true,
165
- 'appointments.confirm': true,
166
- 'appointments.cancel': true,
167
- 'messaging': true,
168
- 'procedures.view': true,
169
- 'procedures.create': false,
170
- 'procedures.edit': false,
171
- 'procedures.delete': false,
172
- 'resources.view': true,
173
- 'resources.create': false,
174
- 'resources.edit': false,
175
- 'resources.delete': false,
176
- 'patients.view': true,
177
- 'patients.edit': true,
178
- 'providers.view': true,
179
- 'providers.manage': false,
180
- 'analytics.view': true,
181
- 'staff.manage': false,
182
- 'settings.manage': false,
215
+ 'billing.view': true,
183
216
  'billing.manage': false,
184
217
  },
185
218
  [ClinicRole.RECEPTIONIST]: {
186
219
  'clinic.view': true,
187
220
  'clinic.edit': false,
221
+ 'clinic.create': false,
188
222
  'reviews.view': true,
189
223
  'calendar.view': true,
224
+ 'calendar.addEvent': true,
225
+ 'calendar.editEvent': false,
226
+ 'calendar.deleteEvent': false,
190
227
  'appointments.view': true,
191
228
  'appointments.confirm': true,
229
+ 'appointments.reschedule': true,
192
230
  'appointments.cancel': false,
193
231
  'messaging': true,
194
232
  'procedures.view': true,
@@ -200,21 +238,37 @@ export const DEFAULT_ROLE_PERMISSIONS: Record<ClinicRole, Record<string, boolean
200
238
  'resources.edit': false,
201
239
  'resources.delete': false,
202
240
  'patients.view': true,
241
+ 'patients.viewDetails': true,
242
+ 'patients.create': true,
203
243
  'patients.edit': false,
244
+ 'patients.manageTokens': false,
204
245
  'providers.view': true,
205
- 'providers.manage': false,
246
+ 'providers.create': false,
247
+ 'providers.invite': false,
248
+ 'providers.edit': false,
206
249
  'analytics.view': false,
207
- 'staff.manage': false,
250
+ 'staff.view': false,
251
+ 'staff.edit': false,
252
+ 'staff.invite': false,
253
+ 'staff.delete': false,
254
+ 'staff.viewTokens': false,
255
+ 'staff.deleteTokens': false,
208
256
  'settings.manage': false,
257
+ 'billing.view': false,
209
258
  'billing.manage': false,
210
259
  },
211
260
  [ClinicRole.ASSISTANT]: {
212
261
  'clinic.view': true,
213
262
  'clinic.edit': false,
263
+ 'clinic.create': false,
214
264
  'reviews.view': true,
215
265
  'calendar.view': true,
266
+ 'calendar.addEvent': false,
267
+ 'calendar.editEvent': false,
268
+ 'calendar.deleteEvent': false,
216
269
  'appointments.view': true,
217
270
  'appointments.confirm': false,
271
+ 'appointments.reschedule': false,
218
272
  'appointments.cancel': false,
219
273
  'messaging': false,
220
274
  'procedures.view': true,
@@ -226,16 +280,181 @@ export const DEFAULT_ROLE_PERMISSIONS: Record<ClinicRole, Record<string, boolean
226
280
  'resources.edit': false,
227
281
  'resources.delete': false,
228
282
  'patients.view': true,
283
+ 'patients.viewDetails': false,
284
+ 'patients.create': false,
229
285
  'patients.edit': false,
286
+ 'patients.manageTokens': false,
230
287
  'providers.view': true,
231
- 'providers.manage': false,
288
+ 'providers.create': false,
289
+ 'providers.invite': false,
290
+ 'providers.edit': false,
232
291
  'analytics.view': false,
233
- 'staff.manage': false,
292
+ 'staff.view': false,
293
+ 'staff.edit': false,
294
+ 'staff.invite': false,
295
+ 'staff.delete': false,
296
+ 'staff.viewTokens': false,
297
+ 'staff.deleteTokens': false,
234
298
  'settings.manage': false,
299
+ 'billing.view': false,
235
300
  'billing.manage': false,
236
301
  },
237
302
  };
238
303
 
304
+ /**
305
+ * Default PlanConfigDocument — used as fallback when Firestore `system/planConfig`
306
+ * doesn't exist or is unavailable. Also used as the seed value.
307
+ *
308
+ * Stripe price IDs reference the SKS Innovation SA sandbox account.
309
+ */
310
+ export const DEFAULT_PLAN_CONFIG: PlanConfigDocument = {
311
+ version: 1,
312
+ updatedAt: null,
313
+ updatedBy: 'system',
314
+
315
+ tiers: {
316
+ free: {
317
+ name: 'Free',
318
+ limits: { maxProvidersPerBranch: 1, maxProceduresPerProvider: 3, maxBranches: 1 },
319
+ },
320
+ connect: {
321
+ name: 'Connect',
322
+ limits: { maxProvidersPerBranch: 3, maxProceduresPerProvider: 10, maxBranches: 1 },
323
+ },
324
+ pro: {
325
+ name: 'Pro',
326
+ limits: { maxProvidersPerBranch: 5, maxProceduresPerProvider: 20, maxBranches: 3 },
327
+ },
328
+ },
329
+
330
+ plans: {
331
+ FREE: {
332
+ priceId: null,
333
+ priceMonthly: 0,
334
+ currency: 'CHF',
335
+ description: 'Get started with basic access',
336
+ stripeProductId: null,
337
+ },
338
+ CONNECT: {
339
+ priceId: 'price_1TD0l7AaFI0fqFWNC8Lg3QJG',
340
+ priceMonthly: 199,
341
+ currency: 'CHF',
342
+ description: 'Consultation tools and integrated booking',
343
+ stripeProductId: null,
344
+ },
345
+ PRO: {
346
+ priceId: 'price_1TD0lEAaFI0fqFWN3tzCrfUb',
347
+ priceMonthly: 449,
348
+ currency: 'CHF',
349
+ description: 'Complete clinic management with CRM and analytics',
350
+ stripeProductId: null,
351
+ },
352
+ },
353
+
354
+ addons: {
355
+ seat: {
356
+ priceId: 'price_1TD0lKAaFI0fqFWNH9pQgTzI',
357
+ pricePerUnit: 39,
358
+ currency: 'CHF',
359
+ allowedTiers: ['connect', 'pro'],
360
+ },
361
+ branch: {
362
+ connect: { priceId: 'price_1TD0lSAaFI0fqFWNjsPvzW1T', pricePerUnit: 119, currency: 'CHF' },
363
+ pro: { priceId: 'price_1TD0lTAaFI0fqFWNYxUnvUxH', pricePerUnit: 279, currency: 'CHF' },
364
+ allowedTiers: ['connect', 'pro'],
365
+ },
366
+ procedure: {
367
+ priceId: 'price_1TCL3eAaFI0fqFWNJVuMYjii',
368
+ pricePerBlock: 19,
369
+ proceduresPerBlock: 5,
370
+ currency: 'CHF',
371
+ allowedTiers: ['connect', 'pro'],
372
+ },
373
+ },
374
+
375
+ priceToModel: {
376
+ 'price_1TD0l7AaFI0fqFWNC8Lg3QJG': 'connect',
377
+ 'price_1TD0lEAaFI0fqFWN3tzCrfUb': 'pro',
378
+ },
379
+
380
+ priceToType: {
381
+ 'price_1TD0l7AaFI0fqFWNC8Lg3QJG': 'CONNECT',
382
+ 'price_1TD0lEAaFI0fqFWN3tzCrfUb': 'PRO',
383
+ },
384
+
385
+ addonPriceIds: [
386
+ 'price_1TD0lKAaFI0fqFWNH9pQgTzI', // seat
387
+ 'price_1TD0lSAaFI0fqFWNjsPvzW1T', // branch connect
388
+ 'price_1TD0lTAaFI0fqFWNYxUnvUxH', // branch pro
389
+ 'price_1TCL3eAaFI0fqFWNJVuMYjii', // procedure
390
+ ],
391
+ };
392
+
393
+ /**
394
+ * Human-readable labels and grouping for permission keys.
395
+ * Used by the staff management UI for role/permission display.
396
+ */
397
+ export const PERMISSION_LABELS: Record<
398
+ string,
399
+ { label: string; description: string; category: string }
400
+ > = {
401
+ 'clinic.view': { label: 'View Clinic', description: 'See clinic information and profile', category: 'Clinic' },
402
+ 'clinic.edit': { label: 'Edit Clinic', description: 'Modify clinic settings and profile', category: 'Clinic' },
403
+ 'clinic.create': { label: 'Create Clinic', description: 'Create new clinic branches', category: 'Clinic' },
404
+ 'reviews.view': { label: 'View Reviews', description: 'See patient reviews and ratings', category: 'Clinic' },
405
+ 'calendar.view': { label: 'View Calendar', description: 'See the clinic calendar and schedules', category: 'Calendar' },
406
+ 'calendar.addEvent': { label: 'Add Events', description: 'Create appointments and blocking events on the calendar', category: 'Calendar' },
407
+ 'calendar.editEvent': { label: 'Edit Events', description: 'Edit blocking events on the calendar', category: 'Calendar' },
408
+ 'calendar.deleteEvent': { label: 'Delete Events', description: 'Delete blocking events from the calendar', category: 'Calendar' },
409
+ 'appointments.view': { label: 'View Appointments', description: 'See appointment list and details', category: 'Appointments' },
410
+ 'appointments.confirm': { label: 'Confirm Appointments', description: 'Confirm, reject, check in, and manage appointment status', category: 'Appointments' },
411
+ 'appointments.reschedule': { label: 'Reschedule Appointments', description: 'Reschedule confirmed appointments to a new time', category: 'Appointments' },
412
+ 'appointments.cancel': { label: 'Cancel Appointments', description: 'Cancel existing appointments', category: 'Appointments' },
413
+ 'messaging': { label: 'Messaging', description: 'Send and receive messages with patients', category: 'Messaging' },
414
+ 'procedures.view': { label: 'View Procedures', description: 'See the procedures catalog', category: 'Procedures' },
415
+ 'procedures.create': { label: 'Create Procedures', description: 'Add new procedures and assign to practitioners', category: 'Procedures' },
416
+ 'procedures.edit': { label: 'Edit Procedures', description: 'Modify existing procedures', category: 'Procedures' },
417
+ 'procedures.delete': { label: 'Delete Procedures', description: 'Remove procedures from the catalog', category: 'Procedures' },
418
+ 'resources.view': { label: 'View Resources', description: 'See clinic resources and equipment', category: 'Resources' },
419
+ 'resources.create': { label: 'Create Resources', description: 'Add new resources', category: 'Resources' },
420
+ 'resources.edit': { label: 'Edit Resources', description: 'Modify existing resources and activate instances', category: 'Resources' },
421
+ 'resources.delete': { label: 'Delete Resources', description: 'Deactivate resources and delete instances', category: 'Resources' },
422
+ 'patients.view': { label: 'View Patients', description: 'See patient list', category: 'Patients' },
423
+ 'patients.viewDetails': { label: 'View Patient Details', description: 'Open patient file and detailed profile', category: 'Patients' },
424
+ 'patients.create': { label: 'Create Patients', description: 'Add new patients', category: 'Patients' },
425
+ 'patients.edit': { label: 'Edit Patients', description: 'Modify patient records and schedule appointments', category: 'Patients' },
426
+ 'patients.manageTokens': { label: 'Manage Patient Tokens', description: 'Create and manage patient invitation tokens', category: 'Patients' },
427
+ 'providers.view': { label: 'View Providers', description: 'See the practitioners/providers list', category: 'Providers' },
428
+ 'providers.create': { label: 'Create Providers', description: 'Add new practitioners to the clinic', category: 'Providers' },
429
+ 'providers.invite': { label: 'Invite Providers', description: 'Invite existing practitioners to the clinic', category: 'Providers' },
430
+ 'providers.edit': { label: 'Edit Providers', description: 'Edit practitioner profiles and manage tokens', category: 'Providers' },
431
+ 'analytics.view': { label: 'View Analytics', description: 'Access analytics dashboards and reports', category: 'Analytics' },
432
+ 'staff.view': { label: 'View Staff', description: 'See staff management page and team members', category: 'Staff Management' },
433
+ 'staff.edit': { label: 'Edit Staff', description: 'Edit roles, permissions, and clinic assignments', category: 'Staff Management' },
434
+ 'staff.invite': { label: 'Invite Staff', description: 'Invite new team members', category: 'Staff Management' },
435
+ 'staff.delete': { label: 'Delete Staff', description: 'Remove team members and cancel invitations', category: 'Staff Management' },
436
+ 'staff.viewTokens': { label: 'View Tokens', description: 'See registration tokens', category: 'Staff Management' },
437
+ 'staff.deleteTokens': { label: 'Delete Tokens', description: 'Delete registration tokens', category: 'Staff Management' },
438
+ 'settings.manage': { label: 'Manage Settings', description: 'Modify clinic group settings', category: 'Administration' },
439
+ 'billing.view': { label: 'View Billing', description: 'View subscription plans and billing history', category: 'Administration' },
440
+ 'billing.manage': { label: 'Manage Billing', description: 'Manage subscriptions, upgrades, and payments', category: 'Administration' },
441
+ };
442
+
443
+ /** All unique permission categories in display order. */
444
+ export const PERMISSION_CATEGORIES = [
445
+ 'Clinic',
446
+ 'Calendar',
447
+ 'Appointments',
448
+ 'Messaging',
449
+ 'Procedures',
450
+ 'Resources',
451
+ 'Patients',
452
+ 'Providers',
453
+ 'Analytics',
454
+ 'Staff Management',
455
+ 'Administration',
456
+ ] as const;
457
+
239
458
  /**
240
459
  * Maps subscription model strings to tier keys.
241
460
  */
@@ -319,11 +319,12 @@ export class AuthService extends BaseService {
319
319
  // Now update the admin with the group ID
320
320
  console.log('[AUTH] Updating admin with clinic group ID');
321
321
  await clinicAdminService.updateClinicAdmin(adminProfile.id, {
322
- // Use admin profile ID, not user UID
323
322
  clinicGroupId: clinicGroup.id,
324
323
  });
325
324
  console.log('[AUTH] Admin updated with clinic group ID successfully');
326
325
 
326
+ // adminsInfo is populated by onCreateClinicAdmin Firestore trigger
327
+
327
328
  // Get the updated admin profile
328
329
  adminProfile = await clinicAdminService.getClinicAdmin(adminProfile.id);
329
330
  } catch (groupCreationError) {
@@ -341,57 +342,23 @@ export class AuthService extends BaseService {
341
342
  token: data.inviteToken,
342
343
  });
343
344
 
344
- // Find the token in the database
345
- console.log('[AUTH] Searching for token in clinic groups');
346
- const groupsRef = collection(this.db, CLINIC_GROUPS_COLLECTION);
347
- const q = query(groupsRef);
348
- const querySnapshot = await getDocs(q);
349
-
350
- let foundGroup: ClinicGroup | null = null;
351
- let foundToken: AdminToken | null = null;
352
-
353
- console.log('[AUTH] Found', querySnapshot.size, 'clinic groups to check');
354
- for (const docSnapshot of querySnapshot.docs) {
355
- const group = docSnapshot.data() as ClinicGroup;
356
- console.log('[AUTH] Checking group', {
357
- groupId: group.id,
358
- groupName: group.name,
359
- });
360
-
361
- // Find the token in the group's tokens
362
- const token = group.adminTokens.find(t => {
363
- const isMatch =
364
- t.token === data.inviteToken &&
365
- t.status === AdminTokenStatus.ACTIVE &&
366
- new Date(t.expiresAt.toDate()) > new Date();
367
-
368
- console.log('[AUTH] Checking token', {
369
- tokenId: t.id,
370
- tokenMatch: t.token === data.inviteToken,
371
- tokenStatus: t.status,
372
- tokenActive: t.status === AdminTokenStatus.ACTIVE,
373
- tokenExpiry: new Date(t.expiresAt.toDate()),
374
- tokenExpired: new Date(t.expiresAt.toDate()) <= new Date(),
375
- isMatch,
376
- });
377
-
378
- return isMatch;
379
- });
345
+ // Find the token using collection group query (O(1) instead of scanning all groups)
346
+ console.log('[AUTH] Searching for token via collection group query');
347
+ const tokenResult = await clinicGroupService.findAdminTokenByValue(data.inviteToken);
380
348
 
381
- if (token) {
382
- foundGroup = group;
383
- foundToken = token;
384
- console.log('[AUTH] Found matching token in group', {
385
- groupId: group.id,
386
- tokenId: token.id,
387
- });
388
- break;
389
- }
349
+ if (!tokenResult) {
350
+ console.error('[AUTH] No valid active token found');
351
+ throw new Error('Invalid or expired invite token');
390
352
  }
391
353
 
392
- if (!foundGroup || !foundToken) {
393
- console.error('[AUTH] No valid token found in any clinic group');
394
- throw new Error('Invalid or expired invite token');
354
+ console.log('[AUTH] Found matching token', {
355
+ tokenId: tokenResult.token.id,
356
+ clinicGroupId: tokenResult.clinicGroupId,
357
+ });
358
+
359
+ const foundGroup = await clinicGroupService.getClinicGroup(tokenResult.clinicGroupId);
360
+ if (!foundGroup) {
361
+ throw new Error('Clinic group not found for token');
395
362
  }
396
363
 
397
364
  clinicGroup = foundGroup;
@@ -198,13 +198,20 @@ export class ClinicGroupService extends BaseService {
198
198
  return ClinicGroupUtils.getActiveAdminTokens(this.db, groupId, adminId, this.app);
199
199
  }
200
200
 
201
- // TODO: Add a method to get all admin tokens for a clinic group (not just active ones)
202
-
203
- // TODO: Refactor admin token methods not to add tokens to the clinicGroup document,
204
- // but to add them to a subcollection called adminTokens that belongs to a specific clinicGroup document
201
+ /**
202
+ * Gets ALL admin tokens for a clinic group (all statuses)
203
+ */
204
+ async getAllAdminTokens(groupId: string, adminId: string): Promise<AdminToken[]> {
205
+ return ClinicGroupUtils.getAllAdminTokens(this.db, groupId, adminId, this.app);
206
+ }
205
207
 
206
- // TODO: Add granular control over admin permissions, e.g. only allow admins to manage certain clinics to tokens directly
207
- // TODO: Generally refactor admin tokens and invites, also create cloud function to send invites and send updates when sombody uses the token
208
+ /**
209
+ * Finds an admin token by its value across all clinic groups.
210
+ * Uses a collection group query for O(1) lookup.
211
+ */
212
+ async findAdminTokenByValue(tokenValue: string): Promise<{ token: AdminToken; clinicGroupId: string } | null> {
213
+ return ClinicGroupUtils.findAdminTokenByValue(this.db, tokenValue);
214
+ }
208
215
 
209
216
  /**
210
217
  * Updates the onboarding status for a clinic group