@blackcode_sa/metaestetics-api 1.12.72 → 1.13.1

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 (37) hide show
  1. package/dist/admin/index.d.mts +872 -1
  2. package/dist/admin/index.d.ts +872 -1
  3. package/dist/admin/index.js +3604 -356
  4. package/dist/admin/index.mjs +3594 -357
  5. package/dist/index.d.mts +1349 -1
  6. package/dist/index.d.ts +1349 -1
  7. package/dist/index.js +5325 -2141
  8. package/dist/index.mjs +4939 -1767
  9. package/package.json +1 -1
  10. package/src/admin/analytics/analytics.admin.service.ts +278 -0
  11. package/src/admin/analytics/index.ts +2 -0
  12. package/src/admin/index.ts +6 -0
  13. package/src/backoffice/services/analytics.service.proposal.md +4 -0
  14. package/src/services/analytics/ARCHITECTURE.md +199 -0
  15. package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -0
  16. package/src/services/analytics/GROUPED_ANALYTICS.md +501 -0
  17. package/src/services/analytics/QUICK_START.md +393 -0
  18. package/src/services/analytics/README.md +304 -0
  19. package/src/services/analytics/SUMMARY.md +141 -0
  20. package/src/services/analytics/TRENDS.md +380 -0
  21. package/src/services/analytics/USAGE_GUIDE.md +518 -0
  22. package/src/services/analytics/analytics-cloud.service.ts +222 -0
  23. package/src/services/analytics/analytics.service.ts +2142 -0
  24. package/src/services/analytics/index.ts +4 -0
  25. package/src/services/analytics/review-analytics.service.ts +941 -0
  26. package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -0
  27. package/src/services/analytics/utils/cost-calculation.utils.ts +182 -0
  28. package/src/services/analytics/utils/grouping.utils.ts +434 -0
  29. package/src/services/analytics/utils/stored-analytics.utils.ts +347 -0
  30. package/src/services/analytics/utils/time-calculation.utils.ts +186 -0
  31. package/src/services/analytics/utils/trend-calculation.utils.ts +200 -0
  32. package/src/services/index.ts +1 -0
  33. package/src/types/analytics/analytics.types.ts +597 -0
  34. package/src/types/analytics/grouped-analytics.types.ts +173 -0
  35. package/src/types/analytics/index.ts +4 -0
  36. package/src/types/analytics/stored-analytics.types.ts +137 -0
  37. package/src/types/index.ts +3 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.12.72",
4
+ "version": "1.13.1",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -0,0 +1,278 @@
1
+ import * as admin from 'firebase-admin';
2
+ import { AnalyticsService } from '../../services/analytics/analytics.service';
3
+ import {
4
+ AnalyticsDateRange,
5
+ AnalyticsFilters,
6
+ EntityType,
7
+ } from '../../types/analytics';
8
+ import {
9
+ PractitionerAnalytics,
10
+ ProcedureAnalytics,
11
+ TimeEfficiencyMetrics,
12
+ CancellationMetrics,
13
+ NoShowMetrics,
14
+ RevenueMetrics,
15
+ ProductUsageMetrics,
16
+ PatientAnalytics,
17
+ ClinicAnalytics,
18
+ DashboardAnalytics,
19
+ GroupedRevenueMetrics,
20
+ GroupedProductUsageMetrics,
21
+ GroupedTimeEfficiencyMetrics,
22
+ GroupedPatientBehaviorMetrics,
23
+ } from '../../types/analytics';
24
+ import { Appointment } from '../../types/appointment';
25
+ import { APPOINTMENTS_COLLECTION } from '../../types/appointment';
26
+
27
+ /**
28
+ * Admin version of AnalyticsService that uses Firebase Admin SDK
29
+ * This is intended for use in Cloud Functions and server-side code
30
+ */
31
+ export class AnalyticsAdminService {
32
+ private analyticsService: AnalyticsService;
33
+ private db: admin.firestore.Firestore;
34
+
35
+ /**
36
+ * Creates a new AnalyticsAdminService instance
37
+ *
38
+ * @param firestore - Admin Firestore instance (optional, defaults to admin.firestore())
39
+ */
40
+ constructor(firestore?: admin.firestore.Firestore) {
41
+ this.db = firestore || admin.firestore();
42
+
43
+ // Create a mock Firebase App for client SDK compatibility
44
+ const mockApp = {
45
+ name: '[DEFAULT]',
46
+ options: {},
47
+ automaticDataCollectionEnabled: false,
48
+ } as any;
49
+
50
+ // Create mock Auth (not used by AnalyticsService)
51
+ const mockAuth = {} as any;
52
+
53
+ // Create AppointmentService adapter that uses admin SDK
54
+ const appointmentService = this.createAppointmentServiceAdapter();
55
+
56
+ // Initialize AnalyticsService with adapted dependencies
57
+ this.analyticsService = new AnalyticsService(
58
+ this.db as any, // Cast admin Firestore to client Firestore type
59
+ mockAuth,
60
+ mockApp,
61
+ appointmentService,
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Creates an adapter for AppointmentService to work with admin SDK
67
+ */
68
+ private createAppointmentServiceAdapter(): any {
69
+ return {
70
+ searchAppointments: async (params: any) => {
71
+ // Build query using admin SDK
72
+ let query: admin.firestore.Query = this.db.collection(APPOINTMENTS_COLLECTION);
73
+
74
+ if (params.clinicBranchId) {
75
+ query = query.where('clinicBranchId', '==', params.clinicBranchId);
76
+ }
77
+ if (params.practitionerId) {
78
+ query = query.where('practitionerId', '==', params.practitionerId);
79
+ }
80
+ if (params.procedureId) {
81
+ query = query.where('procedureId', '==', params.procedureId);
82
+ }
83
+ if (params.patientId) {
84
+ query = query.where('patientId', '==', params.patientId);
85
+ }
86
+ if (params.startDate) {
87
+ const startDate = params.startDate instanceof Date
88
+ ? params.startDate
89
+ : params.startDate.toDate();
90
+ const startTimestamp = admin.firestore.Timestamp.fromDate(startDate);
91
+ query = query.where('appointmentStartTime', '>=', startTimestamp);
92
+ }
93
+ if (params.endDate) {
94
+ const endDate = params.endDate instanceof Date
95
+ ? params.endDate
96
+ : params.endDate.toDate();
97
+ const endTimestamp = admin.firestore.Timestamp.fromDate(endDate);
98
+ query = query.where('appointmentStartTime', '<=', endTimestamp);
99
+ }
100
+
101
+ const snapshot = await query.get();
102
+ const appointments = snapshot.docs.map(doc => ({
103
+ id: doc.id,
104
+ ...doc.data(),
105
+ })) as Appointment[];
106
+
107
+ return {
108
+ appointments,
109
+ total: appointments.length,
110
+ };
111
+ },
112
+ };
113
+ }
114
+
115
+ // Delegate all methods to the underlying AnalyticsService
116
+ // We expose them here so they can be called with admin SDK context
117
+
118
+ async getPractitionerAnalytics(
119
+ practitionerId: string,
120
+ dateRange?: AnalyticsDateRange,
121
+ options?: any,
122
+ ): Promise<PractitionerAnalytics> {
123
+ return this.analyticsService.getPractitionerAnalytics(practitionerId, dateRange, options);
124
+ }
125
+
126
+ async getProcedureAnalytics(
127
+ procedureId?: string,
128
+ dateRange?: AnalyticsDateRange,
129
+ options?: any,
130
+ ): Promise<ProcedureAnalytics | ProcedureAnalytics[]> {
131
+ return this.analyticsService.getProcedureAnalytics(procedureId, dateRange, options);
132
+ }
133
+
134
+ async getTimeEfficiencyMetrics(
135
+ filters?: AnalyticsFilters,
136
+ dateRange?: AnalyticsDateRange,
137
+ options?: any,
138
+ ): Promise<TimeEfficiencyMetrics> {
139
+ return this.analyticsService.getTimeEfficiencyMetrics(filters, dateRange, options);
140
+ }
141
+
142
+ async getTimeEfficiencyMetricsByEntity(
143
+ groupBy: EntityType,
144
+ dateRange?: AnalyticsDateRange,
145
+ filters?: AnalyticsFilters,
146
+ ): Promise<GroupedTimeEfficiencyMetrics[]> {
147
+ return this.analyticsService.getTimeEfficiencyMetricsByEntity(groupBy, dateRange, filters);
148
+ }
149
+
150
+ async getCancellationMetrics(
151
+ groupBy: EntityType,
152
+ dateRange?: AnalyticsDateRange,
153
+ options?: any,
154
+ ): Promise<CancellationMetrics | CancellationMetrics[]> {
155
+ return this.analyticsService.getCancellationMetrics(groupBy, dateRange, options);
156
+ }
157
+
158
+ async getNoShowMetrics(
159
+ groupBy: EntityType,
160
+ dateRange?: AnalyticsDateRange,
161
+ options?: any,
162
+ ): Promise<NoShowMetrics | NoShowMetrics[]> {
163
+ return this.analyticsService.getNoShowMetrics(groupBy, dateRange, options);
164
+ }
165
+
166
+ async getRevenueMetrics(
167
+ filters?: AnalyticsFilters,
168
+ dateRange?: AnalyticsDateRange,
169
+ options?: any,
170
+ ): Promise<RevenueMetrics> {
171
+ return this.analyticsService.getRevenueMetrics(filters, dateRange, options);
172
+ }
173
+
174
+ async getRevenueMetricsByEntity(
175
+ groupBy: EntityType,
176
+ dateRange?: AnalyticsDateRange,
177
+ filters?: AnalyticsFilters,
178
+ ): Promise<GroupedRevenueMetrics[]> {
179
+ return this.analyticsService.getRevenueMetricsByEntity(groupBy, dateRange, filters);
180
+ }
181
+
182
+ async getProductUsageMetrics(
183
+ productId?: string,
184
+ dateRange?: AnalyticsDateRange,
185
+ ): Promise<ProductUsageMetrics | ProductUsageMetrics[]> {
186
+ return this.analyticsService.getProductUsageMetrics(productId, dateRange);
187
+ }
188
+
189
+ async getProductUsageMetricsByEntity(
190
+ groupBy: EntityType,
191
+ dateRange?: AnalyticsDateRange,
192
+ filters?: AnalyticsFilters,
193
+ ): Promise<GroupedProductUsageMetrics[]> {
194
+ return this.analyticsService.getProductUsageMetricsByEntity(groupBy, dateRange, filters);
195
+ }
196
+
197
+ async getPatientAnalytics(
198
+ patientId?: string,
199
+ dateRange?: AnalyticsDateRange,
200
+ ): Promise<PatientAnalytics | PatientAnalytics[]> {
201
+ return this.analyticsService.getPatientAnalytics(patientId, dateRange);
202
+ }
203
+
204
+ async getPatientBehaviorMetricsByEntity(
205
+ groupBy: 'clinic' | 'practitioner' | 'procedure' | 'technology',
206
+ dateRange?: AnalyticsDateRange,
207
+ filters?: AnalyticsFilters,
208
+ ): Promise<GroupedPatientBehaviorMetrics[]> {
209
+ return this.analyticsService.getPatientBehaviorMetricsByEntity(groupBy, dateRange, filters);
210
+ }
211
+
212
+ async getClinicAnalytics(
213
+ clinicBranchId: string,
214
+ dateRange?: AnalyticsDateRange,
215
+ ): Promise<ClinicAnalytics | ClinicAnalytics[]> {
216
+ // Use getDashboardData to get clinic-level analytics
217
+ const dashboard = await this.analyticsService.getDashboardData(
218
+ { clinicBranchId },
219
+ dateRange,
220
+ );
221
+
222
+ // Get clinic info
223
+ const clinicDoc = await this.db.collection('clinics').doc(clinicBranchId).get();
224
+ const clinicData = clinicDoc.data();
225
+ const clinicName = clinicData?.name || 'Unknown';
226
+
227
+ // Convert DashboardAnalytics to ClinicAnalytics format
228
+ return {
229
+ clinicBranchId,
230
+ clinicName,
231
+ totalAppointments: dashboard.overview.totalAppointments,
232
+ completedAppointments: dashboard.overview.completedAppointments,
233
+ canceledAppointments: dashboard.overview.canceledAppointments,
234
+ noShowAppointments: dashboard.overview.noShowAppointments,
235
+ cancellationRate: dashboard.overview.cancellationRate,
236
+ noShowRate: dashboard.overview.noShowRate,
237
+ totalRevenue: dashboard.overview.totalRevenue,
238
+ averageRevenuePerAppointment: dashboard.overview.averageRevenuePerAppointment,
239
+ currency: dashboard.overview.currency,
240
+ practitionerCount: dashboard.overview.uniquePractitioners,
241
+ patientCount: dashboard.overview.uniquePatients,
242
+ procedureCount: dashboard.overview.uniqueProcedures,
243
+ topPractitioners: dashboard.practitionerMetrics.slice(0, 5).map(p => ({
244
+ practitionerId: p.practitionerId,
245
+ practitionerName: p.practitionerName,
246
+ appointmentCount: p.totalAppointments,
247
+ revenue: p.totalRevenue,
248
+ })),
249
+ topProcedures: dashboard.procedureMetrics.slice(0, 5).map(p => ({
250
+ procedureId: p.procedureId,
251
+ procedureName: p.procedureName,
252
+ appointmentCount: p.totalAppointments,
253
+ revenue: p.totalRevenue,
254
+ })),
255
+ };
256
+ }
257
+
258
+ async getDashboardData(
259
+ filters?: AnalyticsFilters,
260
+ dateRange?: AnalyticsDateRange,
261
+ options?: any,
262
+ ): Promise<DashboardAnalytics> {
263
+ return this.analyticsService.getDashboardData(filters, dateRange, options);
264
+ }
265
+
266
+ /**
267
+ * Expose fetchAppointments for direct access if needed
268
+ * This method is used internally by AnalyticsService
269
+ */
270
+ async fetchAppointments(
271
+ filters?: AnalyticsFilters,
272
+ dateRange?: AnalyticsDateRange,
273
+ ): Promise<any[]> {
274
+ // Access the private method via the service
275
+ return (this.analyticsService as any).fetchAppointments(filters, dateRange);
276
+ }
277
+ }
278
+
@@ -0,0 +1,2 @@
1
+ export * from './analytics.admin.service';
2
+
@@ -64,6 +64,7 @@ TimestampUtils.enableServerMode();
64
64
 
65
65
  // Export all services and utilities from the admin sub-modules.
66
66
  export * from './aggregation';
67
+ export * from './analytics';
67
68
  export * from './booking';
68
69
  export * from './calendar';
69
70
  export * from './documentation-templates';
@@ -73,3 +74,8 @@ export * from './mailing';
73
74
  export * from './notifications';
74
75
  export * from './requirements';
75
76
  export * from './users';
77
+
78
+ // Export analytics types for Cloud Functions
79
+ export type * from '../types/analytics';
80
+ export * from '../types/analytics';
81
+ export { CLINICS_COLLECTION } from '../types/clinic';
@@ -1,9 +1,13 @@
1
1
  # Analytics Service Proposal
2
2
 
3
+ > **Note**: This proposal has been implemented. The service is located in `src/services/analytics/` and types in `src/types/analytics/`. See the [README](../../services/analytics/README.md) for usage documentation.
4
+
3
5
  ## Overview
4
6
 
5
7
  This document proposes the design and implementation of an `analytics.service.ts` service for the Clinic Admin app. This service will provide comprehensive financial and analytical intelligence about doctors, procedures, appointments, patients, products, and clinic operations.
6
8
 
9
+ **Status**: ✅ **IMPLEMENTED** - See `src/services/analytics/analytics.service.ts`
10
+
7
11
  ## Data Sources & Connections
8
12
 
9
13
  ### Primary Data Sources
@@ -0,0 +1,199 @@
1
+ # Analytics Service Architecture
2
+
3
+ ## Overview
4
+
5
+ The Analytics Service uses a **hybrid approach** combining **pre-computed analytics** (stored in Firestore) with **on-demand calculation** (fallback). This architecture optimizes for:
6
+
7
+ - **Performance**: Fast reads from pre-computed data
8
+ - **Cost**: Reduced Firestore reads and compute time
9
+ - **Flexibility**: Can still calculate on-demand when needed
10
+
11
+ ## Architecture Components
12
+
13
+ ### 1. Cloud Functions (Pre-computation)
14
+
15
+ **Location**: `Cloud/functions/src/analytics/computeAnalytics.ts`
16
+
17
+ **Schedule**: Runs every 12 hours via Cloud Scheduler
18
+
19
+ **What it does**:
20
+ - Queries all appointments for each clinic
21
+ - Computes analytics for multiple periods (daily, weekly, monthly, yearly, all_time)
22
+ - Stores computed analytics in Firestore subcollections
23
+
24
+ **Storage Structure**:
25
+ ```
26
+ clinics/{clinicBranchId}/
27
+ └── analytics/
28
+ ├── dashboard/
29
+ │ └── {period}/
30
+ │ └── current
31
+ ├── clinic/
32
+ │ └── {period}/
33
+ │ └── current
34
+ ├── practitioners/
35
+ │ └── {period}/
36
+ │ └── {practitionerId}
37
+ ├── procedures/
38
+ │ └── {period}/
39
+ │ └── {procedureId}
40
+ ├── time_efficiency/
41
+ │ └── {period}/
42
+ │ └── current
43
+ ├── revenue/
44
+ │ └── {period}/
45
+ │ └── current
46
+ ├── cancellations/
47
+ │ └── {period}/
48
+ │ └── clinic
49
+ └── no_shows/
50
+ └── {period}/
51
+ └── clinic
52
+ ```
53
+
54
+ ### 2. Analytics Service (Client-side)
55
+
56
+ **Location**: `Api/src/services/analytics/analytics.service.ts`
57
+
58
+ **Behavior**:
59
+ 1. **First**: Tries to read from stored analytics
60
+ 2. **Checks**: If data is fresh (within maxCacheAgeHours, default 12 hours)
61
+ 3. **Falls back**: Calculates on-demand if:
62
+ - No stored data exists
63
+ - Data is stale (older than maxCacheAgeHours)
64
+ - `useCache: false` is specified
65
+
66
+ ### 3. Storage Types
67
+
68
+ **Location**: `Api/src/types/analytics/stored-analytics.types.ts`
69
+
70
+ Defines types for stored analytics documents, including metadata about when and how they were computed.
71
+
72
+ ## Data Flow
73
+
74
+ ### Pre-computation Flow (Cloud Function)
75
+
76
+ ```
77
+ Cloud Scheduler (every 12 hours)
78
+
79
+ computeAnalyticsForAllClinics()
80
+
81
+ For each clinic:
82
+ ├── Compute dashboard analytics
83
+ ├── Compute clinic analytics
84
+ ├── Compute practitioner analytics (for each practitioner)
85
+ ├── Compute procedure analytics (for each procedure)
86
+ ├── Compute time efficiency metrics
87
+ ├── Compute revenue metrics
88
+ ├── Compute cancellation metrics
89
+ └── Compute no-show metrics
90
+
91
+ Store in Firestore subcollections
92
+ ```
93
+
94
+ ### Client Read Flow
95
+
96
+ ```
97
+ Client requests analytics
98
+
99
+ AnalyticsService.getDashboardData()
100
+
101
+ Check stored analytics?
102
+ ├── Yes → Read from Firestore
103
+ │ ├── Fresh? → Return cached data ✅
104
+ │ └── Stale? → Calculate on-demand
105
+ └── No → Calculate on-demand
106
+ ```
107
+
108
+ ## Benefits
109
+
110
+ ### Performance
111
+ - **Fast reads**: Single document read vs. querying hundreds/thousands of appointments
112
+ - **Reduced latency**: Pre-computed data returns instantly
113
+ - **Scalable**: Works efficiently even with large datasets
114
+
115
+ ### Cost Optimization
116
+ - **Fewer reads**: 1 read vs. potentially hundreds/thousands
117
+ - **Reduced compute**: Calculations run server-side, not on every client request
118
+ - **Predictable costs**: Fixed cost per clinic per period
119
+
120
+ ### Flexibility
121
+ - **On-demand fallback**: Can still calculate when needed
122
+ - **Custom date ranges**: Can override cached data for specific queries
123
+ - **Real-time option**: Can disable caching for live data
124
+
125
+ ## Usage Examples
126
+
127
+ ### Using Pre-computed Analytics (Default)
128
+
129
+ ```typescript
130
+ // Automatically uses cached data if available and fresh
131
+ const dashboard = await analyticsService.getDashboardData(
132
+ { clinicBranchId: 'clinic-123' },
133
+ { start: new Date('2024-01-01'), end: new Date('2024-12-31') }
134
+ );
135
+ ```
136
+
137
+ ### Forcing On-demand Calculation
138
+
139
+ ```typescript
140
+ // Bypass cache and calculate on-demand
141
+ const dashboard = await analyticsService.getDashboardData(
142
+ { clinicBranchId: 'clinic-123' },
143
+ { start: new Date('2024-01-01'), end: new Date('2024-12-31') },
144
+ { useCache: false }
145
+ );
146
+ ```
147
+
148
+ ### Custom Cache Age
149
+
150
+ ```typescript
151
+ // Use cache if data is less than 6 hours old (instead of default 12)
152
+ const dashboard = await analyticsService.getDashboardData(
153
+ { clinicBranchId: 'clinic-123' },
154
+ { start: new Date('2024-01-01'), end: new Date('2024-12-31') },
155
+ { maxCacheAgeHours: 6 }
156
+ );
157
+ ```
158
+
159
+ ## Configuration
160
+
161
+ ### Cloud Function Schedule
162
+
163
+ Edit `Cloud/functions/src/analytics/computeAnalytics.ts`:
164
+
165
+ ```typescript
166
+ schedule: "every 12 hours" // Change to "every 6 hours", "every 24 hours", etc.
167
+ ```
168
+
169
+ ### Default Cache Age
170
+
171
+ Edit `Api/src/services/analytics/utils/stored-analytics.utils.ts`:
172
+
173
+ ```typescript
174
+ maxCacheAgeHours = 12 // Change default cache age
175
+ ```
176
+
177
+ ## Monitoring
178
+
179
+ ### Cloud Function Logs
180
+
181
+ Check Cloud Function logs for:
182
+ - Computation success/failure
183
+ - Processing time per clinic
184
+ - Errors during computation
185
+
186
+ ### Firestore Usage
187
+
188
+ Monitor Firestore reads:
189
+ - Pre-computed reads: 1 per analytics request
190
+ - On-demand reads: Variable (depends on appointment count)
191
+
192
+ ## Future Enhancements
193
+
194
+ 1. **Incremental Updates**: Only recompute changed periods
195
+ 2. **Real-time Triggers**: Update analytics when appointments change
196
+ 3. **Aggregated Views**: Pre-compute common dashboard views
197
+ 4. **Historical Snapshots**: Keep historical analytics for trend analysis
198
+ 5. **Multi-clinic Aggregation**: Aggregate analytics across clinic groups
199
+