@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/dist/admin/index.d.mts +3 -1
- package/dist/admin/index.d.ts +3 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/index.d.mts +236 -6
- package/dist/index.d.ts +236 -6
- package/dist/index.js +2095 -1787
- package/dist/index.mjs +1199 -898
- package/package.json +1 -1
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +1 -1
- package/src/config/index.ts +3 -0
- package/src/config/tiers.config.ts +258 -39
- package/src/services/auth/auth.service.ts +16 -49
- package/src/services/clinic/clinic-group.service.ts +13 -6
- package/src/services/clinic/utils/clinic-group.utils.ts +110 -89
- package/src/services/plan-config.service.ts +55 -0
- package/src/services/resource/resource.service.ts +86 -17
- package/src/services/tier-enforcement.ts +15 -10
- package/src/types/clinic/index.ts +8 -1
- package/src/types/clinic/rbac.types.ts +36 -1
- package/src/types/concern/index.ts +66 -0
- package/src/types/index.ts +6 -0
- package/src/types/system/index.ts +1 -0
- package/src/types/system/planConfig.types.ts +86 -0
- package/src/validations/clinic.schema.ts +9 -3
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.
|
|
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
|
-
|
|
62
|
+
clinics: admin.firestore.FieldValue.arrayUnion(clinicId),
|
|
63
63
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
64
64
|
});
|
|
65
65
|
|
package/src/config/index.ts
CHANGED
|
@@ -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/
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
162
|
+
'providers.create': true,
|
|
163
|
+
'providers.invite': true,
|
|
164
|
+
'providers.edit': true,
|
|
128
165
|
'analytics.view': true,
|
|
129
|
-
'staff.
|
|
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.
|
|
204
|
+
'providers.create': true,
|
|
205
|
+
'providers.invite': true,
|
|
206
|
+
'providers.edit': true,
|
|
154
207
|
'analytics.view': true,
|
|
155
|
-
'staff.
|
|
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.
|
|
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.
|
|
246
|
+
'providers.create': false,
|
|
247
|
+
'providers.invite': false,
|
|
248
|
+
'providers.edit': false,
|
|
206
249
|
'analytics.view': false,
|
|
207
|
-
'staff.
|
|
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.
|
|
288
|
+
'providers.create': false,
|
|
289
|
+
'providers.invite': false,
|
|
290
|
+
'providers.edit': false,
|
|
232
291
|
'analytics.view': false,
|
|
233
|
-
'staff.
|
|
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
|
|
345
|
-
console.log('[AUTH] Searching for token
|
|
346
|
-
const
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|