@blackcode_sa/metaestetics-api 1.10.0 → 1.11.0

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.
@@ -16,8 +16,9 @@ import {
16
16
  writeBatch,
17
17
  arrayUnion,
18
18
  arrayRemove,
19
- } from 'firebase/firestore';
20
- import { BaseService } from '../base.service';
19
+ } from "firebase/firestore";
20
+ import { getFunctions, httpsCallable } from "firebase/functions";
21
+ import { BaseService } from "../base.service";
21
22
  import {
22
23
  Clinic,
23
24
  CreateClinicData,
@@ -29,36 +30,41 @@ import {
29
30
  ClinicBranchSetupData,
30
31
  CLINIC_ADMINS_COLLECTION,
31
32
  DoctorInfo,
32
- } from '../../types/clinic';
33
+ } from "../../types/clinic";
33
34
  // Correct imports
34
- import { ProcedureSummaryInfo } from '../../types/procedure';
35
- import { ClinicInfo } from '../../types/profile';
36
- import { ClinicGroupService } from './clinic-group.service';
37
- import { ClinicAdminService } from './clinic-admin.service';
38
- import { geohashForLocation, geohashQueryBounds, distanceBetween } from 'geofire-common';
35
+ import { ProcedureSummaryInfo } from "../../types/procedure";
36
+ import { ClinicInfo } from "../../types/profile";
37
+ import { ClinicGroupService } from "./clinic-group.service";
38
+ import { ClinicAdminService } from "./clinic-admin.service";
39
+ import {
40
+ geohashForLocation,
41
+ geohashQueryBounds,
42
+ distanceBetween,
43
+ } from "geofire-common";
39
44
  import {
40
45
  clinicSchema,
41
46
  createClinicSchema,
42
47
  updateClinicSchema,
43
48
  clinicBranchSetupSchema,
44
- } from '../../validations/clinic.schema';
45
- import { z } from 'zod';
46
- import { Auth } from 'firebase/auth';
47
- import { Firestore } from 'firebase/firestore';
48
- import { FirebaseApp } from 'firebase/app';
49
- import * as ClinicUtils from './utils/clinic.utils';
50
- import * as TagUtils from './utils/tag.utils';
51
- import * as SearchUtils from './utils/search.utils';
52
- import * as AdminUtils from './utils/admin.utils';
53
- import * as FilterUtils from './utils/filter.utils';
54
- import { ClinicReviewInfo } from '../../types/reviews';
55
- import { PRACTITIONERS_COLLECTION } from '../../types/practitioner';
56
- import { MediaService, MediaAccessLevel } from '../media/media.service';
49
+ } from "../../validations/clinic.schema";
50
+ import { z } from "zod";
51
+ import { Auth } from "firebase/auth";
52
+ import { Firestore } from "firebase/firestore";
53
+ import { FirebaseApp } from "firebase/app";
54
+ import * as ClinicUtils from "./utils/clinic.utils";
55
+ import * as TagUtils from "./utils/tag.utils";
56
+ import * as SearchUtils from "./utils/search.utils";
57
+ import * as AdminUtils from "./utils/admin.utils";
58
+ import * as FilterUtils from "./utils/filter.utils";
59
+ import { ClinicReviewInfo } from "../../types/reviews";
60
+ import { PRACTITIONERS_COLLECTION } from "../../types/practitioner";
61
+ import { MediaService, MediaAccessLevel } from "../media/media.service";
57
62
 
58
63
  export class ClinicService extends BaseService {
59
64
  private clinicGroupService: ClinicGroupService;
60
65
  private clinicAdminService: ClinicAdminService;
61
66
  private mediaService: MediaService;
67
+ private functions: any;
62
68
 
63
69
  constructor(
64
70
  db: Firestore,
@@ -66,12 +72,34 @@ export class ClinicService extends BaseService {
66
72
  app: FirebaseApp,
67
73
  clinicGroupService: ClinicGroupService,
68
74
  clinicAdminService: ClinicAdminService,
69
- mediaService: MediaService,
75
+ mediaService: MediaService
70
76
  ) {
71
77
  super(db, auth, app);
72
78
  this.clinicAdminService = clinicAdminService;
73
79
  this.clinicGroupService = clinicGroupService;
74
80
  this.mediaService = mediaService;
81
+ this.functions = getFunctions(app);
82
+ }
83
+
84
+ /**
85
+ * Get timezone from coordinates using the callable function
86
+ * @param lat Latitude
87
+ * @param lng Longitude
88
+ * @returns IANA timezone string
89
+ */
90
+ private async getTimezone(lat: number, lng: number): Promise<string | null> {
91
+ try {
92
+ const getTimezoneFromCoordinates = httpsCallable(
93
+ this.functions,
94
+ "getTimezoneFromCoordinates"
95
+ );
96
+ const result = await getTimezoneFromCoordinates({ lat, lng });
97
+ const data = result.data as { timezone: string };
98
+ return data.timezone;
99
+ } catch (error) {
100
+ console.error("Error getting timezone:", error);
101
+ return null;
102
+ }
75
103
  }
76
104
 
77
105
  /**
@@ -84,23 +112,25 @@ export class ClinicService extends BaseService {
84
112
  private async processMedia(
85
113
  media: string | File | Blob | null | undefined,
86
114
  ownerId: string,
87
- collectionName: string,
115
+ collectionName: string
88
116
  ): Promise<string | null> {
89
117
  if (!media) return null;
90
118
 
91
119
  // If already a string URL, return it directly
92
- if (typeof media === 'string') {
120
+ if (typeof media === "string") {
93
121
  return media;
94
122
  }
95
123
 
96
124
  // If it's a File, upload it using MediaService
97
125
  if (media instanceof File || media instanceof Blob) {
98
- console.log(`[ClinicService] Uploading ${collectionName} media for ${ownerId}`);
126
+ console.log(
127
+ `[ClinicService] Uploading ${collectionName} media for ${ownerId}`
128
+ );
99
129
  const metadata = await this.mediaService.uploadMedia(
100
130
  media,
101
131
  ownerId,
102
132
  MediaAccessLevel.PUBLIC,
103
- collectionName,
133
+ collectionName
104
134
  );
105
135
  return metadata.url;
106
136
  }
@@ -118,14 +148,18 @@ export class ClinicService extends BaseService {
118
148
  private async processMediaArray(
119
149
  mediaArray: (string | File | Blob)[] | undefined,
120
150
  ownerId: string,
121
- collectionName: string,
151
+ collectionName: string
122
152
  ): Promise<string[]> {
123
153
  if (!mediaArray || mediaArray.length === 0) return [];
124
154
 
125
155
  const result: string[] = [];
126
156
 
127
157
  for (const media of mediaArray) {
128
- const processedUrl = await this.processMedia(media, ownerId, collectionName);
158
+ const processedUrl = await this.processMedia(
159
+ media,
160
+ ownerId,
161
+ collectionName
162
+ );
129
163
  if (processedUrl) {
130
164
  result.push(processedUrl);
131
165
  }
@@ -144,14 +178,18 @@ export class ClinicService extends BaseService {
144
178
  private async processPhotosWithTags(
145
179
  photosWithTags: { url: string | File | Blob; tag: string }[] | undefined,
146
180
  ownerId: string,
147
- collectionName: string,
181
+ collectionName: string
148
182
  ): Promise<{ url: string; tag: string }[]> {
149
183
  if (!photosWithTags || photosWithTags.length === 0) return [];
150
184
 
151
185
  const result: { url: string; tag: string }[] = [];
152
186
 
153
187
  for (const item of photosWithTags) {
154
- const processedUrl = await this.processMedia(item.url, ownerId, collectionName);
188
+ const processedUrl = await this.processMedia(
189
+ item.url,
190
+ ownerId,
191
+ collectionName
192
+ );
155
193
  if (processedUrl) {
156
194
  result.push({
157
195
  url: processedUrl,
@@ -167,7 +205,10 @@ export class ClinicService extends BaseService {
167
205
  * Creates a new clinic.
168
206
  * Handles both URL strings and File uploads for media fields.
169
207
  */
170
- async createClinic(data: CreateClinicData, creatorAdminId: string): Promise<Clinic> {
208
+ async createClinic(
209
+ data: CreateClinicData,
210
+ creatorAdminId: string
211
+ ): Promise<Clinic> {
171
212
  try {
172
213
  // Generate ID first so we can use it for media uploads
173
214
  const clinicId = this.generateId();
@@ -175,31 +216,40 @@ export class ClinicService extends BaseService {
175
216
  // Validate data - this now works because mediaResourceSchema has been updated to support Files/Blobs
176
217
  const validatedData = createClinicSchema.parse(data);
177
218
 
178
- const group = await this.clinicGroupService.getClinicGroup(validatedData.clinicGroupId);
219
+ const group = await this.clinicGroupService.getClinicGroup(
220
+ validatedData.clinicGroupId
221
+ );
179
222
  if (!group) {
180
- throw new Error(`Clinic group ${validatedData.clinicGroupId} not found`);
223
+ throw new Error(
224
+ `Clinic group ${validatedData.clinicGroupId} not found`
225
+ );
181
226
  }
182
227
 
183
228
  // Process media fields - convert File/Blob objects to URLs
184
- const logoUrl = await this.processMedia(validatedData.logo, clinicId, 'clinic-logos');
229
+ const logoUrl = await this.processMedia(
230
+ validatedData.logo,
231
+ clinicId,
232
+ "clinic-logos"
233
+ );
185
234
  const coverPhotoUrl = await this.processMedia(
186
235
  validatedData.coverPhoto,
187
236
  clinicId,
188
- 'clinic-cover-photos',
237
+ "clinic-cover-photos"
189
238
  );
190
239
  const featuredPhotos = await this.processMediaArray(
191
240
  validatedData.featuredPhotos,
192
241
  clinicId,
193
- 'clinic-featured-photos',
242
+ "clinic-featured-photos"
194
243
  );
195
244
  const photosWithTags = await this.processPhotosWithTags(
196
245
  validatedData.photosWithTags,
197
246
  clinicId,
198
- 'clinic-gallery',
247
+ "clinic-gallery"
199
248
  );
200
249
 
201
250
  const location = validatedData.location;
202
251
  const hash = geohashForLocation([location.latitude, location.longitude]);
252
+ const tz = await this.getTimezone(location.latitude, location.longitude);
203
253
 
204
254
  const defaultReviewInfo: ClinicReviewInfo = {
205
255
  totalReviews: 0,
@@ -212,7 +262,7 @@ export class ClinicService extends BaseService {
212
262
  recommendationPercentage: 0,
213
263
  };
214
264
 
215
- const clinicData: Omit<Clinic, 'createdAt' | 'updatedAt'> & {
265
+ const clinicData: Omit<Clinic, "createdAt" | "updatedAt"> & {
216
266
  createdAt: FieldValue;
217
267
  updatedAt: FieldValue;
218
268
  } = {
@@ -221,7 +271,7 @@ export class ClinicService extends BaseService {
221
271
  name: validatedData.name,
222
272
  nameLower: validatedData.name.toLowerCase(), // Add this line
223
273
  description: validatedData.description,
224
- location: { ...location, geohash: hash },
274
+ location: { ...location, geohash: hash, tz },
225
275
  contactInfo: validatedData.contactInfo,
226
276
  workingHours: validatedData.workingHours,
227
277
  tags: validatedData.tags,
@@ -234,8 +284,12 @@ export class ClinicService extends BaseService {
234
284
  proceduresInfo: validatedData.proceduresInfo || [],
235
285
  reviewInfo: defaultReviewInfo,
236
286
  admins: [creatorAdminId],
237
- isActive: validatedData.isActive !== undefined ? validatedData.isActive : true,
238
- isVerified: validatedData.isVerified !== undefined ? validatedData.isVerified : false,
287
+ isActive:
288
+ validatedData.isActive !== undefined ? validatedData.isActive : true,
289
+ isVerified:
290
+ validatedData.isVerified !== undefined
291
+ ? validatedData.isVerified
292
+ : false,
239
293
  logo: logoUrl,
240
294
  createdAt: serverTimestamp(),
241
295
  updatedAt: serverTimestamp(),
@@ -266,13 +320,13 @@ export class ClinicService extends BaseService {
266
320
  console.log(`[ClinicService] Clinic created successfully: ${clinicId}`);
267
321
 
268
322
  const savedClinic = await this.getClinic(clinicId);
269
- if (!savedClinic) throw new Error('Failed to retrieve created clinic');
323
+ if (!savedClinic) throw new Error("Failed to retrieve created clinic");
270
324
  return savedClinic;
271
325
  } catch (error) {
272
326
  if (error instanceof z.ZodError) {
273
- throw new Error('Invalid clinic data: ' + error.message);
327
+ throw new Error("Invalid clinic data: " + error.message);
274
328
  }
275
- console.error('Error creating clinic:', error);
329
+ console.error("Error creating clinic:", error);
276
330
  throw error;
277
331
  }
278
332
  }
@@ -284,7 +338,7 @@ export class ClinicService extends BaseService {
284
338
  async updateClinic(
285
339
  clinicId: string,
286
340
  data: Partial<CreateClinicData>,
287
- adminId: string,
341
+ adminId: string
288
342
  ): Promise<Clinic> {
289
343
  try {
290
344
  // First check if clinic exists
@@ -304,14 +358,18 @@ export class ClinicService extends BaseService {
304
358
 
305
359
  // Process media fields if provided
306
360
  if (validatedData.logo !== undefined) {
307
- updatePayload.logo = await this.processMedia(validatedData.logo, clinicId, 'clinic-logos');
361
+ updatePayload.logo = await this.processMedia(
362
+ validatedData.logo,
363
+ clinicId,
364
+ "clinic-logos"
365
+ );
308
366
  }
309
367
 
310
368
  if (validatedData.coverPhoto !== undefined) {
311
369
  updatePayload.coverPhoto = await this.processMedia(
312
370
  validatedData.coverPhoto,
313
371
  clinicId,
314
- 'clinic-cover-photos',
372
+ "clinic-cover-photos"
315
373
  );
316
374
  }
317
375
 
@@ -319,7 +377,7 @@ export class ClinicService extends BaseService {
319
377
  updatePayload.featuredPhotos = await this.processMediaArray(
320
378
  validatedData.featuredPhotos,
321
379
  clinicId,
322
- 'clinic-featured-photos',
380
+ "clinic-featured-photos"
323
381
  );
324
382
  }
325
383
 
@@ -327,27 +385,28 @@ export class ClinicService extends BaseService {
327
385
  updatePayload.photosWithTags = await this.processPhotosWithTags(
328
386
  validatedData.photosWithTags,
329
387
  clinicId,
330
- 'clinic-gallery',
388
+ "clinic-gallery"
331
389
  );
332
390
  }
333
391
 
334
392
  // Process non-media fields
335
393
  const fieldsToUpdate = [
336
- 'name',
337
- 'description',
338
- 'contactInfo',
339
- 'workingHours',
340
- 'tags',
341
- 'doctors',
342
- 'procedures',
343
- 'proceduresInfo',
344
- 'isActive',
345
- 'isVerified',
394
+ "name",
395
+ "description",
396
+ "contactInfo",
397
+ "workingHours",
398
+ "tags",
399
+ "doctors",
400
+ "procedures",
401
+ "proceduresInfo",
402
+ "isActive",
403
+ "isVerified",
346
404
  ];
347
405
 
348
406
  for (const field of fieldsToUpdate) {
349
407
  if (validatedData[field as keyof typeof validatedData] !== undefined) {
350
- updatePayload[field] = validatedData[field as keyof typeof validatedData];
408
+ updatePayload[field] =
409
+ validatedData[field as keyof typeof validatedData];
351
410
  }
352
411
  }
353
412
 
@@ -359,9 +418,11 @@ export class ClinicService extends BaseService {
359
418
  // Handle location update with geohash
360
419
  if (validatedData.location) {
361
420
  const loc = validatedData.location;
421
+ const tz = await this.getTimezone(loc.latitude, loc.longitude);
362
422
  updatePayload.location = {
363
423
  ...loc,
364
424
  geohash: geohashForLocation([loc.latitude, loc.longitude]),
425
+ tz,
365
426
  };
366
427
  }
367
428
 
@@ -374,11 +435,11 @@ export class ClinicService extends BaseService {
374
435
 
375
436
  // Return the updated clinic
376
437
  const updatedClinic = await this.getClinic(clinicId);
377
- if (!updatedClinic) throw new Error('Failed to retrieve updated clinic');
438
+ if (!updatedClinic) throw new Error("Failed to retrieve updated clinic");
378
439
  return updatedClinic;
379
440
  } catch (error) {
380
441
  if (error instanceof z.ZodError) {
381
- throw new Error('Invalid clinic update data: ' + error.message);
442
+ throw new Error("Invalid clinic update data: " + error.message);
382
443
  }
383
444
  console.error(`Error updating clinic ${clinicId}:`, error);
384
445
  throw error;
@@ -433,9 +494,14 @@ export class ClinicService extends BaseService {
433
494
  procedures?: string[];
434
495
  tags?: ClinicTag[];
435
496
  // Add other relevant filters based on Clinic/ProcedureSummaryInfo fields
436
- },
497
+ }
437
498
  ): Promise<Clinic[]> {
438
- return SearchUtils.findClinicsInRadius(this.db, center, radiusInKm, filters);
499
+ return SearchUtils.findClinicsInRadius(
500
+ this.db,
501
+ center,
502
+ radiusInKm,
503
+ filters
504
+ );
439
505
  }
440
506
 
441
507
  async addTags(
@@ -443,9 +509,16 @@ export class ClinicService extends BaseService {
443
509
  adminId: string,
444
510
  newTags: {
445
511
  tags?: ClinicTag[];
446
- },
512
+ }
447
513
  ): Promise<Clinic> {
448
- return TagUtils.addTags(this.db, clinicId, adminId, newTags, this.clinicAdminService, this.app);
514
+ return TagUtils.addTags(
515
+ this.db,
516
+ clinicId,
517
+ adminId,
518
+ newTags,
519
+ this.clinicAdminService,
520
+ this.app
521
+ );
449
522
  }
450
523
 
451
524
  async removeTags(
@@ -453,7 +526,7 @@ export class ClinicService extends BaseService {
453
526
  adminId: string,
454
527
  tagsToRemove: {
455
528
  tags?: ClinicTag[];
456
- },
529
+ }
457
530
  ): Promise<Clinic> {
458
531
  return TagUtils.removeTags(
459
532
  this.db,
@@ -461,7 +534,7 @@ export class ClinicService extends BaseService {
461
534
  adminId,
462
535
  tagsToRemove,
463
536
  this.clinicAdminService,
464
- this.app,
537
+ this.app
465
538
  );
466
539
  }
467
540
 
@@ -470,14 +543,14 @@ export class ClinicService extends BaseService {
470
543
  options?: {
471
544
  isActive?: boolean;
472
545
  includeGroupClinics?: boolean; // ako je true i admin je owner, uključuje sve klinike grupe
473
- },
546
+ }
474
547
  ): Promise<Clinic[]> {
475
548
  return ClinicUtils.getClinicsByAdmin(
476
549
  this.db,
477
550
  adminId,
478
551
  options,
479
552
  this.clinicAdminService,
480
- this.clinicGroupService,
553
+ this.clinicGroupService
481
554
  );
482
555
  }
483
556
 
@@ -486,7 +559,7 @@ export class ClinicService extends BaseService {
486
559
  this.db,
487
560
  adminId,
488
561
  this.clinicAdminService,
489
- this.clinicGroupService,
562
+ this.clinicGroupService
490
563
  );
491
564
  }
492
565
 
@@ -497,22 +570,24 @@ export class ClinicService extends BaseService {
497
570
  async createClinicBranch(
498
571
  clinicGroupId: string,
499
572
  setupData: ClinicBranchSetupData,
500
- adminId: string,
573
+ adminId: string
501
574
  ): Promise<Clinic> {
502
- console.log('[CLINIC_SERVICE] Starting clinic branch creation', {
575
+ console.log("[CLINIC_SERVICE] Starting clinic branch creation", {
503
576
  clinicGroupId,
504
577
  adminId,
505
578
  });
506
579
 
507
580
  // Verify clinic group exists
508
- const clinicGroup = await this.clinicGroupService.getClinicGroup(clinicGroupId);
581
+ const clinicGroup = await this.clinicGroupService.getClinicGroup(
582
+ clinicGroupId
583
+ );
509
584
  if (!clinicGroup) {
510
- console.error('[CLINIC_SERVICE] Clinic group not found', {
585
+ console.error("[CLINIC_SERVICE] Clinic group not found", {
511
586
  clinicGroupId,
512
587
  });
513
588
  throw new Error(`Clinic group with ID ${clinicGroupId} not found`);
514
589
  }
515
- console.log('[CLINIC_SERVICE] Clinic group verified');
590
+ console.log("[CLINIC_SERVICE] Clinic group verified");
516
591
 
517
592
  // Validate branch setup data first
518
593
  const validatedSetupData = clinicBranchSetupSchema.parse(setupData);
@@ -538,7 +613,7 @@ export class ClinicService extends BaseService {
538
613
  isVerified: false,
539
614
  };
540
615
 
541
- console.log('[CLINIC_SERVICE] Creating clinic branch', {
616
+ console.log("[CLINIC_SERVICE] Creating clinic branch", {
542
617
  name: createClinicData.name,
543
618
  hasLogo: !!createClinicData.logo,
544
619
  hasCoverPhoto: !!createClinicData.coverPhoto,
@@ -547,7 +622,7 @@ export class ClinicService extends BaseService {
547
622
  // Use createClinic which now handles validation and media uploads
548
623
  const clinic = await this.createClinic(createClinicData, adminId);
549
624
 
550
- console.log('[CLINIC_SERVICE] Clinic branch created successfully', {
625
+ console.log("[CLINIC_SERVICE] Clinic branch created successfully", {
551
626
  clinicId: clinic.id,
552
627
  });
553
628
  return clinic;
@@ -559,7 +634,7 @@ export class ClinicService extends BaseService {
559
634
 
560
635
  async getAllClinics(
561
636
  pagination?: number,
562
- lastDoc?: any,
637
+ lastDoc?: any
563
638
  ): Promise<{ clinics: Clinic[]; lastDoc: any }> {
564
639
  return ClinicUtils.getAllClinics(this.db, pagination, lastDoc);
565
640
  }
@@ -568,9 +643,15 @@ export class ClinicService extends BaseService {
568
643
  center: { latitude: number; longitude: number },
569
644
  rangeInKm: number,
570
645
  pagination?: number,
571
- lastDoc?: any,
646
+ lastDoc?: any
572
647
  ): Promise<{ clinics: (Clinic & { distance: number })[]; lastDoc: any }> {
573
- return ClinicUtils.getAllClinicsInRange(this.db, center, rangeInKm, pagination, lastDoc);
648
+ return ClinicUtils.getAllClinicsInRange(
649
+ this.db,
650
+ center,
651
+ rangeInKm,
652
+ pagination,
653
+ lastDoc
654
+ );
574
655
  }
575
656
 
576
657
  /**
@@ -621,7 +702,7 @@ export class ClinicService extends BaseService {
621
702
  return {
622
703
  id: doc.id,
623
704
  name: data.name,
624
- address: data.location?.address || '',
705
+ address: data.location?.address || "",
625
706
  latitude: data.location?.latitude,
626
707
  longitude: data.location?.longitude,
627
708
  };
@@ -221,7 +221,8 @@ export class ProcedureService extends BaseService {
221
221
  const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
222
222
  id: procedureId,
223
223
  ...validatedData,
224
- nameLower: validatedData.nameLower || validatedData.name.toLowerCase(),
224
+ // Ensure nameLower is always set even if omitted by client
225
+ nameLower: (validatedData as any).nameLower || validatedData.name.toLowerCase(),
225
226
  photos: processedPhotos,
226
227
  category, // Embed full objects
227
228
  subcategory,
@@ -376,7 +377,7 @@ export class ProcedureService extends BaseService {
376
377
  const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
377
378
  id: procedureId,
378
379
  ...validatedData,
379
- nameLower: validatedData.nameLower || validatedData.name.toLowerCase(),
380
+ nameLower: (validatedData as any).nameLower || validatedData.name.toLowerCase(),
380
381
  practitionerId: practitionerId, // Override practitionerId with the correct one
381
382
  photos: processedPhotos,
382
383
  category,
@@ -196,6 +196,8 @@ export interface Appointment {
196
196
  clinicBranchId: string;
197
197
  /** Aggregated clinic information (snapshot) */
198
198
  clinicInfo: ClinicInfo;
199
+ /** IANA timezone of the clinic */
200
+ clinic_tz: string;
199
201
 
200
202
  /** ID of the practitioner */
201
203
  practitionerId: string;
@@ -299,6 +301,7 @@ export interface CreateAppointmentData {
299
301
  patientNotes?: string | null;
300
302
  initialStatus: AppointmentStatus;
301
303
  initialPaymentStatus?: PaymentStatus; // Defaults to UNPAID if not provided
304
+ clinic_tz: string;
302
305
  }
303
306
 
304
307
  /**
@@ -336,6 +339,7 @@ export interface UpdateAppointmentData {
336
339
  cost?: number; // If cost is adjusted
337
340
  clinicBranchId?: string; // If appointment is moved to another branch (complex scenario)
338
341
  practitionerId?: string; // If practitioner is changed
342
+ clinic_tz?: string;
339
343
 
340
344
  /** NEW: For updating linked forms - typically managed by dedicated methods */
341
345
  linkedFormIds?: string[] | FieldValue;
@@ -44,6 +44,7 @@ export interface ClinicLocation {
44
44
  latitude: number;
45
45
  longitude: number;
46
46
  geohash?: string | null;
47
+ tz?: string | null;
47
48
  }
48
49
 
49
50
  /**
@@ -228,6 +229,7 @@ export interface CreateClinicGroupData {
228
229
  calendarSyncEnabled?: boolean;
229
230
  autoConfirmAppointments?: boolean;
230
231
  businessIdentificationNumber?: string | null;
232
+ tz?: string | null;
231
233
  onboarding?: {
232
234
  completed?: boolean;
233
235
  step?: number;
@@ -248,6 +248,7 @@ export const createAppointmentSchema = z
248
248
  initialPaymentStatus: paymentStatusSchema
249
249
  .optional()
250
250
  .default(PaymentStatus.UNPAID),
251
+ clinic_tz: z.string().min(1, "Timezone is required"),
251
252
  })
252
253
  .refine((data) => data.appointmentEndTime > data.appointmentStartTime, {
253
254
  message: "Appointment end time must be after start time",
@@ -325,6 +326,7 @@ export const updateAppointmentSchema = z
325
326
  cost: z.number().min(0).optional(),
326
327
  clinicBranchId: z.string().min(MIN_STRING_LENGTH).optional(),
327
328
  practitionerId: z.string().min(MIN_STRING_LENGTH).optional(),
329
+ clinic_tz: z.string().min(MIN_STRING_LENGTH).optional(),
328
330
  linkedForms: z
329
331
  .union([z.array(linkedFormInfoSchema).max(MAX_ARRAY_LENGTH), z.any()])
330
332
  .optional(),
@@ -42,6 +42,7 @@ export const clinicLocationSchema = z.object({
42
42
  latitude: z.number().min(-90).max(90),
43
43
  longitude: z.number().min(-180).max(180),
44
44
  geohash: z.string().nullable().optional(),
45
+ tz: z.string().nullable().optional(),
45
46
  });
46
47
 
47
48
  /**
@@ -261,6 +262,7 @@ export const createClinicGroupSchema = z.object({
261
262
  calendarSyncEnabled: z.boolean().optional(),
262
263
  autoConfirmAppointments: z.boolean().optional(),
263
264
  businessIdentificationNumber: z.string().optional().nullable(),
265
+ tz: z.string().nullable().optional(),
264
266
  onboarding: z
265
267
  .object({
266
268
  completed: z.boolean().optional().default(false),
@@ -1,18 +1,16 @@
1
- import { z } from "zod";
2
- import { ProcedureFamily } from "../backoffice/types/static/procedure-family.types";
3
- import {
4
- Currency,
5
- PricingMeasure,
6
- } from "../backoffice/types/static/pricing.types";
7
- import { clinicInfoSchema, doctorInfoSchema } from "./shared.schema";
8
- import { procedureReviewInfoSchema } from "./reviews.schema";
9
- import { mediaResourceSchema } from "./media.schema";
1
+ import { z } from 'zod';
2
+ import { ProcedureFamily } from '../backoffice/types/static/procedure-family.types';
3
+ import { Currency, PricingMeasure } from '../backoffice/types/static/pricing.types';
4
+ import { clinicInfoSchema, doctorInfoSchema } from './shared.schema';
5
+ import { procedureReviewInfoSchema } from './reviews.schema';
6
+ import { mediaResourceSchema } from './media.schema';
10
7
  /**
11
8
  * Schema for creating a new procedure
12
9
  */
13
10
  export const createProcedureSchema = z.object({
14
11
  name: z.string().min(1).max(200),
15
- nameLower: z.string().min(1).max(200),
12
+ // Optional: service will derive from name if not provided by client
13
+ nameLower: z.string().min(1).max(200).optional(),
16
14
  description: z.string().min(1).max(2000),
17
15
  family: z.nativeEnum(ProcedureFamily),
18
16
  categoryId: z.string().min(1),