@blackcode_sa/metaestetics-api 1.15.16 → 1.15.17

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 (42) hide show
  1. package/dist/admin/index.d.mts +377 -222
  2. package/dist/admin/index.d.ts +377 -222
  3. package/dist/admin/index.js +625 -206
  4. package/dist/admin/index.mjs +624 -206
  5. package/dist/backoffice/index.d.mts +24 -0
  6. package/dist/backoffice/index.d.ts +24 -0
  7. package/dist/index.d.mts +371 -4
  8. package/dist/index.d.ts +371 -4
  9. package/dist/index.js +2227 -1580
  10. package/dist/index.mjs +1543 -891
  11. package/package.json +1 -1
  12. package/src/admin/aggregation/appointment/README.md +24 -2
  13. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +46 -0
  14. package/src/admin/booking/README.md +61 -2
  15. package/src/admin/booking/booking.admin.ts +257 -0
  16. package/src/admin/booking/booking.calculator.ts +139 -1
  17. package/src/admin/booking/booking.types.ts +17 -0
  18. package/src/admin/calendar/README.md +56 -1
  19. package/src/admin/calendar/index.ts +1 -0
  20. package/src/admin/calendar/resource-calendar.admin.ts +198 -0
  21. package/src/config/index.ts +1 -0
  22. package/src/config/tiers.config.ts +116 -0
  23. package/src/services/index.ts +1 -0
  24. package/src/services/plan-config.service.ts +55 -0
  25. package/src/services/resource/README.md +119 -0
  26. package/src/services/resource/index.ts +1 -0
  27. package/src/services/resource/resource.service.ts +555 -0
  28. package/src/services/tier-enforcement.ts +15 -10
  29. package/src/types/appointment/index.ts +7 -0
  30. package/src/types/calendar/index.ts +1 -0
  31. package/src/types/clinic/index.ts +1 -0
  32. package/src/types/clinic/rbac.types.ts +3 -2
  33. package/src/types/index.ts +6 -0
  34. package/src/types/procedure/index.ts +6 -0
  35. package/src/types/resource/README.md +153 -0
  36. package/src/types/resource/index.ts +199 -0
  37. package/src/types/system/index.ts +1 -0
  38. package/src/types/system/planConfig.types.ts +86 -0
  39. package/src/validations/README.md +94 -0
  40. package/src/validations/index.ts +1 -0
  41. package/src/validations/procedure.schema.ts +12 -0
  42. package/src/validations/resource.schema.ts +57 -0
@@ -0,0 +1,555 @@
1
+ import {
2
+ collection,
3
+ doc,
4
+ getDoc,
5
+ getDocs,
6
+ setDoc,
7
+ deleteDoc,
8
+ query,
9
+ where,
10
+ orderBy,
11
+ writeBatch,
12
+ updateDoc,
13
+ serverTimestamp,
14
+ Timestamp,
15
+ } from "firebase/firestore";
16
+ import { BaseService } from "../base.service";
17
+ import {
18
+ Resource,
19
+ ResourceInstance,
20
+ ResourceCalendarEvent,
21
+ ResourceStatus,
22
+ CreateResourceData,
23
+ UpdateResourceData,
24
+ CreateResourceBlockingEventParams,
25
+ UpdateResourceBlockingEventParams,
26
+ RESOURCES_COLLECTION,
27
+ RESOURCE_INSTANCES_SUBCOLLECTION,
28
+ RESOURCE_CALENDAR_SUBCOLLECTION,
29
+ } from "../../types/resource";
30
+ import { CalendarEventType, CalendarEventStatus } from "../../types/calendar";
31
+ import {
32
+ createResourceBlockingEventSchema,
33
+ updateResourceBlockingEventSchema,
34
+ } from "../../validations/resource.schema";
35
+
36
+ /**
37
+ * Service for managing clinic resources and their instances.
38
+ * Resources are stored as subcollections under clinics:
39
+ * clinics/{clinicId}/resources/{resourceId}
40
+ * clinics/{clinicId}/resources/{resourceId}/instances/{instanceId}
41
+ * clinics/{clinicId}/resources/{resourceId}/instances/{instanceId}/calendar/{eventId}
42
+ */
43
+ export class ResourceService extends BaseService {
44
+ /**
45
+ * Gets reference to a clinic's resources collection
46
+ */
47
+ private getResourcesRef(clinicBranchId: string) {
48
+ return collection(this.db, "clinics", clinicBranchId, RESOURCES_COLLECTION);
49
+ }
50
+
51
+ /**
52
+ * Gets reference to a specific resource document
53
+ */
54
+ private getResourceDocRef(clinicBranchId: string, resourceId: string) {
55
+ return doc(
56
+ this.db,
57
+ "clinics",
58
+ clinicBranchId,
59
+ RESOURCES_COLLECTION,
60
+ resourceId
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Gets reference to a resource's instances subcollection
66
+ */
67
+ private getInstancesRef(clinicBranchId: string, resourceId: string) {
68
+ return collection(
69
+ this.db,
70
+ "clinics",
71
+ clinicBranchId,
72
+ RESOURCES_COLLECTION,
73
+ resourceId,
74
+ RESOURCE_INSTANCES_SUBCOLLECTION
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Gets reference to an instance's calendar subcollection
80
+ */
81
+ private getInstanceCalendarRef(
82
+ clinicBranchId: string,
83
+ resourceId: string,
84
+ instanceId: string
85
+ ) {
86
+ return collection(
87
+ this.db,
88
+ "clinics",
89
+ clinicBranchId,
90
+ RESOURCES_COLLECTION,
91
+ resourceId,
92
+ RESOURCE_INSTANCES_SUBCOLLECTION,
93
+ instanceId,
94
+ RESOURCE_CALENDAR_SUBCOLLECTION
95
+ );
96
+ }
97
+
98
+ /**
99
+ * Creates a new resource with its instances.
100
+ * Creates the resource document and N instance subdocuments in a single batch.
101
+ */
102
+ async createResource(data: CreateResourceData): Promise<Resource> {
103
+ const batch = writeBatch(this.db);
104
+
105
+ const resourceRef = doc(this.getResourcesRef(data.clinicBranchId));
106
+ const resourceId = resourceRef.id;
107
+
108
+ const now = serverTimestamp();
109
+
110
+ const resourceData: Omit<Resource, "createdAt" | "updatedAt"> & {
111
+ createdAt: any;
112
+ updatedAt: any;
113
+ } = {
114
+ id: resourceId,
115
+ clinicBranchId: data.clinicBranchId,
116
+ name: data.name,
117
+ nameLower: data.name.toLowerCase(),
118
+ category: data.category,
119
+ description: data.description || undefined,
120
+ quantity: data.quantity,
121
+ status: ResourceStatus.ACTIVE,
122
+ linkedProcedureIds: [],
123
+ createdAt: now,
124
+ updatedAt: now,
125
+ };
126
+
127
+ batch.set(resourceRef, resourceData);
128
+
129
+ // Create instance subdocuments
130
+ for (let i = 1; i <= data.quantity; i++) {
131
+ const instanceRef = doc(
132
+ this.getInstancesRef(data.clinicBranchId, resourceId)
133
+ );
134
+ const instanceData: Omit<ResourceInstance, "createdAt" | "updatedAt"> & {
135
+ createdAt: any;
136
+ updatedAt: any;
137
+ } = {
138
+ id: instanceRef.id,
139
+ resourceId,
140
+ clinicBranchId: data.clinicBranchId,
141
+ label: `${data.name} #${i}`,
142
+ index: i,
143
+ status: ResourceStatus.ACTIVE,
144
+ createdAt: now,
145
+ updatedAt: now,
146
+ };
147
+ batch.set(instanceRef, instanceData);
148
+ }
149
+
150
+ await batch.commit();
151
+
152
+ // Fetch and return the created resource
153
+ const created = await this.getResource(data.clinicBranchId, resourceId);
154
+ if (!created) throw new Error("Failed to read created resource");
155
+ return created;
156
+ }
157
+
158
+ /**
159
+ * Gets a single resource by ID
160
+ */
161
+ async getResource(
162
+ clinicBranchId: string,
163
+ resourceId: string
164
+ ): Promise<Resource | null> {
165
+ const docRef = this.getResourceDocRef(clinicBranchId, resourceId);
166
+ const docSnap = await getDoc(docRef);
167
+ if (!docSnap.exists()) return null;
168
+ return { id: docSnap.id, ...docSnap.data() } as Resource;
169
+ }
170
+
171
+ /**
172
+ * Gets all resources for a clinic branch
173
+ */
174
+ async getResourcesByClinic(clinicBranchId: string): Promise<Resource[]> {
175
+ const q = query(
176
+ this.getResourcesRef(clinicBranchId),
177
+ orderBy("nameLower")
178
+ );
179
+ const snapshot = await getDocs(q);
180
+ return snapshot.docs.map(
181
+ (d) => ({ id: d.id, ...d.data() } as Resource)
182
+ );
183
+ }
184
+
185
+ /**
186
+ * Gets all active resources for a clinic branch
187
+ */
188
+ async getActiveResourcesByClinic(
189
+ clinicBranchId: string
190
+ ): Promise<Resource[]> {
191
+ const q = query(
192
+ this.getResourcesRef(clinicBranchId),
193
+ where("status", "==", ResourceStatus.ACTIVE),
194
+ orderBy("nameLower")
195
+ );
196
+ const snapshot = await getDocs(q);
197
+ return snapshot.docs.map(
198
+ (d) => ({ id: d.id, ...d.data() } as Resource)
199
+ );
200
+ }
201
+
202
+ /**
203
+ * Updates a resource. Handles quantity changes:
204
+ * - Increasing quantity: creates new instances
205
+ * - Decreasing quantity: blocked if affected instances have future bookings
206
+ */
207
+ async updateResource(
208
+ clinicBranchId: string,
209
+ resourceId: string,
210
+ data: UpdateResourceData
211
+ ): Promise<Resource> {
212
+ const existing = await this.getResource(clinicBranchId, resourceId);
213
+ if (!existing) throw new Error(`Resource ${resourceId} not found`);
214
+
215
+ const updateData: Record<string, any> = {
216
+ updatedAt: serverTimestamp(),
217
+ };
218
+
219
+ if (data.name !== undefined) {
220
+ updateData.name = data.name;
221
+ updateData.nameLower = data.name.toLowerCase();
222
+ }
223
+ if (data.category !== undefined) updateData.category = data.category;
224
+ if (data.description !== undefined)
225
+ updateData.description = data.description;
226
+ if (data.status !== undefined) updateData.status = data.status;
227
+
228
+ // Handle quantity change
229
+ if (data.quantity !== undefined && data.quantity !== existing.quantity) {
230
+ if (data.quantity > existing.quantity) {
231
+ // Add new instances
232
+ const batch = writeBatch(this.db);
233
+ const now = serverTimestamp();
234
+ const resourceName = data.name || existing.name;
235
+
236
+ for (let i = existing.quantity + 1; i <= data.quantity; i++) {
237
+ const instanceRef = doc(
238
+ this.getInstancesRef(clinicBranchId, resourceId)
239
+ );
240
+ const instanceData: Omit<
241
+ ResourceInstance,
242
+ "createdAt" | "updatedAt"
243
+ > & { createdAt: any; updatedAt: any } = {
244
+ id: instanceRef.id,
245
+ resourceId,
246
+ clinicBranchId,
247
+ label: `${resourceName} #${i}`,
248
+ index: i,
249
+ status: ResourceStatus.ACTIVE,
250
+ createdAt: now,
251
+ updatedAt: now,
252
+ };
253
+ batch.set(instanceRef, instanceData);
254
+ }
255
+
256
+ updateData.quantity = data.quantity;
257
+
258
+ // Update resource doc in the batch too
259
+ const resourceDocRef = this.getResourceDocRef(
260
+ clinicBranchId,
261
+ resourceId
262
+ );
263
+ batch.update(resourceDocRef, updateData);
264
+ await batch.commit();
265
+
266
+ const updated = await this.getResource(clinicBranchId, resourceId);
267
+ if (!updated) throw new Error("Failed to read updated resource");
268
+ return updated;
269
+ } else {
270
+ // Decreasing quantity - check for future bookings on affected instances
271
+ const instances = await this.getResourceInstances(
272
+ clinicBranchId,
273
+ resourceId
274
+ );
275
+ const instancesToRemove = instances
276
+ .filter((inst) => inst.index > data.quantity!)
277
+ .filter((inst) => inst.status === ResourceStatus.ACTIVE);
278
+
279
+ for (const instance of instancesToRemove) {
280
+ const hasFutureBookings = await this.instanceHasFutureBookings(
281
+ clinicBranchId,
282
+ resourceId,
283
+ instance.id
284
+ );
285
+ if (hasFutureBookings) {
286
+ throw new Error(
287
+ `Cannot reduce quantity: instance "${instance.label}" has future bookings. Cancel those bookings first.`
288
+ );
289
+ }
290
+ }
291
+
292
+ // Safe to deactivate instances
293
+ const batch = writeBatch(this.db);
294
+ for (const instance of instancesToRemove) {
295
+ const instanceRef = doc(
296
+ this.getInstancesRef(clinicBranchId, resourceId),
297
+ instance.id
298
+ );
299
+ batch.update(instanceRef, {
300
+ status: ResourceStatus.INACTIVE,
301
+ updatedAt: serverTimestamp(),
302
+ });
303
+ }
304
+
305
+ updateData.quantity = data.quantity;
306
+ const resourceDocRef = this.getResourceDocRef(
307
+ clinicBranchId,
308
+ resourceId
309
+ );
310
+ batch.update(resourceDocRef, updateData);
311
+ await batch.commit();
312
+
313
+ const updated = await this.getResource(clinicBranchId, resourceId);
314
+ if (!updated) throw new Error("Failed to read updated resource");
315
+ return updated;
316
+ }
317
+ }
318
+
319
+ // Simple update (no quantity change)
320
+ const resourceDocRef = this.getResourceDocRef(clinicBranchId, resourceId);
321
+ await updateDoc(resourceDocRef, updateData);
322
+
323
+ const updated = await this.getResource(clinicBranchId, resourceId);
324
+ if (!updated) throw new Error("Failed to read updated resource");
325
+ return updated;
326
+ }
327
+
328
+ /**
329
+ * Soft deletes a resource by setting status to INACTIVE
330
+ */
331
+ async deleteResource(
332
+ clinicBranchId: string,
333
+ resourceId: string
334
+ ): Promise<void> {
335
+ await this.updateResource(clinicBranchId, resourceId, {
336
+ status: ResourceStatus.INACTIVE,
337
+ });
338
+ }
339
+
340
+ /**
341
+ * Gets all instances for a resource
342
+ */
343
+ async getResourceInstances(
344
+ clinicBranchId: string,
345
+ resourceId: string
346
+ ): Promise<ResourceInstance[]> {
347
+ const q = query(
348
+ this.getInstancesRef(clinicBranchId, resourceId),
349
+ orderBy("index")
350
+ );
351
+ const snapshot = await getDocs(q);
352
+ return snapshot.docs.map(
353
+ (d) => ({ id: d.id, ...d.data() } as ResourceInstance)
354
+ );
355
+ }
356
+
357
+ /**
358
+ * Gets active instances for a resource
359
+ */
360
+ async getActiveResourceInstances(
361
+ clinicBranchId: string,
362
+ resourceId: string
363
+ ): Promise<ResourceInstance[]> {
364
+ const q = query(
365
+ this.getInstancesRef(clinicBranchId, resourceId),
366
+ where("status", "==", ResourceStatus.ACTIVE),
367
+ orderBy("index")
368
+ );
369
+ const snapshot = await getDocs(q);
370
+ return snapshot.docs.map(
371
+ (d) => ({ id: d.id, ...d.data() } as ResourceInstance)
372
+ );
373
+ }
374
+
375
+ /**
376
+ * Gets calendar events for a specific resource instance within a time range
377
+ */
378
+ async getResourceCalendarEvents(
379
+ clinicBranchId: string,
380
+ resourceId: string,
381
+ instanceId: string,
382
+ start: Timestamp,
383
+ end: Timestamp
384
+ ): Promise<ResourceCalendarEvent[]> {
385
+ const calendarRef = this.getInstanceCalendarRef(
386
+ clinicBranchId,
387
+ resourceId,
388
+ instanceId
389
+ );
390
+ const q = query(
391
+ calendarRef,
392
+ where("eventTime.start", ">=", start),
393
+ where("eventTime.start", "<=", end),
394
+ orderBy("eventTime.start")
395
+ );
396
+ const snapshot = await getDocs(q);
397
+ return snapshot.docs.map(
398
+ (d) => ({ id: d.id, ...d.data() } as ResourceCalendarEvent)
399
+ );
400
+ }
401
+
402
+ /**
403
+ * Checks if a resource instance has any future active bookings
404
+ */
405
+ private async instanceHasFutureBookings(
406
+ clinicBranchId: string,
407
+ resourceId: string,
408
+ instanceId: string
409
+ ): Promise<boolean> {
410
+ const now = Timestamp.now();
411
+ const calendarRef = this.getInstanceCalendarRef(
412
+ clinicBranchId,
413
+ resourceId,
414
+ instanceId
415
+ );
416
+ const q = query(
417
+ calendarRef,
418
+ where("eventTime.start", ">=", now),
419
+ where("status", "in", ["pending", "confirmed"])
420
+ );
421
+ const snapshot = await getDocs(q);
422
+ return !snapshot.empty;
423
+ }
424
+
425
+ // --- Resource Instance Blocking Events ---
426
+
427
+ /**
428
+ * Creates a blocking event on a resource instance's calendar.
429
+ * Blocking events prevent the instance from being booked during the specified time.
430
+ */
431
+ async createResourceBlockingEvent(
432
+ params: CreateResourceBlockingEventParams
433
+ ): Promise<ResourceCalendarEvent> {
434
+ createResourceBlockingEventSchema.parse(params);
435
+
436
+ const calendarRef = this.getInstanceCalendarRef(
437
+ params.clinicBranchId,
438
+ params.resourceId,
439
+ params.resourceInstanceId
440
+ );
441
+ const eventRef = doc(calendarRef);
442
+ const now = serverTimestamp();
443
+
444
+ const eventData = {
445
+ id: eventRef.id,
446
+ resourceId: params.resourceId,
447
+ resourceInstanceId: params.resourceInstanceId,
448
+ clinicBranchId: params.clinicBranchId,
449
+ eventType: CalendarEventType.BLOCKING,
450
+ eventName: params.eventName,
451
+ eventTime: params.eventTime,
452
+ status: CalendarEventStatus.CONFIRMED,
453
+ description: params.description || "",
454
+ createdAt: now,
455
+ updatedAt: now,
456
+ };
457
+
458
+ await setDoc(eventRef, eventData);
459
+
460
+ return {
461
+ ...eventData,
462
+ createdAt: Timestamp.now(),
463
+ updatedAt: Timestamp.now(),
464
+ } as ResourceCalendarEvent;
465
+ }
466
+
467
+ /**
468
+ * Updates an existing blocking event on a resource instance's calendar.
469
+ * Only provided fields are updated.
470
+ */
471
+ async updateResourceBlockingEvent(
472
+ params: UpdateResourceBlockingEventParams
473
+ ): Promise<ResourceCalendarEvent> {
474
+ updateResourceBlockingEventSchema.parse(params);
475
+
476
+ const calendarRef = this.getInstanceCalendarRef(
477
+ params.clinicBranchId,
478
+ params.resourceId,
479
+ params.resourceInstanceId
480
+ );
481
+ const eventRef = doc(calendarRef, params.eventId);
482
+
483
+ const eventSnap = await getDoc(eventRef);
484
+ if (!eventSnap.exists()) {
485
+ throw new Error(`Blocking event ${params.eventId} not found`);
486
+ }
487
+
488
+ const updateData: Record<string, any> = {
489
+ updatedAt: serverTimestamp(),
490
+ };
491
+
492
+ if (params.eventName !== undefined) {
493
+ updateData.eventName = params.eventName;
494
+ }
495
+ if (params.eventTime !== undefined) {
496
+ updateData.eventTime = params.eventTime;
497
+ }
498
+ if (params.description !== undefined) {
499
+ updateData.description = params.description;
500
+ }
501
+
502
+ await updateDoc(eventRef, updateData);
503
+
504
+ const updatedSnap = await getDoc(eventRef);
505
+ return { id: updatedSnap.id, ...updatedSnap.data() } as ResourceCalendarEvent;
506
+ }
507
+
508
+ /**
509
+ * Deletes a blocking event from a resource instance's calendar (hard delete).
510
+ */
511
+ async deleteResourceBlockingEvent(
512
+ clinicBranchId: string,
513
+ resourceId: string,
514
+ instanceId: string,
515
+ eventId: string
516
+ ): Promise<void> {
517
+ const calendarRef = this.getInstanceCalendarRef(
518
+ clinicBranchId,
519
+ resourceId,
520
+ instanceId
521
+ );
522
+ const eventRef = doc(calendarRef, eventId);
523
+
524
+ const eventSnap = await getDoc(eventRef);
525
+ if (!eventSnap.exists()) {
526
+ throw new Error(`Blocking event ${eventId} not found`);
527
+ }
528
+
529
+ await deleteDoc(eventRef);
530
+ }
531
+
532
+ /**
533
+ * Gets all blocking events for a resource instance, ordered by start time.
534
+ */
535
+ async getResourceInstanceBlockingEvents(
536
+ clinicBranchId: string,
537
+ resourceId: string,
538
+ instanceId: string
539
+ ): Promise<ResourceCalendarEvent[]> {
540
+ const calendarRef = this.getInstanceCalendarRef(
541
+ clinicBranchId,
542
+ resourceId,
543
+ instanceId
544
+ );
545
+ const q = query(
546
+ calendarRef,
547
+ where("eventType", "==", CalendarEventType.BLOCKING),
548
+ orderBy("eventTime.start")
549
+ );
550
+ const snapshot = await getDocs(q);
551
+ return snapshot.docs.map(
552
+ (d) => ({ id: d.id, ...d.data() } as ResourceCalendarEvent)
553
+ );
554
+ }
555
+ }
@@ -14,6 +14,7 @@ import {
14
14
  import { PRACTITIONERS_COLLECTION } from '../types/practitioner/index';
15
15
  import { PROCEDURES_COLLECTION } from '../types/procedure/index';
16
16
  import { TIER_CONFIG, resolveEffectiveTier } from '../config/tiers.config';
17
+ import { planConfigService } from './plan-config.service';
17
18
 
18
19
  export class TierLimitError extends Error {
19
20
  public readonly code = 'TIER_LIMIT_EXCEEDED';
@@ -171,10 +172,11 @@ export async function enforceProviderLimit(
171
172
  branchId: string
172
173
  ): Promise<void> {
173
174
  const { tier, billing } = await getClinicGroupTierData(db, clinicGroupId);
174
- const config = TIER_CONFIG[tier];
175
- if (!config) return;
175
+ const dynamicConfig = await planConfigService.getConfig(db);
176
+ const tierDef = dynamicConfig.tiers[tier] ?? TIER_CONFIG[tier];
177
+ if (!tierDef) return;
176
178
 
177
- const baseMax = config.limits.maxProvidersPerBranch;
179
+ const baseMax = tierDef.limits.maxProvidersPerBranch;
178
180
  if (baseMax === -1) return;
179
181
 
180
182
  const addOns = billing?.addOns || {};
@@ -199,15 +201,17 @@ export async function enforceProcedureLimit(
199
201
  count: number = 1
200
202
  ): Promise<void> {
201
203
  const { tier, billing } = await getClinicGroupTierData(db, clinicGroupId);
202
- const config = TIER_CONFIG[tier];
203
- if (!config) return;
204
+ const dynamicConfig = await planConfigService.getConfig(db);
205
+ const tierDef = dynamicConfig.tiers[tier] ?? TIER_CONFIG[tier];
206
+ if (!tierDef) return;
204
207
 
205
- const baseMax = config.limits.maxProceduresPerProvider;
208
+ const baseMax = tierDef.limits.maxProceduresPerProvider;
206
209
  if (baseMax === -1) return;
207
210
 
208
211
  const addOns = billing?.addOns || {};
209
212
  const procedureBlocks = addOns[branchId]?.procedureBlocks || 0;
210
- const effectiveMax = baseMax + (procedureBlocks * 5);
213
+ const proceduresPerBlock = dynamicConfig.addons?.procedure?.proceduresPerBlock ?? 5;
214
+ const effectiveMax = baseMax + (procedureBlocks * proceduresPerBlock);
211
215
 
212
216
  const currentCount = await countProceduresForProvider(db, branchId, providerId);
213
217
  if (currentCount + count > effectiveMax) {
@@ -224,10 +228,11 @@ export async function enforceBranchLimit(
224
228
  clinicGroupId: string
225
229
  ): Promise<void> {
226
230
  const { tier, billing } = await getClinicGroupTierData(db, clinicGroupId);
227
- const config = TIER_CONFIG[tier];
228
- if (!config) return;
231
+ const dynamicConfig = await planConfigService.getConfig(db);
232
+ const tierDef = dynamicConfig.tiers[tier] ?? TIER_CONFIG[tier];
233
+ if (!tierDef) return;
229
234
 
230
- const baseMax = config.limits.maxBranches;
235
+ const baseMax = tierDef.limits.maxBranches;
231
236
  if (baseMax === -1) return;
232
237
 
233
238
  const branchAddonCount = billing?.branchAddonCount || 0;
@@ -7,6 +7,7 @@ import { Requirement } from '../../backoffice/types/requirement.types';
7
7
  import { FilledDocumentStatus } from '../documentation-templates';
8
8
  import type { ContraindicationDynamic, ProcedureFamily } from '../../backoffice';
9
9
  import type { MediaResource } from '../../services/media/media.service';
10
+ import type { ResourceBookingInfo } from '../resource';
10
11
  import { string } from 'zod/v4';
11
12
 
12
13
  /**
@@ -390,6 +391,9 @@ export interface Appointment {
390
391
 
391
392
  /** NEW: Metadata for the appointment - used for area selection and photos */
392
393
  metadata?: AppointmentMetadata;
394
+
395
+ /** Resources booked for this appointment (e.g., surgery room instance, device instance) */
396
+ resourceBookings?: ResourceBookingInfo[];
393
397
  }
394
398
 
395
399
  /**
@@ -470,6 +474,9 @@ export interface UpdateAppointmentData {
470
474
 
471
475
  /** NEW: For updating metadata */
472
476
  metadata?: AppointmentMetadata;
477
+
478
+ /** For updating resource bookings */
479
+ resourceBookings?: ResourceBookingInfo[] | FieldValue;
473
480
  }
474
481
 
475
482
  /**
@@ -48,6 +48,7 @@ export enum CalendarEventType {
48
48
  BLOCKING = "blocking",
49
49
  BREAK = "break",
50
50
  FREE_DAY = "free_day",
51
+ RESOURCE_BOOKING = "resource_booking",
51
52
  OTHER = "other",
52
53
  }
53
54
 
@@ -236,6 +236,7 @@ export enum BillingTransactionType {
236
236
  SUBSCRIPTION_CANCELED = 'subscription_canceled',
237
237
  SUBSCRIPTION_REACTIVATED = 'subscription_reactivated',
238
238
  SUBSCRIPTION_DELETED = 'subscription_deleted',
239
+ ADDON_PURCHASED = 'addon_purchased',
239
240
  }
240
241
 
241
242
  /**
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * RBAC (Role-Based Access Control) types for clinic staff management
3
3
  */
4
+ import { Timestamp } from 'firebase/firestore';
4
5
 
5
6
  /**
6
7
  * Roles that can be assigned to clinic staff members
@@ -24,8 +25,8 @@ export interface ClinicStaffMember {
24
25
  role: ClinicRole;
25
26
  permissions: Record<string, boolean>;
26
27
  isActive: boolean;
27
- createdAt?: any;
28
- updatedAt?: any;
28
+ createdAt?: Timestamp;
29
+ updatedAt?: Timestamp;
29
30
  }
30
31
 
31
32
  /**
@@ -37,9 +37,15 @@ export * from "./procedure";
37
37
  // Profile types
38
38
  export * from "./profile";
39
39
 
40
+ // Resource types
41
+ export * from "./resource";
42
+
40
43
  // Reviews types
41
44
  export * from "./reviews";
42
45
 
46
+ // System config types
47
+ export * from "./system";
48
+
43
49
  // Analytics types
44
50
  export * from "./analytics";
45
51