@blackcode_sa/metaestetics-api 1.15.10 → 1.15.14
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 +13 -0
- package/dist/admin/index.d.ts +13 -0
- package/dist/index.d.mts +36 -31
- package/dist/index.d.ts +36 -31
- package/dist/index.js +75 -139
- package/dist/index.mjs +75 -136
- package/package.json +1 -1
- package/src/config/tiers.config.ts +16 -20
- package/src/services/practitioner/practitioner.service.ts +3 -3
- package/src/services/procedure/procedure.service.ts +6 -4
- package/src/services/tier-enforcement.ts +106 -156
- package/src/types/clinic/index.ts +13 -0
- package/src/types/clinic/rbac.types.ts +6 -5
|
@@ -53,21 +53,23 @@ export type PermissionKey = keyof typeof PERMISSION_KEYS;
|
|
|
53
53
|
* All features are available on every tier — tiers differ only in usage caps.
|
|
54
54
|
* -1 means unlimited.
|
|
55
55
|
*
|
|
56
|
-
*
|
|
57
|
-
* -
|
|
58
|
-
* -
|
|
59
|
-
* -
|
|
56
|
+
* Updated 2026-03-17 (client meeting):
|
|
57
|
+
* - Appointments and messages are NEVER limited (bad patient UX)
|
|
58
|
+
* - Providers are per branch, not per clinic group
|
|
59
|
+
* - Procedures are per provider (each provider gets this allowance)
|
|
60
|
+
* - Add-ons available on Connect/Pro: +1 branch, +1 provider, +10 procedures
|
|
61
|
+
*
|
|
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
|
|
60
65
|
*/
|
|
61
66
|
export const TIER_CONFIG: Record<string, TierConfig> = {
|
|
62
67
|
free: {
|
|
63
68
|
tier: 'free',
|
|
64
69
|
name: 'Free',
|
|
65
70
|
limits: {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
maxAppointmentsPerMonth: 3,
|
|
69
|
-
maxMessagesPerMonth: 10,
|
|
70
|
-
maxStaff: 1,
|
|
71
|
+
maxProvidersPerBranch: 1,
|
|
72
|
+
maxProceduresPerProvider: 3,
|
|
71
73
|
maxBranches: 1,
|
|
72
74
|
},
|
|
73
75
|
},
|
|
@@ -75,11 +77,8 @@ export const TIER_CONFIG: Record<string, TierConfig> = {
|
|
|
75
77
|
tier: 'connect',
|
|
76
78
|
name: 'Connect',
|
|
77
79
|
limits: {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
maxAppointmentsPerMonth: 100,
|
|
81
|
-
maxMessagesPerMonth: -1,
|
|
82
|
-
maxStaff: 5,
|
|
80
|
+
maxProvidersPerBranch: 3,
|
|
81
|
+
maxProceduresPerProvider: 10,
|
|
83
82
|
maxBranches: 1,
|
|
84
83
|
},
|
|
85
84
|
},
|
|
@@ -87,12 +86,9 @@ export const TIER_CONFIG: Record<string, TierConfig> = {
|
|
|
87
86
|
tier: 'pro',
|
|
88
87
|
name: 'Pro',
|
|
89
88
|
limits: {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
maxMessagesPerMonth: -1,
|
|
94
|
-
maxStaff: -1,
|
|
95
|
-
maxBranches: -1,
|
|
89
|
+
maxProvidersPerBranch: 10,
|
|
90
|
+
maxProceduresPerProvider: 20,
|
|
91
|
+
maxBranches: 3,
|
|
96
92
|
},
|
|
97
93
|
},
|
|
98
94
|
};
|
|
@@ -192,7 +192,7 @@ export class PractitionerService extends BaseService {
|
|
|
192
192
|
if (clinicSnap.exists()) {
|
|
193
193
|
const clinicGroupId = (clinicSnap.data() as any).clinicGroupId;
|
|
194
194
|
if (clinicGroupId) {
|
|
195
|
-
await enforceProviderLimit(this.db, clinicGroupId);
|
|
195
|
+
await enforceProviderLimit(this.db, clinicGroupId, validData.clinics[0]);
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
}
|
|
@@ -296,9 +296,9 @@ export class PractitionerService extends BaseService {
|
|
|
296
296
|
throw new Error(`Clinic ${clinicId} not found`);
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
// Enforce tier limit before creating draft practitioner
|
|
299
|
+
// Enforce tier limit before creating draft practitioner (per-branch)
|
|
300
300
|
if (clinic.clinicGroupId) {
|
|
301
|
-
await enforceProviderLimit(this.db, clinic.clinicGroupId);
|
|
301
|
+
await enforceProviderLimit(this.db, clinic.clinicGroupId, clinicId);
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
// Make sure the primary clinic (clinicId) is always included
|
|
@@ -363,8 +363,8 @@ export class ProcedureService extends BaseService {
|
|
|
363
363
|
}
|
|
364
364
|
const clinic = clinicSnapshot.data() as Clinic; // Assert type
|
|
365
365
|
|
|
366
|
-
// Enforce tier limit before creating procedure
|
|
367
|
-
await enforceProcedureLimit(this.db, clinic.clinicGroupId);
|
|
366
|
+
// Enforce tier limit before creating procedure (per-provider, per-branch)
|
|
367
|
+
await enforceProcedureLimit(this.db, clinic.clinicGroupId, validatedData.clinicBranchId, validatedData.practitionerId);
|
|
368
368
|
|
|
369
369
|
const practitionerRef = doc(this.db, PRACTITIONERS_COLLECTION, validatedData.practitionerId);
|
|
370
370
|
const practitionerSnapshot = await getDoc(practitionerRef);
|
|
@@ -885,8 +885,10 @@ export class ProcedureService extends BaseService {
|
|
|
885
885
|
}
|
|
886
886
|
const clinic = clinicSnapshot.data() as Clinic;
|
|
887
887
|
|
|
888
|
-
// Enforce tier limit
|
|
889
|
-
|
|
888
|
+
// Enforce tier limit for each practitioner (per-provider, per-branch)
|
|
889
|
+
for (const practitionerId of practitionerIds) {
|
|
890
|
+
await enforceProcedureLimit(this.db, clinic.clinicGroupId, validatedData.clinicBranchId, practitionerId);
|
|
891
|
+
}
|
|
890
892
|
|
|
891
893
|
// 3. Handle media uploads once for efficiency
|
|
892
894
|
let processedPhotos: string[] = [];
|
|
@@ -29,9 +29,13 @@ export class TierLimitError extends Error {
|
|
|
29
29
|
maxAllowed: number
|
|
30
30
|
) {
|
|
31
31
|
const tierLabel = currentTier.charAt(0).toUpperCase() + currentTier.slice(1);
|
|
32
|
+
const canBuyAddons = currentTier === 'connect' || currentTier === 'pro';
|
|
33
|
+
const actionHint = canBuyAddons
|
|
34
|
+
? 'Please upgrade your plan or purchase an add-on.'
|
|
35
|
+
: 'Please upgrade to Connect or Pro to add more.';
|
|
32
36
|
super(
|
|
33
37
|
`Your ${tierLabel} plan allows a maximum of ${maxAllowed} ${resource}. ` +
|
|
34
|
-
`You currently have ${currentCount}.
|
|
38
|
+
`You currently have ${currentCount}. ${actionHint}`
|
|
35
39
|
);
|
|
36
40
|
this.name = 'TierLimitError';
|
|
37
41
|
this.currentTier = currentTier;
|
|
@@ -42,85 +46,84 @@ export class TierLimitError extends Error {
|
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
/**
|
|
45
|
-
*
|
|
46
|
-
* Returns the resolved tier string (e.g., 'free', 'connect', 'pro').
|
|
49
|
+
* Reads the clinic group doc and returns tier + billing data in one call.
|
|
47
50
|
*/
|
|
48
|
-
|
|
51
|
+
async function getClinicGroupTierData(
|
|
49
52
|
db: Firestore,
|
|
50
53
|
clinicGroupId: string
|
|
51
|
-
): Promise<
|
|
54
|
+
): Promise<{
|
|
55
|
+
tier: string;
|
|
56
|
+
billing: Record<string, any> | undefined;
|
|
57
|
+
}> {
|
|
52
58
|
const groupRef = doc(db, CLINIC_GROUPS_COLLECTION, clinicGroupId);
|
|
53
59
|
const groupSnap = await getDoc(groupRef);
|
|
54
60
|
if (!groupSnap.exists()) {
|
|
55
61
|
throw new Error(`Clinic group ${clinicGroupId} not found`);
|
|
56
62
|
}
|
|
57
|
-
const
|
|
58
|
-
|
|
63
|
+
const data = groupSnap.data();
|
|
64
|
+
const subscriptionModel = data.subscriptionModel || 'no_subscription';
|
|
65
|
+
return {
|
|
66
|
+
tier: resolveEffectiveTier(subscriptionModel),
|
|
67
|
+
billing: data.billing,
|
|
68
|
+
};
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
/**
|
|
62
|
-
*
|
|
72
|
+
* Fetches the effective tier for a clinic group.
|
|
63
73
|
*/
|
|
64
|
-
async function
|
|
74
|
+
export async function getEffectiveTier(
|
|
65
75
|
db: Firestore,
|
|
66
76
|
clinicGroupId: string
|
|
67
|
-
): Promise<
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
where('isActive', '==', true)
|
|
72
|
-
);
|
|
73
|
-
const clinicsSnap = await getDocs(clinicsQuery);
|
|
74
|
-
const clinicIds = clinicsSnap.docs.map(d => d.id);
|
|
75
|
-
|
|
76
|
-
if (clinicIds.length === 0) return 0;
|
|
77
|
+
): Promise<string> {
|
|
78
|
+
const { tier } = await getClinicGroupTierData(db, clinicGroupId);
|
|
79
|
+
return tier;
|
|
80
|
+
}
|
|
77
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Counts active providers (practitioners) assigned to a specific clinic branch.
|
|
84
|
+
*/
|
|
85
|
+
async function countProvidersInBranch(
|
|
86
|
+
db: Firestore,
|
|
87
|
+
branchId: string
|
|
88
|
+
): Promise<number> {
|
|
78
89
|
const practitionersQuery = query(
|
|
79
90
|
collection(db, PRACTITIONERS_COLLECTION),
|
|
80
91
|
where('isActive', '==', true)
|
|
81
92
|
);
|
|
82
93
|
const practitionersSnap = await getDocs(practitionersQuery);
|
|
83
94
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return clinics.some(c => clinicIdSet.has(c));
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
return uniqueProviders.length;
|
|
95
|
+
return practitionersSnap.docs.filter(d => {
|
|
96
|
+
const clinics: string[] = d.data().clinics || [];
|
|
97
|
+
return clinics.includes(branchId);
|
|
98
|
+
}).length;
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
/**
|
|
95
|
-
* Counts active procedures
|
|
102
|
+
* Counts active procedures for a specific provider in a specific branch.
|
|
103
|
+
* Uses unique technologyId to avoid double-counting same procedure type.
|
|
96
104
|
*/
|
|
97
|
-
async function
|
|
105
|
+
async function countProceduresForProvider(
|
|
98
106
|
db: Firestore,
|
|
99
|
-
|
|
107
|
+
branchId: string,
|
|
108
|
+
providerId: string
|
|
100
109
|
): Promise<number> {
|
|
101
|
-
const
|
|
102
|
-
collection(db,
|
|
103
|
-
where('
|
|
110
|
+
const proceduresQuery = query(
|
|
111
|
+
collection(db, PROCEDURES_COLLECTION),
|
|
112
|
+
where('clinicBranchId', '==', branchId),
|
|
113
|
+
where('practitionerId', '==', providerId),
|
|
104
114
|
where('isActive', '==', true)
|
|
105
115
|
);
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
collection(db, PROCEDURES_COLLECTION),
|
|
116
|
-
where('clinicBranchId', 'in', batch),
|
|
117
|
-
where('isActive', '==', true)
|
|
118
|
-
);
|
|
119
|
-
const proceduresSnap = await getDocs(proceduresQuery);
|
|
120
|
-
totalProcedures += proceduresSnap.size;
|
|
121
|
-
}
|
|
116
|
+
const proceduresSnap = await getDocs(proceduresQuery);
|
|
117
|
+
|
|
118
|
+
const uniqueTechnologyIds = new Set<string>();
|
|
119
|
+
proceduresSnap.docs.forEach(d => {
|
|
120
|
+
const technologyId = d.data().technologyId;
|
|
121
|
+
if (technologyId) {
|
|
122
|
+
uniqueTechnologyIds.add(technologyId);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
122
125
|
|
|
123
|
-
return
|
|
126
|
+
return uniqueTechnologyIds.size;
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
/**
|
|
@@ -140,151 +143,98 @@ async function countBranchesInGroup(
|
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
/**
|
|
143
|
-
*
|
|
144
|
-
* Throws TierLimitError if the limit would be exceeded.
|
|
145
|
-
*/
|
|
146
|
-
export async function enforceProviderLimit(
|
|
147
|
-
db: Firestore,
|
|
148
|
-
clinicGroupId: string
|
|
149
|
-
): Promise<void> {
|
|
150
|
-
const tier = await getEffectiveTier(db, clinicGroupId);
|
|
151
|
-
const config = TIER_CONFIG[tier];
|
|
152
|
-
if (!config) return;
|
|
153
|
-
|
|
154
|
-
const max = config.limits.maxProviders;
|
|
155
|
-
if (max === -1) return;
|
|
156
|
-
|
|
157
|
-
const currentCount = await countProvidersInGroup(db, clinicGroupId);
|
|
158
|
-
if (currentCount + 1 > max) {
|
|
159
|
-
throw new TierLimitError('providers', tier, currentCount, max);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Enforces tier limit before creating a procedure.
|
|
165
|
-
* @param count - Number of procedures being created (default 1, higher for bulk)
|
|
146
|
+
* Resolves the clinicGroupId for a given branch.
|
|
166
147
|
*/
|
|
167
|
-
|
|
148
|
+
async function resolveClinicGroupId(
|
|
168
149
|
db: Firestore,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const max = config.limits.maxProcedures;
|
|
177
|
-
if (max === -1) return;
|
|
178
|
-
|
|
179
|
-
const currentCount = await countProceduresInGroup(db, clinicGroupId);
|
|
180
|
-
if (currentCount + count > max) {
|
|
181
|
-
throw new TierLimitError('procedures', tier, currentCount, max);
|
|
150
|
+
branchId: string
|
|
151
|
+
): Promise<string> {
|
|
152
|
+
const clinicRef = doc(db, CLINICS_COLLECTION, branchId);
|
|
153
|
+
const clinicSnap = await getDoc(clinicRef);
|
|
154
|
+
if (!clinicSnap.exists()) {
|
|
155
|
+
throw new Error(`Clinic branch ${branchId} not found`);
|
|
182
156
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
* Enforces tier limit before creating a clinic branch.
|
|
187
|
-
*/
|
|
188
|
-
export async function enforceBranchLimit(
|
|
189
|
-
db: Firestore,
|
|
190
|
-
clinicGroupId: string
|
|
191
|
-
): Promise<void> {
|
|
192
|
-
const tier = await getEffectiveTier(db, clinicGroupId);
|
|
193
|
-
const config = TIER_CONFIG[tier];
|
|
194
|
-
if (!config) return;
|
|
195
|
-
|
|
196
|
-
const max = config.limits.maxBranches;
|
|
197
|
-
if (max === -1) return;
|
|
198
|
-
|
|
199
|
-
const currentCount = await countBranchesInGroup(db, clinicGroupId);
|
|
200
|
-
if (currentCount + 1 > max) {
|
|
201
|
-
throw new TierLimitError('clinic branches', tier, currentCount, max);
|
|
157
|
+
const clinicGroupId = clinicSnap.data().clinicGroupId;
|
|
158
|
+
if (!clinicGroupId) {
|
|
159
|
+
throw new Error(`Clinic branch ${branchId} has no clinicGroupId`);
|
|
202
160
|
}
|
|
161
|
+
return clinicGroupId;
|
|
203
162
|
}
|
|
204
163
|
|
|
205
164
|
/**
|
|
206
|
-
* Enforces tier limit before
|
|
165
|
+
* Enforces tier limit before adding a provider to a specific branch.
|
|
166
|
+
* Add-on aware: effective limit = base + per-branch extraProviders.
|
|
207
167
|
*/
|
|
208
|
-
export async function
|
|
168
|
+
export async function enforceProviderLimit(
|
|
209
169
|
db: Firestore,
|
|
210
|
-
clinicGroupId: string
|
|
170
|
+
clinicGroupId: string,
|
|
171
|
+
branchId: string
|
|
211
172
|
): Promise<void> {
|
|
212
|
-
const tier = await
|
|
173
|
+
const { tier, billing } = await getClinicGroupTierData(db, clinicGroupId);
|
|
213
174
|
const config = TIER_CONFIG[tier];
|
|
214
175
|
if (!config) return;
|
|
215
176
|
|
|
216
|
-
const
|
|
217
|
-
if (
|
|
177
|
+
const baseMax = config.limits.maxProvidersPerBranch;
|
|
178
|
+
if (baseMax === -1) return;
|
|
218
179
|
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
where('isActive', '==', true)
|
|
223
|
-
);
|
|
224
|
-
const staffSnap = await getDocs(staffQuery);
|
|
225
|
-
const currentCount = staffSnap.size;
|
|
180
|
+
const addOns = billing?.addOns || {};
|
|
181
|
+
const extraProviders = addOns[branchId]?.extraProviders || 0;
|
|
182
|
+
const effectiveMax = baseMax + extraProviders;
|
|
226
183
|
|
|
227
|
-
|
|
228
|
-
|
|
184
|
+
const currentCount = await countProvidersInBranch(db, branchId);
|
|
185
|
+
if (currentCount + 1 > effectiveMax) {
|
|
186
|
+
throw new TierLimitError('providers in this branch', tier, currentCount, effectiveMax);
|
|
229
187
|
}
|
|
230
188
|
}
|
|
231
189
|
|
|
232
190
|
/**
|
|
233
|
-
* Enforces tier limit before creating
|
|
234
|
-
*
|
|
191
|
+
* Enforces tier limit before creating a procedure for a provider in a branch.
|
|
192
|
+
* Add-on aware: effective limit = base + (procedureBlocks × 10).
|
|
235
193
|
*/
|
|
236
|
-
export async function
|
|
194
|
+
export async function enforceProcedureLimit(
|
|
237
195
|
db: Firestore,
|
|
238
|
-
clinicGroupId: string
|
|
196
|
+
clinicGroupId: string,
|
|
197
|
+
branchId: string,
|
|
198
|
+
providerId: string,
|
|
199
|
+
count: number = 1
|
|
239
200
|
): Promise<void> {
|
|
240
|
-
const tier = await
|
|
201
|
+
const { tier, billing } = await getClinicGroupTierData(db, clinicGroupId);
|
|
241
202
|
const config = TIER_CONFIG[tier];
|
|
242
203
|
if (!config) return;
|
|
243
204
|
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
205
|
+
const baseMax = config.limits.maxProceduresPerProvider;
|
|
206
|
+
if (baseMax === -1) return;
|
|
246
207
|
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
const counterRef = doc(
|
|
251
|
-
db,
|
|
252
|
-
`${CLINIC_GROUPS_COLLECTION}/${clinicGroupId}/usage_counters/${yearMonth}`
|
|
253
|
-
);
|
|
254
|
-
const counterSnap = await getDoc(counterRef);
|
|
255
|
-
const currentCount = counterSnap.exists() ? (counterSnap.data()?.appointmentsCreated || 0) : 0;
|
|
208
|
+
const addOns = billing?.addOns || {};
|
|
209
|
+
const procedureBlocks = addOns[branchId]?.procedureBlocks || 0;
|
|
210
|
+
const effectiveMax = baseMax + (procedureBlocks * 10);
|
|
256
211
|
|
|
257
|
-
|
|
258
|
-
|
|
212
|
+
const currentCount = await countProceduresForProvider(db, branchId, providerId);
|
|
213
|
+
if (currentCount + count > effectiveMax) {
|
|
214
|
+
throw new TierLimitError('procedures for this provider', tier, currentCount, effectiveMax);
|
|
259
215
|
}
|
|
260
216
|
}
|
|
261
217
|
|
|
262
218
|
/**
|
|
263
|
-
* Enforces tier limit before
|
|
264
|
-
*
|
|
219
|
+
* Enforces tier limit before creating a clinic branch.
|
|
220
|
+
* Add-on aware: effective limit = base + branchAddonCount.
|
|
265
221
|
*/
|
|
266
|
-
export async function
|
|
222
|
+
export async function enforceBranchLimit(
|
|
267
223
|
db: Firestore,
|
|
268
224
|
clinicGroupId: string
|
|
269
225
|
): Promise<void> {
|
|
270
|
-
const tier = await
|
|
226
|
+
const { tier, billing } = await getClinicGroupTierData(db, clinicGroupId);
|
|
271
227
|
const config = TIER_CONFIG[tier];
|
|
272
228
|
if (!config) return;
|
|
273
229
|
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
230
|
+
const baseMax = config.limits.maxBranches;
|
|
231
|
+
if (baseMax === -1) return;
|
|
276
232
|
|
|
277
|
-
const
|
|
278
|
-
const
|
|
233
|
+
const branchAddonCount = billing?.branchAddonCount || 0;
|
|
234
|
+
const effectiveMax = baseMax + branchAddonCount;
|
|
279
235
|
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
);
|
|
284
|
-
const counterSnap = await getDoc(counterRef);
|
|
285
|
-
const currentCount = counterSnap.exists() ? (counterSnap.data()?.messagesCount || 0) : 0;
|
|
286
|
-
|
|
287
|
-
if (currentCount + 1 > max) {
|
|
288
|
-
throw new TierLimitError('messages this month', tier, currentCount, max);
|
|
236
|
+
const currentCount = await countBranchesInGroup(db, clinicGroupId);
|
|
237
|
+
if (currentCount + 1 > effectiveMax) {
|
|
238
|
+
throw new TierLimitError('clinic branches', tier, currentCount, effectiveMax);
|
|
289
239
|
}
|
|
290
240
|
}
|
|
@@ -210,6 +210,19 @@ export interface BillingInfo {
|
|
|
210
210
|
updatedAt: Timestamp;
|
|
211
211
|
seatCount?: number;
|
|
212
212
|
seatPriceId?: string;
|
|
213
|
+
/** Number of extra branches purchased (group-level add-on) */
|
|
214
|
+
branchAddonCount?: number;
|
|
215
|
+
branchAddonPriceId?: string;
|
|
216
|
+
/** Total procedure blocks across all branches (for Stripe sync) */
|
|
217
|
+
procedureAddonTotalBlocks?: number;
|
|
218
|
+
procedureAddonPriceId?: string;
|
|
219
|
+
/** Per-branch add-on tracking */
|
|
220
|
+
addOns?: {
|
|
221
|
+
[branchId: string]: {
|
|
222
|
+
extraProviders?: number;
|
|
223
|
+
procedureBlocks?: number;
|
|
224
|
+
};
|
|
225
|
+
};
|
|
213
226
|
}
|
|
214
227
|
|
|
215
228
|
/**
|
|
@@ -41,13 +41,14 @@ export interface RolePermissionConfig {
|
|
|
41
41
|
* Usage limits for a given subscription tier.
|
|
42
42
|
* All features are available on every tier — only usage is capped.
|
|
43
43
|
* -1 means unlimited.
|
|
44
|
+
*
|
|
45
|
+
* Providers and procedures are scoped per branch, not per clinic group.
|
|
46
|
+
* Procedures are per provider (each provider in a branch gets this allowance).
|
|
47
|
+
* Appointments and messages are never limited.
|
|
44
48
|
*/
|
|
45
49
|
export interface TierLimits {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
maxAppointmentsPerMonth: number;
|
|
49
|
-
maxMessagesPerMonth: number;
|
|
50
|
-
maxStaff: number;
|
|
50
|
+
maxProvidersPerBranch: number;
|
|
51
|
+
maxProceduresPerProvider: number;
|
|
51
52
|
maxBranches: number;
|
|
52
53
|
}
|
|
53
54
|
|