@blackcode_sa/metaestetics-api 1.15.14 → 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 +376 -9
  8. package/dist/index.d.ts +376 -9
  9. package/dist/index.js +2228 -1581
  10. package/dist/index.mjs +1544 -892
  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 +121 -5
  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 +16 -11
  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
@@ -13,6 +13,7 @@ import {
13
13
  } from "../../types/calendar";
14
14
  import { PractitionerClinicWorkingHours } from "../../types/practitioner";
15
15
  import { Clinic } from "../../types/clinic";
16
+ import { ResourceCalendarEvent } from "../../types/resource";
16
17
 
17
18
  /**
18
19
  * Calculator for determining available booking slots
@@ -88,11 +89,20 @@ export class BookingAvailabilityCalculator {
88
89
  practitionerCalendarEvents
89
90
  );
90
91
 
92
+ // Step 5: Apply resource availability constraints (if procedure requires resources)
93
+ if (request.resourceCalendarEventsMap) {
94
+ availableIntervals = this.applyResourceAvailability(
95
+ availableIntervals,
96
+ request.resourceCalendarEventsMap,
97
+ timeframe
98
+ );
99
+ }
100
+
91
101
  console.log(
92
102
  `After all filters, have ${availableIntervals.length} available intervals`
93
103
  );
94
104
 
95
- // Step 5: Generate available slots based on scheduling interval and procedure duration
105
+ // Step 6: Generate available slots based on scheduling interval and procedure duration
96
106
  const availableSlots = this.generateAvailableSlots(
97
107
  availableIntervals,
98
108
  schedulingIntervalMinutes,
@@ -709,4 +719,132 @@ export class BookingAvailabilityCalculator {
709
719
 
710
720
  return result;
711
721
  }
722
+
723
+ /**
724
+ * Apply resource availability constraints to the available intervals.
725
+ * For each required resource:
726
+ * - Compute each instance's availability (full timeframe minus its busy events)
727
+ * - Union all instance availabilities (at least one must be free)
728
+ * Then intersect all resource availabilities with the current doctor availability.
729
+ *
730
+ * @param intervals - Current available intervals (doctor availability)
731
+ * @param resourceEventsMap - Map of resource data with per-instance calendar events
732
+ * @param timeframe - Overall timeframe being considered
733
+ * @returns Intervals where doctor AND all required resources are available
734
+ */
735
+ private static applyResourceAvailability(
736
+ intervals: TimeInterval[],
737
+ resourceEventsMap: Record<
738
+ string,
739
+ {
740
+ resourceId: string;
741
+ resourceName: string;
742
+ quantity: number;
743
+ instanceEvents: Record<string, ResourceCalendarEvent[]>;
744
+ }
745
+ >,
746
+ timeframe: { start: Timestamp; end: Timestamp }
747
+ ): TimeInterval[] {
748
+ if (!intervals.length) return [];
749
+
750
+ const resourceIds = Object.keys(resourceEventsMap);
751
+ if (resourceIds.length === 0) return intervals;
752
+
753
+ console.log(
754
+ `Applying resource availability for ${resourceIds.length} resource(s)`
755
+ );
756
+
757
+ let result = intervals;
758
+
759
+ // For each required resource, compute its combined availability and intersect
760
+ for (const resourceId of resourceIds) {
761
+ const resourceData = resourceEventsMap[resourceId];
762
+ const resourceAvailability = this.computeResourceAvailability(
763
+ resourceData.instanceEvents,
764
+ timeframe
765
+ );
766
+
767
+ console.log(
768
+ `Resource "${resourceData.resourceName}" (qty=${resourceData.quantity}): ${resourceAvailability.length} available intervals`
769
+ );
770
+
771
+ // Intersect doctor availability with this resource's availability
772
+ result = this.intersectIntervals(result, resourceAvailability);
773
+
774
+ if (result.length === 0) {
775
+ console.log(
776
+ `No availability remaining after applying resource "${resourceData.resourceName}"`
777
+ );
778
+ return [];
779
+ }
780
+ }
781
+
782
+ return result;
783
+ }
784
+
785
+ /**
786
+ * Compute combined availability for a single resource across all its instances.
787
+ * For each instance, subtract its busy events from the full timeframe.
788
+ * Then union all instance availabilities — if at least one instance is free, the resource is available.
789
+ *
790
+ * @param instanceEvents - Calendar events keyed by instanceId
791
+ * @param timeframe - Overall timeframe
792
+ * @returns Combined availability intervals for the resource
793
+ */
794
+ private static computeResourceAvailability(
795
+ instanceEvents: Record<string, ResourceCalendarEvent[]>,
796
+ timeframe: { start: Timestamp; end: Timestamp }
797
+ ): TimeInterval[] {
798
+ const instanceIds = Object.keys(instanceEvents);
799
+ if (instanceIds.length === 0) {
800
+ // No instances means the full timeframe is available (no constraints)
801
+ return [{ start: timeframe.start, end: timeframe.end }];
802
+ }
803
+
804
+ // Collect availability intervals from all instances
805
+ const allInstanceAvailabilities: TimeInterval[] = [];
806
+
807
+ for (const instanceId of instanceIds) {
808
+ const events = instanceEvents[instanceId];
809
+ // Start with full timeframe
810
+ let instanceAvailability: TimeInterval[] = [
811
+ { start: timeframe.start, end: timeframe.end },
812
+ ];
813
+
814
+ // Subtract each busy event from this instance's availability
815
+ for (const event of events) {
816
+ // Blocking events always subtract from availability regardless of status.
817
+ // Booking events only subtract when actively held (pending or confirmed).
818
+ const isBlockingEvent =
819
+ (event as any).eventType === CalendarEventType.BLOCKING ||
820
+ (event as any).eventType === CalendarEventType.BREAK ||
821
+ (event as any).eventType === CalendarEventType.FREE_DAY;
822
+
823
+ const isActiveBooking =
824
+ event.status === CalendarEventStatus.PENDING ||
825
+ event.status === CalendarEventStatus.CONFIRMED;
826
+
827
+ if (isBlockingEvent || isActiveBooking) {
828
+ const busyInterval: TimeInterval = {
829
+ start: event.eventTime.start,
830
+ end: event.eventTime.end,
831
+ };
832
+
833
+ const newAvailability: TimeInterval[] = [];
834
+ for (const interval of instanceAvailability) {
835
+ newAvailability.push(
836
+ ...this.subtractInterval(interval, busyInterval)
837
+ );
838
+ }
839
+ instanceAvailability = newAvailability;
840
+ }
841
+ }
842
+
843
+ // Add this instance's available intervals to the combined pool
844
+ allInstanceAvailabilities.push(...instanceAvailability);
845
+ }
846
+
847
+ // Union (merge) all instance availabilities — at least one instance being free is sufficient
848
+ return this.mergeOverlappingIntervals(allInstanceAvailabilities);
849
+ }
712
850
  }
@@ -3,6 +3,7 @@ import { Clinic } from "../../types/clinic";
3
3
  import { Practitioner } from "../../types/practitioner";
4
4
  import { Procedure } from "../../types/procedure";
5
5
  import { CalendarEvent } from "../../types/calendar";
6
+ import { ResourceCalendarEvent } from "../../types/resource";
6
7
 
7
8
  /**
8
9
  * Request parameters for calculating available booking slots
@@ -31,6 +32,22 @@ export interface BookingAvailabilityRequest {
31
32
 
32
33
  /** IANA timezone of the clinic */
33
34
  tz: string;
35
+
36
+ /**
37
+ * Resource calendar events per resource, keyed by resourceId.
38
+ * Each entry contains all instances' events for that resource.
39
+ * Only present when the procedure has resourceRequirements.
40
+ */
41
+ resourceCalendarEventsMap?: Record<
42
+ string,
43
+ {
44
+ resourceId: string;
45
+ resourceName: string;
46
+ quantity: number;
47
+ /** Calendar events keyed by instanceId */
48
+ instanceEvents: Record<string, ResourceCalendarEvent[]>;
49
+ }
50
+ >;
34
51
  }
35
52
 
36
53
  /**
@@ -4,4 +4,59 @@ This directory contains services related to calendar functionality that are inte
4
4
 
5
5
  ## Services
6
6
 
7
- - `synced-calendars.service.ts`: Manages the synchronization of calendars with external providers. It should not be used on the client side.
7
+ ### `calendar.admin.service.ts` CalendarAdminService
8
+
9
+ Manages calendar events for practitioners, patients, and clinics. Used by the `AppointmentAggregationService` to update calendar event statuses and times during the appointment lifecycle.
10
+
11
+ **Key methods:**
12
+ - `updateAppointmentCalendarEventStatus(appointment, newStatus)` — Updates the status of all calendar events for an appointment
13
+ - `updateAppointmentCalendarEventTime(appointment, newEventTime)` — Updates the time range of calendar events (on reschedule)
14
+ - `deleteAppointmentCalendarEvents(appointment)` — Deletes all calendar events for a deleted appointment
15
+
16
+ ---
17
+
18
+ ### `resource-calendar.admin.ts` — ResourceCalendarAdminService
19
+
20
+ Manages calendar events on **resource instances** throughout the appointment lifecycle. This service is part of the [Clinic Resources System](../../../../docs/RESOURCE_SYSTEM.md).
21
+
22
+ **Constructor:**
23
+ ```typescript
24
+ constructor(firestore?: admin.firestore.Firestore)
25
+ ```
26
+
27
+ **Methods:**
28
+
29
+ #### `updateResourceBookingEventsStatus(appointment, newStatus)`
30
+ Updates the status of all resource calendar events for an appointment. Iterates through `appointment.resourceBookings` and updates each event's `status` and `updatedAt`.
31
+
32
+ **Firestore path:** `clinics/{clinicBranchId}/resources/{resourceId}/instances/{instanceId}/calendar/{calendarEventId}`
33
+
34
+ #### `updateResourceBookingEventsTime(appointment, newEventTime)`
35
+ Updates the time range of all resource calendar events (used during reschedule). Updates `eventTime.start`, `eventTime.end`, and `updatedAt`.
36
+
37
+ #### `deleteResourceBookingEvents(appointment)`
38
+ Deletes all resource calendar events for a deleted appointment. Uses a Firestore batch to delete all events atomically.
39
+
40
+ **Lifecycle integration:** Called by `AppointmentAggregationService` during appointment transitions:
41
+
42
+ | Transition | Action |
43
+ |---|---|
44
+ | PENDING → CONFIRMED | `updateResourceBookingEventsStatus(after, CONFIRMED)` |
45
+ | RESCHEDULED → CONFIRMED | `updateResourceBookingEventsStatus(after, CONFIRMED)` |
46
+ | Any → CANCELLED / NO_SHOW | `updateResourceBookingEventsStatus(after, CANCELLED)` |
47
+ | Any → COMPLETED | `updateResourceBookingEventsStatus(after, COMPLETED)` |
48
+ | RESCHEDULED_BY_CLINIC | `updateResourceBookingEventsTime()` + `updateResourceBookingEventsStatus(after, PENDING)` |
49
+ | Time change (same status) | `updateResourceBookingEventsTime()` |
50
+ | Appointment deleted | `deleteResourceBookingEvents()` |
51
+
52
+ **Safety:** All methods check for `appointment.resourceBookings` before proceeding. If no resource bookings exist (backward-compatible appointments), the methods return early.
53
+
54
+ ---
55
+
56
+ ### `synced-calendars.service.ts`
57
+
58
+ Manages the synchronization of calendars with external providers. It should not be used on the client side.
59
+
60
+ ## Export
61
+
62
+ `index.ts` exports `CalendarAdminService` and `ResourceCalendarAdminService`.
@@ -1 +1,2 @@
1
1
  export * from "./calendar.admin.service";
2
+ export * from "./resource-calendar.admin";
@@ -0,0 +1,198 @@
1
+ import * as admin from "firebase-admin";
2
+ import { Appointment } from "../../types/appointment";
3
+ import { CalendarEventStatus } from "../../types/calendar";
4
+ import { Logger } from "../logger";
5
+ import { CLINICS_COLLECTION } from "../../types/clinic";
6
+ import {
7
+ RESOURCES_COLLECTION,
8
+ RESOURCE_INSTANCES_SUBCOLLECTION,
9
+ RESOURCE_CALENDAR_SUBCOLLECTION,
10
+ ResourceBookingInfo,
11
+ } from "../../types/resource";
12
+
13
+ /**
14
+ * @class ResourceCalendarAdminService
15
+ * @description Handles administrative tasks for resource calendar events linked to appointments,
16
+ * such as status updates, time changes, or deletions during appointment lifecycle events.
17
+ */
18
+ export class ResourceCalendarAdminService {
19
+ private db: admin.firestore.Firestore;
20
+
21
+ constructor(firestore?: admin.firestore.Firestore) {
22
+ this.db = firestore || admin.firestore();
23
+ Logger.info("[ResourceCalendarAdminService] Initialized.");
24
+ }
25
+
26
+ /**
27
+ * Builds the Firestore document path for a resource calendar event.
28
+ */
29
+ private getResourceCalendarEventPath(
30
+ clinicBranchId: string,
31
+ booking: ResourceBookingInfo
32
+ ): string {
33
+ return `${CLINICS_COLLECTION}/${clinicBranchId}/${RESOURCES_COLLECTION}/${booking.resourceId}/${RESOURCE_INSTANCES_SUBCOLLECTION}/${booking.resourceInstanceId}/${RESOURCE_CALENDAR_SUBCOLLECTION}/${booking.calendarEventId}`;
34
+ }
35
+
36
+ /**
37
+ * Updates the status of all resource calendar events associated with a given appointment.
38
+ *
39
+ * @param appointment - The appointment object containing resourceBookings.
40
+ * @param newStatus - The new CalendarEventStatus to set.
41
+ */
42
+ async updateResourceBookingEventsStatus(
43
+ appointment: Appointment,
44
+ newStatus: CalendarEventStatus
45
+ ): Promise<void> {
46
+ const resourceBookings = appointment.resourceBookings;
47
+ if (!resourceBookings || resourceBookings.length === 0) {
48
+ Logger.debug(
49
+ `[ResourceCalendarAdminService] No resource bookings on appointment ${appointment.id}, skipping status update.`
50
+ );
51
+ return;
52
+ }
53
+
54
+ Logger.info(
55
+ `[ResourceCalendarAdminService] Updating ${resourceBookings.length} resource calendar event(s) to ${newStatus} for appointment ${appointment.id}`
56
+ );
57
+
58
+ const batch = this.db.batch();
59
+ const serverTimestamp = admin.firestore.FieldValue.serverTimestamp();
60
+
61
+ for (const booking of resourceBookings) {
62
+ const eventPath = this.getResourceCalendarEventPath(
63
+ appointment.clinicBranchId,
64
+ booking
65
+ );
66
+ const eventRef = this.db.doc(eventPath);
67
+ batch.update(eventRef, {
68
+ status: newStatus,
69
+ updatedAt: serverTimestamp,
70
+ });
71
+ Logger.debug(
72
+ `[ResourceCalendarAdminService] Added status update for resource event ${booking.calendarEventId} (${booking.resourceName} / ${booking.resourceInstanceLabel})`
73
+ );
74
+ }
75
+
76
+ try {
77
+ await batch.commit();
78
+ Logger.info(
79
+ `[ResourceCalendarAdminService] Successfully updated ${resourceBookings.length} resource calendar event statuses for appointment ${appointment.id}.`
80
+ );
81
+ } catch (error) {
82
+ Logger.error(
83
+ `[ResourceCalendarAdminService] Error updating resource calendar event statuses for appointment ${appointment.id}:`,
84
+ error
85
+ );
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Updates the eventTime (start and end) of all resource calendar events
92
+ * associated with a given appointment.
93
+ *
94
+ * @param appointment - The appointment object containing resourceBookings.
95
+ * @param newEventTime - The new event time with admin Timestamps.
96
+ */
97
+ async updateResourceBookingEventsTime(
98
+ appointment: Appointment,
99
+ newEventTime: {
100
+ start: admin.firestore.Timestamp;
101
+ end: admin.firestore.Timestamp;
102
+ }
103
+ ): Promise<void> {
104
+ const resourceBookings = appointment.resourceBookings;
105
+ if (!resourceBookings || resourceBookings.length === 0) {
106
+ Logger.debug(
107
+ `[ResourceCalendarAdminService] No resource bookings on appointment ${appointment.id}, skipping time update.`
108
+ );
109
+ return;
110
+ }
111
+
112
+ Logger.info(
113
+ `[ResourceCalendarAdminService] Updating ${resourceBookings.length} resource calendar event time(s) for appointment ${appointment.id}`
114
+ );
115
+
116
+ const batch = this.db.batch();
117
+ const serverTimestamp = admin.firestore.FieldValue.serverTimestamp();
118
+
119
+ for (const booking of resourceBookings) {
120
+ const eventPath = this.getResourceCalendarEventPath(
121
+ appointment.clinicBranchId,
122
+ booking
123
+ );
124
+ const eventRef = this.db.doc(eventPath);
125
+ batch.update(eventRef, {
126
+ eventTime: {
127
+ start: newEventTime.start,
128
+ end: newEventTime.end,
129
+ },
130
+ updatedAt: serverTimestamp,
131
+ });
132
+ Logger.debug(
133
+ `[ResourceCalendarAdminService] Added time update for resource event ${booking.calendarEventId} (${booking.resourceName} / ${booking.resourceInstanceLabel})`
134
+ );
135
+ }
136
+
137
+ try {
138
+ await batch.commit();
139
+ Logger.info(
140
+ `[ResourceCalendarAdminService] Successfully updated ${resourceBookings.length} resource calendar event times for appointment ${appointment.id}.`
141
+ );
142
+ } catch (error) {
143
+ Logger.error(
144
+ `[ResourceCalendarAdminService] Error updating resource calendar event times for appointment ${appointment.id}:`,
145
+ error
146
+ );
147
+ throw error;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Deletes all resource calendar events associated with a given appointment.
153
+ *
154
+ * @param appointment - The appointment object containing resourceBookings.
155
+ */
156
+ async deleteResourceBookingEvents(
157
+ appointment: Appointment
158
+ ): Promise<void> {
159
+ const resourceBookings = appointment.resourceBookings;
160
+ if (!resourceBookings || resourceBookings.length === 0) {
161
+ Logger.debug(
162
+ `[ResourceCalendarAdminService] No resource bookings on appointment ${appointment.id}, skipping deletion.`
163
+ );
164
+ return;
165
+ }
166
+
167
+ Logger.info(
168
+ `[ResourceCalendarAdminService] Deleting ${resourceBookings.length} resource calendar event(s) for appointment ${appointment.id}`
169
+ );
170
+
171
+ const batch = this.db.batch();
172
+
173
+ for (const booking of resourceBookings) {
174
+ const eventPath = this.getResourceCalendarEventPath(
175
+ appointment.clinicBranchId,
176
+ booking
177
+ );
178
+ const eventRef = this.db.doc(eventPath);
179
+ batch.delete(eventRef);
180
+ Logger.debug(
181
+ `[ResourceCalendarAdminService] Added deletion for resource event ${booking.calendarEventId} (${booking.resourceName} / ${booking.resourceInstanceLabel})`
182
+ );
183
+ }
184
+
185
+ try {
186
+ await batch.commit();
187
+ Logger.info(
188
+ `[ResourceCalendarAdminService] Successfully deleted ${resourceBookings.length} resource calendar events for appointment ${appointment.id}.`
189
+ );
190
+ } catch (error) {
191
+ Logger.error(
192
+ `[ResourceCalendarAdminService] Error deleting resource calendar events for appointment ${appointment.id}:`,
193
+ error
194
+ );
195
+ throw error;
196
+ }
197
+ }
198
+ }
@@ -12,6 +12,7 @@ export {
12
12
  PERMISSION_KEYS,
13
13
  TIER_CONFIG,
14
14
  DEFAULT_ROLE_PERMISSIONS,
15
+ DEFAULT_PLAN_CONFIG,
15
16
  resolveEffectiveTier,
16
17
  } from "./tiers.config";
17
18
  export type { PermissionKey } from "./tiers.config";
@@ -1,5 +1,6 @@
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.
@@ -27,6 +28,12 @@ export const PERMISSION_KEYS = {
27
28
  'procedures.edit': true,
28
29
  'procedures.delete': true,
29
30
 
31
+ // Resources
32
+ 'resources.view': true,
33
+ 'resources.create': true,
34
+ 'resources.edit': true,
35
+ 'resources.delete': true,
36
+
30
37
  // Patients
31
38
  'patients.view': true,
32
39
  'patients.edit': true,
@@ -57,11 +64,11 @@ export type PermissionKey = keyof typeof PERMISSION_KEYS;
57
64
  * - Appointments and messages are NEVER limited (bad patient UX)
58
65
  * - Providers are per branch, not per clinic group
59
66
  * - Procedures are per provider (each provider gets this allowance)
60
- * - Add-ons available on Connect/Pro: +1 branch, +1 provider, +10 procedures
67
+ * - Add-ons available on Connect/Pro: +1 branch, +1 provider, +5 treatments
61
68
  *
62
- * Free: 1 branch, 1 provider/branch, 3 procedures/provider
63
- * Connect (CHF 59/mo): 1 branch, 3 providers/branch, 10 procedures/provider
64
- * Pro (CHF 149/mo): 3 branches, 10 providers/branch, 20 procedures/provider
69
+ * Free: 1 branch, 1 provider/branch, 3 treatments/provider
70
+ * Connect (CHF 199/mo): 1 branch, 3 providers/branch, 10 treatments/provider
71
+ * Pro (CHF 449/mo): 3 branches, 5 providers/branch, 20 treatments/provider
65
72
  */
66
73
  export const TIER_CONFIG: Record<string, TierConfig> = {
67
74
  free: {
@@ -86,7 +93,7 @@ export const TIER_CONFIG: Record<string, TierConfig> = {
86
93
  tier: 'pro',
87
94
  name: 'Pro',
88
95
  limits: {
89
- maxProvidersPerBranch: 10,
96
+ maxProvidersPerBranch: 5,
90
97
  maxProceduresPerProvider: 20,
91
98
  maxBranches: 3,
92
99
  },
@@ -111,6 +118,10 @@ export const DEFAULT_ROLE_PERMISSIONS: Record<ClinicRole, Record<string, boolean
111
118
  'procedures.create': true,
112
119
  'procedures.edit': true,
113
120
  'procedures.delete': true,
121
+ 'resources.view': true,
122
+ 'resources.create': true,
123
+ 'resources.edit': true,
124
+ 'resources.delete': true,
114
125
  'patients.view': true,
115
126
  'patients.edit': true,
116
127
  'providers.view': true,
@@ -133,6 +144,10 @@ export const DEFAULT_ROLE_PERMISSIONS: Record<ClinicRole, Record<string, boolean
133
144
  'procedures.create': true,
134
145
  'procedures.edit': true,
135
146
  'procedures.delete': true,
147
+ 'resources.view': true,
148
+ 'resources.create': true,
149
+ 'resources.edit': true,
150
+ 'resources.delete': true,
136
151
  'patients.view': true,
137
152
  'patients.edit': true,
138
153
  'providers.view': true,
@@ -155,6 +170,10 @@ export const DEFAULT_ROLE_PERMISSIONS: Record<ClinicRole, Record<string, boolean
155
170
  'procedures.create': false,
156
171
  'procedures.edit': false,
157
172
  'procedures.delete': false,
173
+ 'resources.view': true,
174
+ 'resources.create': false,
175
+ 'resources.edit': false,
176
+ 'resources.delete': false,
158
177
  'patients.view': true,
159
178
  'patients.edit': true,
160
179
  'providers.view': true,
@@ -177,6 +196,10 @@ export const DEFAULT_ROLE_PERMISSIONS: Record<ClinicRole, Record<string, boolean
177
196
  'procedures.create': false,
178
197
  'procedures.edit': false,
179
198
  'procedures.delete': false,
199
+ 'resources.view': true,
200
+ 'resources.create': false,
201
+ 'resources.edit': false,
202
+ 'resources.delete': false,
180
203
  'patients.view': true,
181
204
  'patients.edit': false,
182
205
  'providers.view': true,
@@ -199,6 +222,10 @@ export const DEFAULT_ROLE_PERMISSIONS: Record<ClinicRole, Record<string, boolean
199
222
  'procedures.create': false,
200
223
  'procedures.edit': false,
201
224
  'procedures.delete': false,
225
+ 'resources.view': true,
226
+ 'resources.create': false,
227
+ 'resources.edit': false,
228
+ 'resources.delete': false,
202
229
  'patients.view': true,
203
230
  'patients.edit': false,
204
231
  'providers.view': true,
@@ -210,6 +237,95 @@ export const DEFAULT_ROLE_PERMISSIONS: Record<ClinicRole, Record<string, boolean
210
237
  },
211
238
  };
212
239
 
240
+ /**
241
+ * Default PlanConfigDocument — used as fallback when Firestore `system/planConfig`
242
+ * doesn't exist or is unavailable. Also used as the seed value.
243
+ *
244
+ * Stripe price IDs reference the SKS Innovation SA sandbox account.
245
+ */
246
+ export const DEFAULT_PLAN_CONFIG: PlanConfigDocument = {
247
+ version: 1,
248
+ updatedAt: null,
249
+ updatedBy: 'system',
250
+
251
+ tiers: {
252
+ free: {
253
+ name: 'Free',
254
+ limits: { maxProvidersPerBranch: 1, maxProceduresPerProvider: 3, maxBranches: 1 },
255
+ },
256
+ connect: {
257
+ name: 'Connect',
258
+ limits: { maxProvidersPerBranch: 3, maxProceduresPerProvider: 10, maxBranches: 1 },
259
+ },
260
+ pro: {
261
+ name: 'Pro',
262
+ limits: { maxProvidersPerBranch: 5, maxProceduresPerProvider: 20, maxBranches: 3 },
263
+ },
264
+ },
265
+
266
+ plans: {
267
+ FREE: {
268
+ priceId: null,
269
+ priceMonthly: 0,
270
+ currency: 'CHF',
271
+ description: 'Get started with basic access',
272
+ stripeProductId: null,
273
+ },
274
+ CONNECT: {
275
+ priceId: 'price_1TD0l7AaFI0fqFWNC8Lg3QJG',
276
+ priceMonthly: 199,
277
+ currency: 'CHF',
278
+ description: 'Consultation tools and integrated booking',
279
+ stripeProductId: null,
280
+ },
281
+ PRO: {
282
+ priceId: 'price_1TD0lEAaFI0fqFWN3tzCrfUb',
283
+ priceMonthly: 449,
284
+ currency: 'CHF',
285
+ description: 'Complete clinic management with CRM and analytics',
286
+ stripeProductId: null,
287
+ },
288
+ },
289
+
290
+ addons: {
291
+ seat: {
292
+ priceId: 'price_1TD0lKAaFI0fqFWNH9pQgTzI',
293
+ pricePerUnit: 39,
294
+ currency: 'CHF',
295
+ allowedTiers: ['connect', 'pro'],
296
+ },
297
+ branch: {
298
+ connect: { priceId: 'price_1TD0lSAaFI0fqFWNjsPvzW1T', pricePerUnit: 119, currency: 'CHF' },
299
+ pro: { priceId: 'price_1TD0lTAaFI0fqFWNYxUnvUxH', pricePerUnit: 279, currency: 'CHF' },
300
+ allowedTiers: ['connect', 'pro'],
301
+ },
302
+ procedure: {
303
+ priceId: 'price_1TCL3eAaFI0fqFWNJVuMYjii',
304
+ pricePerBlock: 19,
305
+ proceduresPerBlock: 5,
306
+ currency: 'CHF',
307
+ allowedTiers: ['connect', 'pro'],
308
+ },
309
+ },
310
+
311
+ priceToModel: {
312
+ 'price_1TD0l7AaFI0fqFWNC8Lg3QJG': 'connect',
313
+ 'price_1TD0lEAaFI0fqFWN3tzCrfUb': 'pro',
314
+ },
315
+
316
+ priceToType: {
317
+ 'price_1TD0l7AaFI0fqFWNC8Lg3QJG': 'CONNECT',
318
+ 'price_1TD0lEAaFI0fqFWN3tzCrfUb': 'PRO',
319
+ },
320
+
321
+ addonPriceIds: [
322
+ 'price_1TD0lKAaFI0fqFWNH9pQgTzI', // seat
323
+ 'price_1TD0lSAaFI0fqFWNjsPvzW1T', // branch connect
324
+ 'price_1TD0lTAaFI0fqFWNYxUnvUxH', // branch pro
325
+ 'price_1TCL3eAaFI0fqFWNJVuMYjii', // procedure
326
+ ],
327
+ };
328
+
213
329
  /**
214
330
  * Maps subscription model strings to tier keys.
215
331
  */
@@ -9,6 +9,7 @@ export * from "./notifications";
9
9
  export * from "./patient";
10
10
  export * from "./practitioner";
11
11
  export * from "./procedure";
12
+ export * from "./resource";
12
13
  export * from "./reviews";
13
14
  export * from "./user";
14
15
  export * from "./base.service";