@blackcode_sa/metaestetics-api 1.6.12 → 1.6.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/index.d.mts CHANGED
@@ -1905,7 +1905,7 @@ interface CreatePatientProfileData {
1905
1905
  /**
1906
1906
  * Tip za ažuriranje Patient profila
1907
1907
  */
1908
- interface UpdatePatientProfileData extends Partial<Omit<PatientProfile, "id" | "createdAt" | "updatedAt">> {
1908
+ interface UpdatePatientProfileData extends Partial<Omit<PatientProfile, 'id' | 'createdAt' | 'updatedAt'>> {
1909
1909
  updatedAt?: FieldValue;
1910
1910
  }
1911
1911
  /**
@@ -1924,7 +1924,7 @@ interface RequesterInfo {
1924
1924
  /** ID of the clinic admin user or practitioner user making the request. */
1925
1925
  id: string;
1926
1926
  /** Role of the requester, determining the search context. */
1927
- role: "clinic_admin" | "practitioner";
1927
+ role: 'clinic_admin' | 'practitioner';
1928
1928
  /** If role is 'clinic_admin', this is the associated clinic ID. */
1929
1929
  associatedClinicId?: string;
1930
1930
  /** If role is 'practitioner', this is the associated practitioner profile ID. */
package/dist/index.d.ts CHANGED
@@ -1905,7 +1905,7 @@ interface CreatePatientProfileData {
1905
1905
  /**
1906
1906
  * Tip za ažuriranje Patient profila
1907
1907
  */
1908
- interface UpdatePatientProfileData extends Partial<Omit<PatientProfile, "id" | "createdAt" | "updatedAt">> {
1908
+ interface UpdatePatientProfileData extends Partial<Omit<PatientProfile, 'id' | 'createdAt' | 'updatedAt'>> {
1909
1909
  updatedAt?: FieldValue;
1910
1910
  }
1911
1911
  /**
@@ -1924,7 +1924,7 @@ interface RequesterInfo {
1924
1924
  /** ID of the clinic admin user or practitioner user making the request. */
1925
1925
  id: string;
1926
1926
  /** Role of the requester, determining the search context. */
1927
- role: "clinic_admin" | "practitioner";
1927
+ role: 'clinic_admin' | 'practitioner';
1928
1928
  /** If role is 'clinic_admin', this is the associated clinic ID. */
1929
1929
  associatedClinicId?: string;
1930
1930
  /** If role is 'practitioner', this is the associated practitioner profile ID. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.6.12",
4
+ "version": "1.6.14",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.mjs",
@@ -62,6 +62,7 @@ import {
62
62
  } from "../../types/calendar";
63
63
  import { DocumentManagerAdminService } from "../documentation-templates/document-manager.admin";
64
64
  import { LinkedFormInfo } from "../../types/appointment";
65
+ import { TimestampUtils } from "../../utils/TimestampUtils";
65
66
 
66
67
  /**
67
68
  * Interface for the data required by orchestrateAppointmentCreation
@@ -205,15 +206,8 @@ export class BookingAdmin {
205
206
  private adminTimestampToClientTimestamp(
206
207
  timestamp: admin.firestore.Timestamp
207
208
  ): any {
208
- // Create a client Timestamp with the same seconds and nanoseconds
209
- return {
210
- seconds: timestamp.seconds,
211
- nanoseconds: timestamp.nanoseconds,
212
- toDate: () => timestamp.toDate(),
213
- toMillis: () => timestamp.toMillis(),
214
- valueOf: () => timestamp.valueOf(),
215
- // Add any other required methods/properties
216
- };
209
+ // Use TimestampUtils instead of custom implementation
210
+ return TimestampUtils.adminToClient(timestamp);
217
211
  }
218
212
 
219
213
  /**
@@ -223,8 +217,8 @@ export class BookingAdmin {
223
217
  return events.map((event) => ({
224
218
  ...event,
225
219
  eventTime: {
226
- start: this.adminTimestampToClientTimestamp(event.eventTime.start),
227
- end: this.adminTimestampToClientTimestamp(event.eventTime.end),
220
+ start: TimestampUtils.adminToClient(event.eventTime.start),
221
+ end: TimestampUtils.adminToClient(event.eventTime.end),
228
222
  },
229
223
  // Convert any other timestamps in the event if needed
230
224
  }));
@@ -12,6 +12,7 @@ import {
12
12
  import { DoctorInfo } from "../types/clinic";
13
13
  import { Procedure, ProcedureSummaryInfo } from "../types/procedure";
14
14
  import { PatientProfile } from "../types/patient";
15
+ import { Appointment, AppointmentStatus } from "../types/appointment";
15
16
 
16
17
  // Explicitly import the services to re-export them by name
17
18
  import { ClinicAggregationService } from "./aggregation/clinic/clinic.aggregation.service";
@@ -46,6 +47,7 @@ export type { Practitioner, PractitionerToken } from "../types/practitioner";
46
47
  export type { DoctorInfo } from "../types/clinic";
47
48
  export type { Procedure, ProcedureSummaryInfo } from "../types/procedure";
48
49
  export type { PatientProfile as Patient } from "../types/patient";
50
+ export type { Appointment } from "../types/appointment";
49
51
 
50
52
  // Re-export enums/consts
51
53
  export {
@@ -55,6 +57,7 @@ export {
55
57
  } from "../types/notifications";
56
58
  export { UserRole } from "../types";
57
59
  export { PractitionerTokenStatus } from "../types/practitioner";
60
+ export { AppointmentStatus } from "../types/appointment";
58
61
 
59
62
  // Export admin classes/services explicitly by name
60
63
  export {
@@ -8,6 +8,7 @@ import { Expo, ExpoPushMessage, ExpoPushTicket } from "expo-server-sdk";
8
8
  import { Appointment, PaymentStatus } from "../../types/appointment";
9
9
  import { UserRole } from "../../types";
10
10
  import { Timestamp as FirebaseClientTimestamp } from "@firebase/firestore";
11
+ import { TimestampUtils } from "../../utils/TimestampUtils";
11
12
 
12
13
  export class NotificationsAdmin {
13
14
  private expo: Expo;
@@ -277,10 +278,9 @@ export class NotificationsAdmin {
277
278
  }
278
279
 
279
280
  const adminTsNow = admin.firestore.Timestamp.now();
280
- const clientCompatibleNotificationTime = new FirebaseClientTimestamp(
281
- adminTsNow.seconds,
282
- adminTsNow.nanoseconds
283
- );
281
+ const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
282
+ adminTsNow
283
+ ) as FirebaseClientTimestamp;
284
284
 
285
285
  const notificationData: Omit<
286
286
  Notification,
@@ -354,10 +354,9 @@ export class NotificationsAdmin {
354
354
  }
355
355
 
356
356
  const adminTsNow = admin.firestore.Timestamp.now();
357
- const clientCompatibleNotificationTime = new FirebaseClientTimestamp(
358
- adminTsNow.seconds,
359
- adminTsNow.nanoseconds
360
- );
357
+ const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
358
+ adminTsNow
359
+ ) as FirebaseClientTimestamp;
361
360
 
362
361
  const notificationData: Omit<
363
362
  Notification,
@@ -406,10 +405,9 @@ export class NotificationsAdmin {
406
405
  const body = `Action Required: A new time has been proposed for your appointment for ${appointment.procedureInfo.name}. Please review in the app.`;
407
406
 
408
407
  const adminTsNow = admin.firestore.Timestamp.now();
409
- const clientCompatibleNotificationTime = new FirebaseClientTimestamp(
410
- adminTsNow.seconds,
411
- adminTsNow.nanoseconds
412
- );
408
+ const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
409
+ adminTsNow
410
+ ) as FirebaseClientTimestamp;
413
411
 
414
412
  const notificationData: Omit<
415
413
  Notification,
@@ -461,10 +459,9 @@ export class NotificationsAdmin {
461
459
  .toLocaleDateString()} is now ${appointment.paymentStatus}.`;
462
460
 
463
461
  const adminTsNow = admin.firestore.Timestamp.now();
464
- const clientCompatibleNotificationTime = new FirebaseClientTimestamp(
465
- adminTsNow.seconds,
466
- adminTsNow.nanoseconds
467
- );
462
+ const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
463
+ adminTsNow
464
+ ) as FirebaseClientTimestamp;
468
465
 
469
466
  const notificationType =
470
467
  appointment.paymentStatus === PaymentStatus.PAID
@@ -518,10 +515,9 @@ export class NotificationsAdmin {
518
515
  const body = `How was your recent appointment for ${appointment.procedureInfo.name}? We'd love to hear your feedback!`;
519
516
 
520
517
  const adminTsNow = admin.firestore.Timestamp.now();
521
- const clientCompatibleNotificationTime = new FirebaseClientTimestamp(
522
- adminTsNow.seconds,
523
- adminTsNow.nanoseconds
524
- );
518
+ const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
519
+ adminTsNow
520
+ ) as FirebaseClientTimestamp;
525
521
 
526
522
  const notificationData: Omit<
527
523
  Notification,
@@ -581,10 +577,9 @@ export class NotificationsAdmin {
581
577
  .toLocaleDateString()}.`;
582
578
 
583
579
  const adminTsNow = admin.firestore.Timestamp.now();
584
- const clientCompatibleNotificationTime = new FirebaseClientTimestamp(
585
- adminTsNow.seconds,
586
- adminTsNow.nanoseconds
587
- );
580
+ const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
581
+ adminTsNow
582
+ ) as FirebaseClientTimestamp;
588
583
 
589
584
  const tempNotificationType = NotificationType.GENERAL_MESSAGE;
590
585
 
@@ -17,6 +17,7 @@ import {
17
17
  import { PatientProfile, PATIENTS_COLLECTION } from "../../types/patient";
18
18
  import { UserRole } from "../../types"; // Assuming UserRole is in the main types index
19
19
  import { NotificationsAdmin } from "../notifications/notifications.admin";
20
+ import { TimestampUtils } from "../../utils/TimestampUtils";
20
21
 
21
22
  /**
22
23
  * @class PatientRequirementsAdminService
@@ -106,10 +107,9 @@ export class PatientRequirementsAdminService {
106
107
  ...currentInstruction,
107
108
  notificationId: undefined,
108
109
  status: PatientInstructionStatus.CANCELLED,
109
- updatedAt: new FirebaseClientTimestamp(
110
- adminTsNow.seconds,
111
- adminTsNow.nanoseconds
112
- ),
110
+ updatedAt: TimestampUtils.adminToClient(
111
+ adminTsNow
112
+ ) as FirebaseClientTimestamp,
113
113
  };
114
114
  updatedInstructions[i] = currentInstruction;
115
115
  instructionUpdatesMade = true;
@@ -144,10 +144,9 @@ export class PatientRequirementsAdminService {
144
144
  currentInstruction = {
145
145
  ...currentInstruction,
146
146
  notificationId: undefined,
147
- updatedAt: new FirebaseClientTimestamp(
148
- adminTsNow.seconds,
149
- adminTsNow.nanoseconds
150
- ),
147
+ updatedAt: TimestampUtils.adminToClient(
148
+ adminTsNow
149
+ ) as FirebaseClientTimestamp,
151
150
  };
152
151
  updatedInstructions[i] = currentInstruction;
153
152
  instructionUpdatesMade = true;
@@ -196,10 +195,9 @@ export class PatientRequirementsAdminService {
196
195
  ...currentInstruction,
197
196
  notificationId: createdNotificationId,
198
197
  status: PatientInstructionStatus.PENDING_NOTIFICATION,
199
- updatedAt: new FirebaseClientTimestamp(
200
- adminTsNow.seconds,
201
- adminTsNow.nanoseconds
202
- ),
198
+ updatedAt: TimestampUtils.adminToClient(
199
+ adminTsNow
200
+ ) as FirebaseClientTimestamp,
203
201
  };
204
202
  updatedInstructions[i] = currentInstruction;
205
203
  instructionUpdatesMade = true;
@@ -225,10 +223,9 @@ export class PatientRequirementsAdminService {
225
223
  const finalAdminTsNow = admin.firestore.Timestamp.now();
226
224
  await instanceDocRef.update({
227
225
  instructions: updatedInstructions, // Array of instructions with actual Timestamps
228
- updatedAt: new FirebaseClientTimestamp(
229
- finalAdminTsNow.seconds,
230
- finalAdminTsNow.nanoseconds
231
- ),
226
+ updatedAt: TimestampUtils.adminToClient(
227
+ finalAdminTsNow
228
+ ) as FirebaseClientTimestamp,
232
229
  });
233
230
  }
234
231
  }
@@ -332,10 +329,9 @@ export class PatientRequirementsAdminService {
332
329
  currentInstruction = {
333
330
  ...currentInstruction,
334
331
  status: PatientInstructionStatus.MISSED,
335
- updatedAt: new FirebaseClientTimestamp(
336
- adminNowForMissed.seconds,
337
- adminNowForMissed.nanoseconds
338
- ),
332
+ updatedAt: TimestampUtils.adminToClient(
333
+ adminNowForMissed
334
+ ) as FirebaseClientTimestamp,
339
335
  };
340
336
  updatedInstructions[i] = currentInstruction;
341
337
  changesMade = true;
@@ -349,10 +345,9 @@ export class PatientRequirementsAdminService {
349
345
  const finalAdminNowForMissedUpdate = admin.firestore.Timestamp.now();
350
346
  await instanceRef.update({
351
347
  instructions: updatedInstructions, // Array of instructions with actual Timestamps
352
- updatedAt: new FirebaseClientTimestamp(
353
- finalAdminNowForMissedUpdate.seconds,
354
- finalAdminNowForMissedUpdate.nanoseconds
355
- ),
348
+ updatedAt: TimestampUtils.adminToClient(
349
+ finalAdminNowForMissedUpdate
350
+ ) as FirebaseClientTimestamp,
356
351
  });
357
352
  console.log(
358
353
  `[PRA_Service] Updated missed instructions for instance ${instanceId}.`
@@ -416,10 +411,9 @@ export class PatientRequirementsAdminService {
416
411
  );
417
412
  await instanceRef.update({
418
413
  overallStatus: PatientRequirementOverallStatus.ACTIVE,
419
- updatedAt: new FirebaseClientTimestamp(
420
- admin.firestore.Timestamp.now().seconds,
421
- admin.firestore.Timestamp.now().nanoseconds
422
- ),
414
+ updatedAt: TimestampUtils.adminToClient(
415
+ admin.firestore.Timestamp.now()
416
+ ) as FirebaseClientTimestamp,
423
417
  });
424
418
  }
425
419
  return;
@@ -468,10 +462,9 @@ export class PatientRequirementsAdminService {
468
462
  const adminTsNow = admin.firestore.Timestamp.now();
469
463
  await instanceRef.update({
470
464
  overallStatus: newOverallStatus,
471
- updatedAt: new FirebaseClientTimestamp(
472
- adminTsNow.seconds,
473
- adminTsNow.nanoseconds
474
- ),
465
+ updatedAt: TimestampUtils.adminToClient(
466
+ adminTsNow
467
+ ) as FirebaseClientTimestamp,
475
468
  });
476
469
  } else {
477
470
  console.log(
@@ -1,26 +1,18 @@
1
- import { Timestamp, FieldValue } from "firebase/firestore";
2
- import type { ProcedureFamily } from "../../backoffice/types/static/procedure-family.types";
3
- import type { TreatmentBenefit } from "../../backoffice/types/static/treatment-benefit.types";
4
- import type {
5
- Currency,
6
- PricingMeasure,
7
- } from "../../backoffice/types/static/pricing.types";
8
- import type { ClinicInfo } from "../profile";
9
- import { ClinicReviewInfo } from "../reviews";
10
- import { ProcedureSummaryInfo } from "../procedure";
1
+ import { Timestamp, FieldValue } from 'firebase/firestore';
2
+ import type { ProcedureFamily } from '../../backoffice/types/static/procedure-family.types';
3
+ import type { TreatmentBenefit } from '../../backoffice/types/static/treatment-benefit.types';
4
+ import type { Currency, PricingMeasure } from '../../backoffice/types/static/pricing.types';
5
+ import type { ClinicInfo } from '../profile';
6
+ import { ClinicReviewInfo } from '../reviews';
7
+ import { ProcedureSummaryInfo } from '../procedure';
11
8
 
12
- export const CLINIC_GROUPS_COLLECTION = "clinic_groups";
13
- export const CLINIC_ADMINS_COLLECTION = "clinic_admins";
14
- export const CLINICS_COLLECTION = "clinics";
9
+ export const CLINIC_GROUPS_COLLECTION = 'clinic_groups';
10
+ export const CLINIC_ADMINS_COLLECTION = 'clinic_admins';
11
+ export const CLINICS_COLLECTION = 'clinics';
15
12
 
16
- import {
17
- PracticeType,
18
- Language,
19
- ClinicTag,
20
- ClinicPhotoTag,
21
- } from "./preferences.types";
13
+ import { PracticeType, Language, ClinicTag, ClinicPhotoTag } from './preferences.types';
22
14
 
23
- export * from "./preferences.types";
15
+ export * from './preferences.types';
24
16
 
25
17
  /**
26
18
  * Interface for clinic contact information
@@ -140,9 +132,9 @@ export interface UpdateClinicAdminData extends Partial<CreateClinicAdminData> {
140
132
  * Enum for admin token status
141
133
  */
142
134
  export enum AdminTokenStatus {
143
- ACTIVE = "active",
144
- USED = "used",
145
- EXPIRED = "expired",
135
+ ACTIVE = 'active',
136
+ USED = 'used',
137
+ EXPIRED = 'expired',
146
138
  }
147
139
 
148
140
  /**
@@ -171,10 +163,10 @@ export interface AdminInfo {
171
163
  * Enum for subscription models
172
164
  */
173
165
  export enum SubscriptionModel {
174
- NO_SUBSCRIPTION = "no_subscription",
175
- BASIC = "basic",
176
- PREMIUM = "premium",
177
- ENTERPRISE = "enterprise",
166
+ NO_SUBSCRIPTION = 'no_subscription',
167
+ BASIC = 'basic',
168
+ PREMIUM = 'premium',
169
+ ENTERPRISE = 'enterprise',
178
170
  }
179
171
 
180
172
  /**
@@ -1,24 +1,24 @@
1
- import { Timestamp, FieldValue } from "firebase/firestore";
2
- import { User } from "..";
3
- import type { PatientMedicalInfo } from "./medical-info.types";
4
- import { PATIENT_MEDICAL_INFO_COLLECTION } from "./medical-info.types";
1
+ import { Timestamp, FieldValue } from 'firebase/firestore';
2
+ import { User } from '..';
3
+ import type { PatientMedicalInfo } from './medical-info.types';
4
+ import { PATIENT_MEDICAL_INFO_COLLECTION } from './medical-info.types';
5
5
 
6
- export const PATIENTS_COLLECTION = "patients";
7
- export const PATIENT_SENSITIVE_INFO_COLLECTION = "sensitive-info";
8
- export const PATIENT_MEDICAL_HISTORY_COLLECTION = "medical-history";
9
- export const PATIENT_APPOINTMENTS_COLLECTION = "appointments";
10
- export const PATIENT_LOCATION_INFO_COLLECTION = "location-info";
6
+ export const PATIENTS_COLLECTION = 'patients';
7
+ export const PATIENT_SENSITIVE_INFO_COLLECTION = 'sensitive-info';
8
+ export const PATIENT_MEDICAL_HISTORY_COLLECTION = 'medical-history';
9
+ export const PATIENT_APPOINTMENTS_COLLECTION = 'appointments';
10
+ export const PATIENT_LOCATION_INFO_COLLECTION = 'location-info';
11
11
 
12
12
  /**
13
13
  * Enumeracija za pol pacijenta
14
14
  */
15
15
  export enum Gender {
16
- MALE = "male",
17
- FEMALE = "female",
18
- TRANSGENDER_MALE = "transgender_male",
19
- TRANSGENDER_FEMALE = "transgender_female",
20
- PREFER_NOT_TO_SAY = "prefer_not_to_say",
21
- OTHER = "other",
16
+ MALE = 'male',
17
+ FEMALE = 'female',
18
+ TRANSGENDER_MALE = 'transgender_male',
19
+ TRANSGENDER_FEMALE = 'transgender_female',
20
+ PREFER_NOT_TO_SAY = 'prefer_not_to_say',
21
+ OTHER = 'other',
22
22
  }
23
23
 
24
24
  /**
@@ -83,8 +83,7 @@ export interface CreatePatientLocationInfoData {
83
83
  /**
84
84
  * Tip za ažuriranje lokacijskih informacija
85
85
  */
86
- export interface UpdatePatientLocationInfoData
87
- extends Partial<CreatePatientLocationInfoData> {
86
+ export interface UpdatePatientLocationInfoData extends Partial<CreatePatientLocationInfoData> {
88
87
  updatedAt?: FieldValue;
89
88
  }
90
89
 
@@ -129,8 +128,7 @@ export interface CreatePatientSensitiveInfoData {
129
128
  /**
130
129
  * Tip za ažuriranje osetljivih informacija
131
130
  */
132
- export interface UpdatePatientSensitiveInfoData
133
- extends Partial<CreatePatientSensitiveInfoData> {
131
+ export interface UpdatePatientSensitiveInfoData extends Partial<CreatePatientSensitiveInfoData> {
134
132
  updatedAt?: FieldValue;
135
133
  }
136
134
 
@@ -198,7 +196,7 @@ export interface CreatePatientProfileData {
198
196
  * Tip za ažuriranje Patient profila
199
197
  */
200
198
  export interface UpdatePatientProfileData
201
- extends Partial<Omit<PatientProfile, "id" | "createdAt" | "updatedAt">> {
199
+ extends Partial<Omit<PatientProfile, 'id' | 'createdAt' | 'updatedAt'>> {
202
200
  // Use Omit to exclude base fields
203
201
  updatedAt?: FieldValue;
204
202
  // Note: doctors, clinics, doctorIds, clinicIds should ideally be updated via specific methods (add/removeDoctor/Clinic)
@@ -221,14 +219,14 @@ export interface RequesterInfo {
221
219
  /** ID of the clinic admin user or practitioner user making the request. */
222
220
  id: string;
223
221
  /** Role of the requester, determining the search context. */
224
- role: "clinic_admin" | "practitioner";
222
+ role: 'clinic_admin' | 'practitioner';
225
223
  /** If role is 'clinic_admin', this is the associated clinic ID. */
226
224
  associatedClinicId?: string;
227
225
  /** If role is 'practitioner', this is the associated practitioner profile ID. */
228
226
  associatedPractitionerId?: string;
229
227
  }
230
228
 
231
- export * from "./medical-info.types";
229
+ export * from './medical-info.types';
232
230
 
233
231
  // This is a type that combines all the patient data - used only in UI Frontend App
234
232
  export interface PatientProfileComplete {
@@ -0,0 +1,176 @@
1
+ # Timestamp Management Strategy
2
+
3
+ ## Problem Statement
4
+
5
+ Firebase provides two different Timestamp implementations across its SDKs:
6
+
7
+ 1. **Client-side Timestamp**: `import { Timestamp } from 'firebase/firestore'`
8
+
9
+ - Used in client applications (web, mobile)
10
+ - Lives in the `firebase/firestore` package
11
+
12
+ 2. **Admin-side Timestamp**: `import * as admin from 'firebase-admin'; admin.firestore.Timestamp`
13
+ - Used in server-side code (Cloud Functions)
14
+ - Lives in the `firebase-admin` package
15
+
16
+ These types are structurally similar but are different JavaScript classes, causing type conflicts when:
17
+
18
+ - Data with client Timestamps is processed in admin code
19
+ - Data with admin Timestamps is sent to client code
20
+ - Shared type definitions across client and admin code
21
+
22
+ ## Solution: TimestampUtils
23
+
24
+ We've created a central `TimestampUtils` class providing:
25
+
26
+ 1. Conversion utilities between admin and client Timestamps
27
+ 2. Best practices for timestamp handling
28
+ 3. Consistent patterns for timestamp operations
29
+
30
+ ## Usage Guidelines
31
+
32
+ ### 1. Type Definitions
33
+
34
+ All shared type definitions (in `src/types/*`) should:
35
+
36
+ - Import Timestamp from client SDK: `import { Timestamp } from 'firebase/firestore'`
37
+ - Define fields using this client Timestamp: `timestamp: Timestamp`
38
+
39
+ This ensures types are consumable by both client and admin code.
40
+
41
+ ### 2. Admin Code (Cloud Functions)
42
+
43
+ When writing admin code that:
44
+
45
+ - **Reads data from Firestore**: No conversion needed, as Firestore returns admin Timestamps
46
+ - **Processes timestamps**: Use standard admin timestamp methods
47
+ - **Writes data to shared types**: Convert admin → client using `TimestampUtils.adminToClient()`
48
+ - **Creates server timestamps**: For fields created with `serverTimestamp()`, use Firebase Admin's version: `admin.firestore.FieldValue.serverTimestamp()`
49
+
50
+ Example:
51
+
52
+ ```typescript
53
+ import * as admin from "firebase-admin";
54
+ import { TimestampUtils } from "../../utils/TimestampUtils";
55
+ import { YourSharedType } from "../../types/shared";
56
+
57
+ // In your admin service
58
+ async function processAndUpdateData() {
59
+ const adminTsNow = admin.firestore.Timestamp.now();
60
+
61
+ // When populating a shared type that clients will use
62
+ const data: YourSharedType = {
63
+ // Convert admin timestamp to client timestamp
64
+ createdAt: TimestampUtils.adminToClient(adminTsNow),
65
+
66
+ // For server timestamps, use admin version
67
+ updatedAt: admin.firestore.FieldValue.serverTimestamp(),
68
+
69
+ // Other fields...
70
+ };
71
+
72
+ // When you need to process objects with nested timestamps
73
+ const objectWithNestedTimestamps = {
74
+ // Complex data from another source
75
+ };
76
+
77
+ // Convert all timestamps in the object from admin to client
78
+ const clientCompatible = TimestampUtils.convertObjectTimestampsAdminToClient(
79
+ objectWithNestedTimestamps
80
+ );
81
+ }
82
+ ```
83
+
84
+ ### 3. Client Code (Web/Mobile Apps)
85
+
86
+ Client code should:
87
+
88
+ - Use `firebase/firestore` Timestamp when needed
89
+ - No conversion is necessary for data returned by `getDocs()` or similar client SDK methods
90
+ - For new timestamps, use `Timestamp.now()` or `Timestamp.fromDate()`
91
+
92
+ ### 4. Utility Functions
93
+
94
+ The `TimestampUtils` class provides these key functions:
95
+
96
+ - `adminToClient(adminTimestamp)`: Converts admin → client Timestamp
97
+ - `clientToAdmin(clientTimestamp)`: Converts client → admin Timestamp
98
+ - `nowAsClient()`: Creates current timestamp as client Timestamp
99
+ - `dateToClientTimestamp(date)`: Converts Date → client Timestamp
100
+ - `dateToAdminTimestamp(date)`: Converts Date → admin Timestamp
101
+ - `convertObjectTimestampsAdminToClient(obj)`: Deep conversion of all timestamps in an object from admin → client
102
+ - `convertObjectTimestampsClientToAdmin(obj)`: Deep conversion of all timestamps in an object from client → admin
103
+
104
+ ## Common Patterns
105
+
106
+ ### 1. Creating New Documents
107
+
108
+ ```typescript
109
+ // In admin code
110
+ const adminTimestamp = admin.firestore.Timestamp.now();
111
+ const clientCompatibleTimestamp = TimestampUtils.adminToClient(adminTimestamp);
112
+
113
+ const newDocument = {
114
+ id: "doc123",
115
+ createdAt: clientCompatibleTimestamp,
116
+ // For server-managed timestamps, use serverTimestamp()
117
+ updatedAt: admin.firestore.FieldValue.serverTimestamp(),
118
+ };
119
+
120
+ await docRef.set(newDocument);
121
+ ```
122
+
123
+ ### 2. Updating Fields in a Document
124
+
125
+ ```typescript
126
+ // In admin code
127
+ await docRef.update({
128
+ someField: "new value",
129
+ // For immediate timestamp, convert admin to client
130
+ processedAt: TimestampUtils.adminToClient(admin.firestore.Timestamp.now()),
131
+ // For server-managed timestamps, use serverTimestamp()
132
+ updatedAt: admin.firestore.FieldValue.serverTimestamp(),
133
+ });
134
+ ```
135
+
136
+ ### 3. Processing Calendar Events
137
+
138
+ Calendar events and date ranges should be consistently handled:
139
+
140
+ ```typescript
141
+ // Calendar event time conversion
142
+ const firestoreCompatibleEventTime = {
143
+ start: {
144
+ seconds: newEventTime.start.seconds,
145
+ nanoseconds: newEventTime.start.nanoseconds,
146
+ },
147
+ end: {
148
+ seconds: newEventTime.end.seconds,
149
+ nanoseconds: newEventTime.end.nanoseconds,
150
+ },
151
+ };
152
+ ```
153
+
154
+ ## Edge Cases and Limitations
155
+
156
+ 1. **null/undefined Handling**: All utility methods handle null safely
157
+ 2. **Serialization**: If serializing for JSON, convert to ISO strings first
158
+ 3. **Performance**: The conversion is lightweight, but for large batches, process efficiently
159
+
160
+ ## Migration Strategy
161
+
162
+ 1. Identify timestamp usage in your code using static analysis or grep
163
+ 2. Replace direct timestamp conversions with TimestampUtils methods
164
+ 3. Update methods like:
165
+ - `adminTimestampToClientTimestamp` → `TimestampUtils.adminToClient`
166
+ - Direct creation of client timestamps → `TimestampUtils.nowAsClient`
167
+ 4. Test thoroughly after each change
168
+
169
+ ## Conclusion
170
+
171
+ Following this strategy will:
172
+
173
+ - Eliminate type errors between admin and client code
174
+ - Provide consistent timestamp handling throughout the codebase
175
+ - Minimize conversion bugs by centralizing conversion logic
176
+ - Make it easier to onboard new developers to the codebase