@blackcode_sa/metaestetics-api 1.13.1 → 1.13.3

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
@@ -6736,6 +6736,40 @@ declare class ProcedureService extends BaseService {
6736
6736
  * @returns The created procedure
6737
6737
  */
6738
6738
  createProcedure(data: CreateProcedureData): Promise<Procedure>;
6739
+ /**
6740
+ * Validates if a practitioner can perform a procedure based on certification requirements.
6741
+ *
6742
+ * @param procedure - The procedure to check
6743
+ * @param practitioner - The practitioner to validate
6744
+ * @returns true if practitioner can perform the procedure, false otherwise
6745
+ */
6746
+ canPractitionerPerformProcedure(procedure: Procedure, practitioner: Practitioner): boolean;
6747
+ /**
6748
+ * Clones an existing procedure for a target practitioner.
6749
+ * This creates a new procedure document with the same data as the source procedure,
6750
+ * but linked to the target practitioner.
6751
+ *
6752
+ * @param sourceProcedureId - The ID of the procedure to clone
6753
+ * @param targetPractitionerId - The ID of the practitioner to assign the cloned procedure to
6754
+ * @param overrides - Optional overrides for the new procedure (e.g. price, duration, isActive)
6755
+ * @returns The newly created procedure
6756
+ */
6757
+ cloneProcedureForPractitioner(sourceProcedureId: string, targetPractitionerId: string, overrides?: Partial<CreateProcedureData> & {
6758
+ isActive?: boolean;
6759
+ }): Promise<Procedure>;
6760
+ /**
6761
+ * Clones an existing procedure for multiple target practitioners.
6762
+ * This creates new procedure documents with the same data as the source procedure,
6763
+ * but linked to each target practitioner.
6764
+ *
6765
+ * @param sourceProcedureId - The ID of the procedure to clone
6766
+ * @param targetPractitionerIds - Array of practitioner IDs to assign the cloned procedure to
6767
+ * @param overrides - Optional overrides for the new procedures (e.g. price, duration, isActive)
6768
+ * @returns Array of newly created procedures
6769
+ */
6770
+ bulkCloneProcedureForPractitioners(sourceProcedureId: string, targetPractitionerIds: string[], overrides?: Partial<CreateProcedureData> & {
6771
+ isActive?: boolean;
6772
+ }): Promise<Procedure[]>;
6739
6773
  /**
6740
6774
  * Creates multiple procedures for a list of practitioners based on common data.
6741
6775
  * This method is optimized for bulk creation to reduce database reads and writes.
package/dist/index.d.ts CHANGED
@@ -6736,6 +6736,40 @@ declare class ProcedureService extends BaseService {
6736
6736
  * @returns The created procedure
6737
6737
  */
6738
6738
  createProcedure(data: CreateProcedureData): Promise<Procedure>;
6739
+ /**
6740
+ * Validates if a practitioner can perform a procedure based on certification requirements.
6741
+ *
6742
+ * @param procedure - The procedure to check
6743
+ * @param practitioner - The practitioner to validate
6744
+ * @returns true if practitioner can perform the procedure, false otherwise
6745
+ */
6746
+ canPractitionerPerformProcedure(procedure: Procedure, practitioner: Practitioner): boolean;
6747
+ /**
6748
+ * Clones an existing procedure for a target practitioner.
6749
+ * This creates a new procedure document with the same data as the source procedure,
6750
+ * but linked to the target practitioner.
6751
+ *
6752
+ * @param sourceProcedureId - The ID of the procedure to clone
6753
+ * @param targetPractitionerId - The ID of the practitioner to assign the cloned procedure to
6754
+ * @param overrides - Optional overrides for the new procedure (e.g. price, duration, isActive)
6755
+ * @returns The newly created procedure
6756
+ */
6757
+ cloneProcedureForPractitioner(sourceProcedureId: string, targetPractitionerId: string, overrides?: Partial<CreateProcedureData> & {
6758
+ isActive?: boolean;
6759
+ }): Promise<Procedure>;
6760
+ /**
6761
+ * Clones an existing procedure for multiple target practitioners.
6762
+ * This creates new procedure documents with the same data as the source procedure,
6763
+ * but linked to each target practitioner.
6764
+ *
6765
+ * @param sourceProcedureId - The ID of the procedure to clone
6766
+ * @param targetPractitionerIds - Array of practitioner IDs to assign the cloned procedure to
6767
+ * @param overrides - Optional overrides for the new procedures (e.g. price, duration, isActive)
6768
+ * @returns Array of newly created procedures
6769
+ */
6770
+ bulkCloneProcedureForPractitioners(sourceProcedureId: string, targetPractitionerIds: string[], overrides?: Partial<CreateProcedureData> & {
6771
+ isActive?: boolean;
6772
+ }): Promise<Procedure[]>;
6739
6773
  /**
6740
6774
  * Creates multiple procedures for a list of practitioners based on common data.
6741
6775
  * This method is optimized for bulk creation to reduce database reads and writes.
package/dist/index.js CHANGED
@@ -20035,6 +20035,25 @@ var ProcedureService = class extends BaseService {
20035
20035
  throw new Error(`Practitioner with ID ${validatedData.practitionerId} not found`);
20036
20036
  }
20037
20037
  const practitioner = practitionerSnapshot.data();
20038
+ const existingProceduresQuery = (0, import_firestore58.query)(
20039
+ (0, import_firestore58.collection)(this.db, PROCEDURES_COLLECTION),
20040
+ (0, import_firestore58.where)("practitionerId", "==", validatedData.practitionerId),
20041
+ (0, import_firestore58.where)("clinicBranchId", "==", validatedData.clinicBranchId),
20042
+ (0, import_firestore58.where)("isActive", "==", true)
20043
+ );
20044
+ const existingProceduresSnapshot = await (0, import_firestore58.getDocs)(existingProceduresQuery);
20045
+ const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
20046
+ const hasSameTechnology = existingProcedures.some(
20047
+ (proc) => {
20048
+ var _a2;
20049
+ return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === validatedData.technologyId;
20050
+ }
20051
+ );
20052
+ if (hasSameTechnology) {
20053
+ throw new Error(
20054
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${(technology == null ? void 0 : technology.name) || validatedData.technologyId}" in this clinic branch`
20055
+ );
20056
+ }
20038
20057
  let processedPhotos = [];
20039
20058
  if (validatedData.photos && validatedData.photos.length > 0) {
20040
20059
  processedPhotos = await this.processMediaArray(
@@ -20137,6 +20156,240 @@ var ProcedureService = class extends BaseService {
20137
20156
  const savedDoc = await (0, import_firestore58.getDoc)(procedureRef);
20138
20157
  return savedDoc.data();
20139
20158
  }
20159
+ /**
20160
+ * Validates if a practitioner can perform a procedure based on certification requirements.
20161
+ *
20162
+ * @param procedure - The procedure to check
20163
+ * @param practitioner - The practitioner to validate
20164
+ * @returns true if practitioner can perform the procedure, false otherwise
20165
+ */
20166
+ canPractitionerPerformProcedure(procedure, practitioner) {
20167
+ if (!practitioner.certification) {
20168
+ return false;
20169
+ }
20170
+ const requiredCert = procedure.certificationRequirement;
20171
+ const practitionerCert = practitioner.certification;
20172
+ const levelOrder = [
20173
+ "aesthetician",
20174
+ "nurse_assistant",
20175
+ "nurse",
20176
+ "nurse_practitioner",
20177
+ "physician_assistant",
20178
+ "doctor",
20179
+ "specialist",
20180
+ "plastic_surgeon"
20181
+ ];
20182
+ const practitionerLevelIndex = levelOrder.indexOf(practitionerCert.level);
20183
+ const requiredLevelIndex = levelOrder.indexOf(requiredCert.minimumLevel);
20184
+ if (practitionerLevelIndex < requiredLevelIndex) {
20185
+ return false;
20186
+ }
20187
+ const requiredSpecialties = requiredCert.requiredSpecialties || [];
20188
+ if (requiredSpecialties.length > 0) {
20189
+ const practitionerSpecialties = practitionerCert.specialties || [];
20190
+ const hasAllRequired = requiredSpecialties.every(
20191
+ (specialty) => practitionerSpecialties.includes(specialty)
20192
+ );
20193
+ if (!hasAllRequired) {
20194
+ return false;
20195
+ }
20196
+ }
20197
+ return true;
20198
+ }
20199
+ /**
20200
+ * Clones an existing procedure for a target practitioner.
20201
+ * This creates a new procedure document with the same data as the source procedure,
20202
+ * but linked to the target practitioner.
20203
+ *
20204
+ * @param sourceProcedureId - The ID of the procedure to clone
20205
+ * @param targetPractitionerId - The ID of the practitioner to assign the cloned procedure to
20206
+ * @param overrides - Optional overrides for the new procedure (e.g. price, duration, isActive)
20207
+ * @returns The newly created procedure
20208
+ */
20209
+ async cloneProcedureForPractitioner(sourceProcedureId, targetPractitionerId, overrides) {
20210
+ var _a, _b, _c;
20211
+ const sourceProcedure = await this.getProcedure(sourceProcedureId);
20212
+ if (!sourceProcedure) {
20213
+ throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
20214
+ }
20215
+ const practitionerRef = (0, import_firestore58.doc)(this.db, PRACTITIONERS_COLLECTION, targetPractitionerId);
20216
+ const practitionerSnapshot = await (0, import_firestore58.getDoc)(practitionerRef);
20217
+ if (!practitionerSnapshot.exists()) {
20218
+ throw new Error(`Target practitioner with ID ${targetPractitionerId} not found`);
20219
+ }
20220
+ const practitioner = practitionerSnapshot.data();
20221
+ if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
20222
+ throw new Error(
20223
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
20224
+ );
20225
+ }
20226
+ const existingProceduresQuery = (0, import_firestore58.query)(
20227
+ (0, import_firestore58.collection)(this.db, PROCEDURES_COLLECTION),
20228
+ (0, import_firestore58.where)("practitionerId", "==", targetPractitionerId),
20229
+ (0, import_firestore58.where)("clinicBranchId", "==", sourceProcedure.clinicBranchId),
20230
+ (0, import_firestore58.where)("isActive", "==", true)
20231
+ );
20232
+ const existingProceduresSnapshot = await (0, import_firestore58.getDocs)(existingProceduresQuery);
20233
+ const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
20234
+ const hasSameTechnology = existingProcedures.some(
20235
+ (proc) => {
20236
+ var _a2, _b2;
20237
+ return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === ((_b2 = sourceProcedure.technology) == null ? void 0 : _b2.id);
20238
+ }
20239
+ );
20240
+ if (hasSameTechnology) {
20241
+ throw new Error(
20242
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${((_a = sourceProcedure.technology) == null ? void 0 : _a.name) || ((_b = sourceProcedure.technology) == null ? void 0 : _b.id)}" in this clinic branch`
20243
+ );
20244
+ }
20245
+ const newProcedureId = this.generateId();
20246
+ const doctorInfo = {
20247
+ id: practitioner.id,
20248
+ name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
20249
+ description: practitioner.basicInfo.bio || "",
20250
+ photo: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : "",
20251
+ rating: ((_c = practitioner.reviewInfo) == null ? void 0 : _c.averageRating) || 0,
20252
+ services: practitioner.procedures || []
20253
+ };
20254
+ const newProcedure = {
20255
+ ...sourceProcedure,
20256
+ id: newProcedureId,
20257
+ practitionerId: targetPractitionerId,
20258
+ doctorInfo,
20259
+ // Link to new doctor
20260
+ // Reset review info for the new procedure
20261
+ reviewInfo: {
20262
+ totalReviews: 0,
20263
+ averageRating: 0,
20264
+ effectivenessOfTreatment: 0,
20265
+ outcomeExplanation: 0,
20266
+ painManagement: 0,
20267
+ followUpCare: 0,
20268
+ valueForMoney: 0,
20269
+ recommendationPercentage: 0
20270
+ },
20271
+ // Apply any overrides if provided
20272
+ ...(overrides == null ? void 0 : overrides.price) !== void 0 && { price: overrides.price },
20273
+ ...(overrides == null ? void 0 : overrides.duration) !== void 0 && { duration: overrides.duration },
20274
+ ...(overrides == null ? void 0 : overrides.description) !== void 0 && { description: overrides.description },
20275
+ // Ensure it's active by default unless specified otherwise
20276
+ isActive: (overrides == null ? void 0 : overrides.isActive) !== void 0 ? overrides.isActive : true
20277
+ };
20278
+ const procedureRef = (0, import_firestore58.doc)(this.db, PROCEDURES_COLLECTION, newProcedureId);
20279
+ await (0, import_firestore58.setDoc)(procedureRef, {
20280
+ ...newProcedure,
20281
+ createdAt: (0, import_firestore58.serverTimestamp)(),
20282
+ updatedAt: (0, import_firestore58.serverTimestamp)()
20283
+ });
20284
+ const savedDoc = await (0, import_firestore58.getDoc)(procedureRef);
20285
+ return savedDoc.data();
20286
+ }
20287
+ /**
20288
+ * Clones an existing procedure for multiple target practitioners.
20289
+ * This creates new procedure documents with the same data as the source procedure,
20290
+ * but linked to each target practitioner.
20291
+ *
20292
+ * @param sourceProcedureId - The ID of the procedure to clone
20293
+ * @param targetPractitionerIds - Array of practitioner IDs to assign the cloned procedure to
20294
+ * @param overrides - Optional overrides for the new procedures (e.g. price, duration, isActive)
20295
+ * @returns Array of newly created procedures
20296
+ */
20297
+ async bulkCloneProcedureForPractitioners(sourceProcedureId, targetPractitionerIds, overrides) {
20298
+ var _a, _b, _c;
20299
+ if (!targetPractitionerIds || targetPractitionerIds.length === 0) {
20300
+ throw new Error("At least one target practitioner ID is required");
20301
+ }
20302
+ const sourceProcedure = await this.getProcedure(sourceProcedureId);
20303
+ if (!sourceProcedure) {
20304
+ throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
20305
+ }
20306
+ const practitionerPromises = targetPractitionerIds.map(
20307
+ (id) => (0, import_firestore58.getDoc)((0, import_firestore58.doc)(this.db, PRACTITIONERS_COLLECTION, id))
20308
+ );
20309
+ const practitionerSnapshots = await Promise.all(practitionerPromises);
20310
+ const practitioners = [];
20311
+ const sourceTechnologyId = (_a = sourceProcedure.technology) == null ? void 0 : _a.id;
20312
+ for (let i = 0; i < practitionerSnapshots.length; i++) {
20313
+ const snapshot = practitionerSnapshots[i];
20314
+ if (!snapshot.exists()) {
20315
+ throw new Error(`Target practitioner with ID ${targetPractitionerIds[i]} not found`);
20316
+ }
20317
+ const practitioner = snapshot.data();
20318
+ if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
20319
+ throw new Error(
20320
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
20321
+ );
20322
+ }
20323
+ const existingProceduresQuery = (0, import_firestore58.query)(
20324
+ (0, import_firestore58.collection)(this.db, PROCEDURES_COLLECTION),
20325
+ (0, import_firestore58.where)("practitionerId", "==", practitioner.id),
20326
+ (0, import_firestore58.where)("clinicBranchId", "==", sourceProcedure.clinicBranchId),
20327
+ (0, import_firestore58.where)("isActive", "==", true)
20328
+ );
20329
+ const existingProceduresSnapshot = await (0, import_firestore58.getDocs)(existingProceduresQuery);
20330
+ const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
20331
+ const hasSameTechnology = existingProcedures.some(
20332
+ (proc) => {
20333
+ var _a2;
20334
+ return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === sourceTechnologyId;
20335
+ }
20336
+ );
20337
+ if (hasSameTechnology) {
20338
+ throw new Error(
20339
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${((_b = sourceProcedure.technology) == null ? void 0 : _b.name) || sourceTechnologyId}" in this clinic branch`
20340
+ );
20341
+ }
20342
+ practitioners.push(practitioner);
20343
+ }
20344
+ const batch = (0, import_firestore58.writeBatch)(this.db);
20345
+ const newProcedures = [];
20346
+ for (const practitioner of practitioners) {
20347
+ const newProcedureId = this.generateId();
20348
+ const doctorInfo = {
20349
+ id: practitioner.id,
20350
+ name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
20351
+ description: practitioner.basicInfo.bio || "",
20352
+ photo: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : "",
20353
+ rating: ((_c = practitioner.reviewInfo) == null ? void 0 : _c.averageRating) || 0,
20354
+ services: practitioner.procedures || []
20355
+ };
20356
+ const newProcedure = {
20357
+ ...sourceProcedure,
20358
+ id: newProcedureId,
20359
+ practitionerId: practitioner.id,
20360
+ doctorInfo,
20361
+ // Reset review info for the new procedure
20362
+ reviewInfo: {
20363
+ totalReviews: 0,
20364
+ averageRating: 0,
20365
+ effectivenessOfTreatment: 0,
20366
+ outcomeExplanation: 0,
20367
+ painManagement: 0,
20368
+ followUpCare: 0,
20369
+ valueForMoney: 0,
20370
+ recommendationPercentage: 0
20371
+ },
20372
+ // Apply any overrides if provided
20373
+ ...(overrides == null ? void 0 : overrides.price) !== void 0 && { price: overrides.price },
20374
+ ...(overrides == null ? void 0 : overrides.duration) !== void 0 && { duration: overrides.duration },
20375
+ ...(overrides == null ? void 0 : overrides.description) !== void 0 && { description: overrides.description },
20376
+ // Ensure it's active by default unless specified otherwise
20377
+ isActive: (overrides == null ? void 0 : overrides.isActive) !== void 0 ? overrides.isActive : true
20378
+ };
20379
+ newProcedures.push(newProcedure);
20380
+ const procedureRef = (0, import_firestore58.doc)(this.db, PROCEDURES_COLLECTION, newProcedureId);
20381
+ batch.set(procedureRef, {
20382
+ ...newProcedure,
20383
+ createdAt: (0, import_firestore58.serverTimestamp)(),
20384
+ updatedAt: (0, import_firestore58.serverTimestamp)()
20385
+ });
20386
+ }
20387
+ await batch.commit();
20388
+ const createdProcedures = await Promise.all(
20389
+ newProcedures.map((p) => this.getProcedure(p.id))
20390
+ );
20391
+ return createdProcedures.filter((p) => p !== null);
20392
+ }
20140
20393
  /**
20141
20394
  * Creates multiple procedures for a list of practitioners based on common data.
20142
20395
  * This method is optimized for bulk creation to reduce database reads and writes.
@@ -20221,6 +20474,40 @@ var ProcedureService = class extends BaseService {
20221
20474
  const notFoundIds = practitionerIds.filter((id) => !foundIds.includes(id));
20222
20475
  throw new Error(`The following practitioners were not found: ${notFoundIds.join(", ")}`);
20223
20476
  }
20477
+ const duplicatePractitioners = [];
20478
+ const duplicateChecks = await Promise.all(
20479
+ practitionerIds.map(async (practitionerId) => {
20480
+ const existingProceduresQuery = (0, import_firestore58.query)(
20481
+ (0, import_firestore58.collection)(this.db, PROCEDURES_COLLECTION),
20482
+ (0, import_firestore58.where)("practitionerId", "==", practitionerId),
20483
+ (0, import_firestore58.where)("clinicBranchId", "==", validatedData.clinicBranchId),
20484
+ (0, import_firestore58.where)("isActive", "==", true)
20485
+ );
20486
+ const existingProceduresSnapshot = await (0, import_firestore58.getDocs)(existingProceduresQuery);
20487
+ const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
20488
+ const hasSameTechnology = existingProcedures.some(
20489
+ (proc) => {
20490
+ var _a2;
20491
+ return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === validatedData.technologyId;
20492
+ }
20493
+ );
20494
+ return { practitionerId, hasSameTechnology };
20495
+ })
20496
+ );
20497
+ duplicateChecks.forEach(({ practitionerId, hasSameTechnology }) => {
20498
+ if (hasSameTechnology) {
20499
+ duplicatePractitioners.push(practitionerId);
20500
+ }
20501
+ });
20502
+ if (duplicatePractitioners.length > 0) {
20503
+ const duplicateNames = duplicatePractitioners.map((id) => {
20504
+ const practitioner = practitionersMap.get(id);
20505
+ return `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`;
20506
+ }).join(", ");
20507
+ throw new Error(
20508
+ `The following practitioner(s) already have a procedure with technology "${(technology == null ? void 0 : technology.name) || validatedData.technologyId}" in this clinic branch: ${duplicateNames}. Please remove them from the selection and try again.`
20509
+ );
20510
+ }
20224
20511
  const batch = (0, import_firestore58.writeBatch)(this.db);
20225
20512
  const createdProcedureIds = [];
20226
20513
  const clinicInfo = {
package/dist/index.mjs CHANGED
@@ -20271,6 +20271,25 @@ var ProcedureService = class extends BaseService {
20271
20271
  throw new Error(`Practitioner with ID ${validatedData.practitionerId} not found`);
20272
20272
  }
20273
20273
  const practitioner = practitionerSnapshot.data();
20274
+ const existingProceduresQuery = query33(
20275
+ collection33(this.db, PROCEDURES_COLLECTION),
20276
+ where33("practitionerId", "==", validatedData.practitionerId),
20277
+ where33("clinicBranchId", "==", validatedData.clinicBranchId),
20278
+ where33("isActive", "==", true)
20279
+ );
20280
+ const existingProceduresSnapshot = await getDocs33(existingProceduresQuery);
20281
+ const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
20282
+ const hasSameTechnology = existingProcedures.some(
20283
+ (proc) => {
20284
+ var _a2;
20285
+ return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === validatedData.technologyId;
20286
+ }
20287
+ );
20288
+ if (hasSameTechnology) {
20289
+ throw new Error(
20290
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${(technology == null ? void 0 : technology.name) || validatedData.technologyId}" in this clinic branch`
20291
+ );
20292
+ }
20274
20293
  let processedPhotos = [];
20275
20294
  if (validatedData.photos && validatedData.photos.length > 0) {
20276
20295
  processedPhotos = await this.processMediaArray(
@@ -20373,6 +20392,240 @@ var ProcedureService = class extends BaseService {
20373
20392
  const savedDoc = await getDoc40(procedureRef);
20374
20393
  return savedDoc.data();
20375
20394
  }
20395
+ /**
20396
+ * Validates if a practitioner can perform a procedure based on certification requirements.
20397
+ *
20398
+ * @param procedure - The procedure to check
20399
+ * @param practitioner - The practitioner to validate
20400
+ * @returns true if practitioner can perform the procedure, false otherwise
20401
+ */
20402
+ canPractitionerPerformProcedure(procedure, practitioner) {
20403
+ if (!practitioner.certification) {
20404
+ return false;
20405
+ }
20406
+ const requiredCert = procedure.certificationRequirement;
20407
+ const practitionerCert = practitioner.certification;
20408
+ const levelOrder = [
20409
+ "aesthetician",
20410
+ "nurse_assistant",
20411
+ "nurse",
20412
+ "nurse_practitioner",
20413
+ "physician_assistant",
20414
+ "doctor",
20415
+ "specialist",
20416
+ "plastic_surgeon"
20417
+ ];
20418
+ const practitionerLevelIndex = levelOrder.indexOf(practitionerCert.level);
20419
+ const requiredLevelIndex = levelOrder.indexOf(requiredCert.minimumLevel);
20420
+ if (practitionerLevelIndex < requiredLevelIndex) {
20421
+ return false;
20422
+ }
20423
+ const requiredSpecialties = requiredCert.requiredSpecialties || [];
20424
+ if (requiredSpecialties.length > 0) {
20425
+ const practitionerSpecialties = practitionerCert.specialties || [];
20426
+ const hasAllRequired = requiredSpecialties.every(
20427
+ (specialty) => practitionerSpecialties.includes(specialty)
20428
+ );
20429
+ if (!hasAllRequired) {
20430
+ return false;
20431
+ }
20432
+ }
20433
+ return true;
20434
+ }
20435
+ /**
20436
+ * Clones an existing procedure for a target practitioner.
20437
+ * This creates a new procedure document with the same data as the source procedure,
20438
+ * but linked to the target practitioner.
20439
+ *
20440
+ * @param sourceProcedureId - The ID of the procedure to clone
20441
+ * @param targetPractitionerId - The ID of the practitioner to assign the cloned procedure to
20442
+ * @param overrides - Optional overrides for the new procedure (e.g. price, duration, isActive)
20443
+ * @returns The newly created procedure
20444
+ */
20445
+ async cloneProcedureForPractitioner(sourceProcedureId, targetPractitionerId, overrides) {
20446
+ var _a, _b, _c;
20447
+ const sourceProcedure = await this.getProcedure(sourceProcedureId);
20448
+ if (!sourceProcedure) {
20449
+ throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
20450
+ }
20451
+ const practitionerRef = doc39(this.db, PRACTITIONERS_COLLECTION, targetPractitionerId);
20452
+ const practitionerSnapshot = await getDoc40(practitionerRef);
20453
+ if (!practitionerSnapshot.exists()) {
20454
+ throw new Error(`Target practitioner with ID ${targetPractitionerId} not found`);
20455
+ }
20456
+ const practitioner = practitionerSnapshot.data();
20457
+ if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
20458
+ throw new Error(
20459
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
20460
+ );
20461
+ }
20462
+ const existingProceduresQuery = query33(
20463
+ collection33(this.db, PROCEDURES_COLLECTION),
20464
+ where33("practitionerId", "==", targetPractitionerId),
20465
+ where33("clinicBranchId", "==", sourceProcedure.clinicBranchId),
20466
+ where33("isActive", "==", true)
20467
+ );
20468
+ const existingProceduresSnapshot = await getDocs33(existingProceduresQuery);
20469
+ const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
20470
+ const hasSameTechnology = existingProcedures.some(
20471
+ (proc) => {
20472
+ var _a2, _b2;
20473
+ return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === ((_b2 = sourceProcedure.technology) == null ? void 0 : _b2.id);
20474
+ }
20475
+ );
20476
+ if (hasSameTechnology) {
20477
+ throw new Error(
20478
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${((_a = sourceProcedure.technology) == null ? void 0 : _a.name) || ((_b = sourceProcedure.technology) == null ? void 0 : _b.id)}" in this clinic branch`
20479
+ );
20480
+ }
20481
+ const newProcedureId = this.generateId();
20482
+ const doctorInfo = {
20483
+ id: practitioner.id,
20484
+ name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
20485
+ description: practitioner.basicInfo.bio || "",
20486
+ photo: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : "",
20487
+ rating: ((_c = practitioner.reviewInfo) == null ? void 0 : _c.averageRating) || 0,
20488
+ services: practitioner.procedures || []
20489
+ };
20490
+ const newProcedure = {
20491
+ ...sourceProcedure,
20492
+ id: newProcedureId,
20493
+ practitionerId: targetPractitionerId,
20494
+ doctorInfo,
20495
+ // Link to new doctor
20496
+ // Reset review info for the new procedure
20497
+ reviewInfo: {
20498
+ totalReviews: 0,
20499
+ averageRating: 0,
20500
+ effectivenessOfTreatment: 0,
20501
+ outcomeExplanation: 0,
20502
+ painManagement: 0,
20503
+ followUpCare: 0,
20504
+ valueForMoney: 0,
20505
+ recommendationPercentage: 0
20506
+ },
20507
+ // Apply any overrides if provided
20508
+ ...(overrides == null ? void 0 : overrides.price) !== void 0 && { price: overrides.price },
20509
+ ...(overrides == null ? void 0 : overrides.duration) !== void 0 && { duration: overrides.duration },
20510
+ ...(overrides == null ? void 0 : overrides.description) !== void 0 && { description: overrides.description },
20511
+ // Ensure it's active by default unless specified otherwise
20512
+ isActive: (overrides == null ? void 0 : overrides.isActive) !== void 0 ? overrides.isActive : true
20513
+ };
20514
+ const procedureRef = doc39(this.db, PROCEDURES_COLLECTION, newProcedureId);
20515
+ await setDoc27(procedureRef, {
20516
+ ...newProcedure,
20517
+ createdAt: serverTimestamp31(),
20518
+ updatedAt: serverTimestamp31()
20519
+ });
20520
+ const savedDoc = await getDoc40(procedureRef);
20521
+ return savedDoc.data();
20522
+ }
20523
+ /**
20524
+ * Clones an existing procedure for multiple target practitioners.
20525
+ * This creates new procedure documents with the same data as the source procedure,
20526
+ * but linked to each target practitioner.
20527
+ *
20528
+ * @param sourceProcedureId - The ID of the procedure to clone
20529
+ * @param targetPractitionerIds - Array of practitioner IDs to assign the cloned procedure to
20530
+ * @param overrides - Optional overrides for the new procedures (e.g. price, duration, isActive)
20531
+ * @returns Array of newly created procedures
20532
+ */
20533
+ async bulkCloneProcedureForPractitioners(sourceProcedureId, targetPractitionerIds, overrides) {
20534
+ var _a, _b, _c;
20535
+ if (!targetPractitionerIds || targetPractitionerIds.length === 0) {
20536
+ throw new Error("At least one target practitioner ID is required");
20537
+ }
20538
+ const sourceProcedure = await this.getProcedure(sourceProcedureId);
20539
+ if (!sourceProcedure) {
20540
+ throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
20541
+ }
20542
+ const practitionerPromises = targetPractitionerIds.map(
20543
+ (id) => getDoc40(doc39(this.db, PRACTITIONERS_COLLECTION, id))
20544
+ );
20545
+ const practitionerSnapshots = await Promise.all(practitionerPromises);
20546
+ const practitioners = [];
20547
+ const sourceTechnologyId = (_a = sourceProcedure.technology) == null ? void 0 : _a.id;
20548
+ for (let i = 0; i < practitionerSnapshots.length; i++) {
20549
+ const snapshot = practitionerSnapshots[i];
20550
+ if (!snapshot.exists()) {
20551
+ throw new Error(`Target practitioner with ID ${targetPractitionerIds[i]} not found`);
20552
+ }
20553
+ const practitioner = snapshot.data();
20554
+ if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
20555
+ throw new Error(
20556
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
20557
+ );
20558
+ }
20559
+ const existingProceduresQuery = query33(
20560
+ collection33(this.db, PROCEDURES_COLLECTION),
20561
+ where33("practitionerId", "==", practitioner.id),
20562
+ where33("clinicBranchId", "==", sourceProcedure.clinicBranchId),
20563
+ where33("isActive", "==", true)
20564
+ );
20565
+ const existingProceduresSnapshot = await getDocs33(existingProceduresQuery);
20566
+ const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
20567
+ const hasSameTechnology = existingProcedures.some(
20568
+ (proc) => {
20569
+ var _a2;
20570
+ return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === sourceTechnologyId;
20571
+ }
20572
+ );
20573
+ if (hasSameTechnology) {
20574
+ throw new Error(
20575
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${((_b = sourceProcedure.technology) == null ? void 0 : _b.name) || sourceTechnologyId}" in this clinic branch`
20576
+ );
20577
+ }
20578
+ practitioners.push(practitioner);
20579
+ }
20580
+ const batch = writeBatch6(this.db);
20581
+ const newProcedures = [];
20582
+ for (const practitioner of practitioners) {
20583
+ const newProcedureId = this.generateId();
20584
+ const doctorInfo = {
20585
+ id: practitioner.id,
20586
+ name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
20587
+ description: practitioner.basicInfo.bio || "",
20588
+ photo: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : "",
20589
+ rating: ((_c = practitioner.reviewInfo) == null ? void 0 : _c.averageRating) || 0,
20590
+ services: practitioner.procedures || []
20591
+ };
20592
+ const newProcedure = {
20593
+ ...sourceProcedure,
20594
+ id: newProcedureId,
20595
+ practitionerId: practitioner.id,
20596
+ doctorInfo,
20597
+ // Reset review info for the new procedure
20598
+ reviewInfo: {
20599
+ totalReviews: 0,
20600
+ averageRating: 0,
20601
+ effectivenessOfTreatment: 0,
20602
+ outcomeExplanation: 0,
20603
+ painManagement: 0,
20604
+ followUpCare: 0,
20605
+ valueForMoney: 0,
20606
+ recommendationPercentage: 0
20607
+ },
20608
+ // Apply any overrides if provided
20609
+ ...(overrides == null ? void 0 : overrides.price) !== void 0 && { price: overrides.price },
20610
+ ...(overrides == null ? void 0 : overrides.duration) !== void 0 && { duration: overrides.duration },
20611
+ ...(overrides == null ? void 0 : overrides.description) !== void 0 && { description: overrides.description },
20612
+ // Ensure it's active by default unless specified otherwise
20613
+ isActive: (overrides == null ? void 0 : overrides.isActive) !== void 0 ? overrides.isActive : true
20614
+ };
20615
+ newProcedures.push(newProcedure);
20616
+ const procedureRef = doc39(this.db, PROCEDURES_COLLECTION, newProcedureId);
20617
+ batch.set(procedureRef, {
20618
+ ...newProcedure,
20619
+ createdAt: serverTimestamp31(),
20620
+ updatedAt: serverTimestamp31()
20621
+ });
20622
+ }
20623
+ await batch.commit();
20624
+ const createdProcedures = await Promise.all(
20625
+ newProcedures.map((p) => this.getProcedure(p.id))
20626
+ );
20627
+ return createdProcedures.filter((p) => p !== null);
20628
+ }
20376
20629
  /**
20377
20630
  * Creates multiple procedures for a list of practitioners based on common data.
20378
20631
  * This method is optimized for bulk creation to reduce database reads and writes.
@@ -20457,6 +20710,40 @@ var ProcedureService = class extends BaseService {
20457
20710
  const notFoundIds = practitionerIds.filter((id) => !foundIds.includes(id));
20458
20711
  throw new Error(`The following practitioners were not found: ${notFoundIds.join(", ")}`);
20459
20712
  }
20713
+ const duplicatePractitioners = [];
20714
+ const duplicateChecks = await Promise.all(
20715
+ practitionerIds.map(async (practitionerId) => {
20716
+ const existingProceduresQuery = query33(
20717
+ collection33(this.db, PROCEDURES_COLLECTION),
20718
+ where33("practitionerId", "==", practitionerId),
20719
+ where33("clinicBranchId", "==", validatedData.clinicBranchId),
20720
+ where33("isActive", "==", true)
20721
+ );
20722
+ const existingProceduresSnapshot = await getDocs33(existingProceduresQuery);
20723
+ const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
20724
+ const hasSameTechnology = existingProcedures.some(
20725
+ (proc) => {
20726
+ var _a2;
20727
+ return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === validatedData.technologyId;
20728
+ }
20729
+ );
20730
+ return { practitionerId, hasSameTechnology };
20731
+ })
20732
+ );
20733
+ duplicateChecks.forEach(({ practitionerId, hasSameTechnology }) => {
20734
+ if (hasSameTechnology) {
20735
+ duplicatePractitioners.push(practitionerId);
20736
+ }
20737
+ });
20738
+ if (duplicatePractitioners.length > 0) {
20739
+ const duplicateNames = duplicatePractitioners.map((id) => {
20740
+ const practitioner = practitionersMap.get(id);
20741
+ return `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`;
20742
+ }).join(", ");
20743
+ throw new Error(
20744
+ `The following practitioner(s) already have a procedure with technology "${(technology == null ? void 0 : technology.name) || validatedData.technologyId}" in this clinic branch: ${duplicateNames}. Please remove them from the selection and try again.`
20745
+ );
20746
+ }
20460
20747
  const batch = writeBatch6(this.db);
20461
20748
  const createdProcedureIds = [];
20462
20749
  const clinicInfo = {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.13.1",
4
+ "version": "1.13.3",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -244,6 +244,25 @@ export class ProcedureService extends BaseService {
244
244
  }
245
245
  const practitioner = practitionerSnapshot.data() as Practitioner; // Assert type
246
246
 
247
+ // Check if practitioner already has a procedure with the same technology ID in this clinic branch
248
+ const existingProceduresQuery = query(
249
+ collection(this.db, PROCEDURES_COLLECTION),
250
+ where('practitionerId', '==', validatedData.practitionerId),
251
+ where('clinicBranchId', '==', validatedData.clinicBranchId),
252
+ where('isActive', '==', true)
253
+ );
254
+ const existingProceduresSnapshot = await getDocs(existingProceduresQuery);
255
+ const existingProcedures = existingProceduresSnapshot.docs.map(doc => doc.data() as Procedure);
256
+
257
+ const hasSameTechnology = existingProcedures.some(
258
+ proc => proc.technology?.id === validatedData.technologyId
259
+ );
260
+ if (hasSameTechnology) {
261
+ throw new Error(
262
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${technology?.name || validatedData.technologyId}" in this clinic branch`
263
+ );
264
+ }
265
+
247
266
  // Process photos if provided
248
267
  let processedPhotos: string[] = [];
249
268
  if (validatedData.photos && validatedData.photos.length > 0) {
@@ -370,6 +389,308 @@ export class ProcedureService extends BaseService {
370
389
  return savedDoc.data() as Procedure;
371
390
  }
372
391
 
392
+ /**
393
+ * Validates if a practitioner can perform a procedure based on certification requirements.
394
+ *
395
+ * @param procedure - The procedure to check
396
+ * @param practitioner - The practitioner to validate
397
+ * @returns true if practitioner can perform the procedure, false otherwise
398
+ */
399
+ canPractitionerPerformProcedure(procedure: Procedure, practitioner: Practitioner): boolean {
400
+ if (!practitioner.certification) {
401
+ return false;
402
+ }
403
+
404
+ const requiredCert = procedure.certificationRequirement;
405
+ const practitionerCert = practitioner.certification;
406
+
407
+ // Check certification level
408
+ const levelOrder = [
409
+ 'aesthetician',
410
+ 'nurse_assistant',
411
+ 'nurse',
412
+ 'nurse_practitioner',
413
+ 'physician_assistant',
414
+ 'doctor',
415
+ 'specialist',
416
+ 'plastic_surgeon',
417
+ ];
418
+
419
+ const practitionerLevelIndex = levelOrder.indexOf(practitionerCert.level);
420
+ const requiredLevelIndex = levelOrder.indexOf(requiredCert.minimumLevel);
421
+
422
+ if (practitionerLevelIndex < requiredLevelIndex) {
423
+ return false;
424
+ }
425
+
426
+ // Check required specialties
427
+ const requiredSpecialties = requiredCert.requiredSpecialties || [];
428
+ if (requiredSpecialties.length > 0) {
429
+ const practitionerSpecialties = practitionerCert.specialties || [];
430
+ const hasAllRequired = requiredSpecialties.every(specialty =>
431
+ practitionerSpecialties.includes(specialty)
432
+ );
433
+ if (!hasAllRequired) {
434
+ return false;
435
+ }
436
+ }
437
+
438
+ return true;
439
+ }
440
+
441
+ /**
442
+ * Clones an existing procedure for a target practitioner.
443
+ * This creates a new procedure document with the same data as the source procedure,
444
+ * but linked to the target practitioner.
445
+ *
446
+ * @param sourceProcedureId - The ID of the procedure to clone
447
+ * @param targetPractitionerId - The ID of the practitioner to assign the cloned procedure to
448
+ * @param overrides - Optional overrides for the new procedure (e.g. price, duration, isActive)
449
+ * @returns The newly created procedure
450
+ */
451
+ async cloneProcedureForPractitioner(
452
+ sourceProcedureId: string,
453
+ targetPractitionerId: string,
454
+ overrides?: Partial<CreateProcedureData> & { isActive?: boolean }
455
+ ): Promise<Procedure> {
456
+ // 1. Fetch source procedure
457
+ const sourceProcedure = await this.getProcedure(sourceProcedureId);
458
+ if (!sourceProcedure) {
459
+ throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
460
+ }
461
+
462
+ // 2. Fetch target practitioner
463
+ const practitionerRef = doc(this.db, PRACTITIONERS_COLLECTION, targetPractitionerId);
464
+ const practitionerSnapshot = await getDoc(practitionerRef);
465
+ if (!practitionerSnapshot.exists()) {
466
+ throw new Error(`Target practitioner with ID ${targetPractitionerId} not found`);
467
+ }
468
+ const practitioner = practitionerSnapshot.data() as Practitioner;
469
+
470
+ // 3. Validate certification
471
+ if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
472
+ throw new Error(
473
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
474
+ );
475
+ }
476
+
477
+ // 4. Check if practitioner already has a procedure with the same technology ID in this clinic branch
478
+ const existingProceduresQuery = query(
479
+ collection(this.db, PROCEDURES_COLLECTION),
480
+ where('practitionerId', '==', targetPractitionerId),
481
+ where('clinicBranchId', '==', sourceProcedure.clinicBranchId),
482
+ where('isActive', '==', true)
483
+ );
484
+ const existingProceduresSnapshot = await getDocs(existingProceduresQuery);
485
+ const existingProcedures = existingProceduresSnapshot.docs.map(doc => doc.data() as Procedure);
486
+
487
+ const hasSameTechnology = existingProcedures.some(
488
+ proc => proc.technology?.id === sourceProcedure.technology?.id
489
+ );
490
+ if (hasSameTechnology) {
491
+ throw new Error(
492
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${sourceProcedure.technology?.name || sourceProcedure.technology?.id}" in this clinic branch`
493
+ );
494
+ }
495
+
496
+ // 5. Prepare data for new procedure
497
+ const newProcedureId = this.generateId();
498
+
499
+ // Create aggregated doctor info for the new procedure
500
+ const doctorInfo = {
501
+ id: practitioner.id,
502
+ name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
503
+ description: practitioner.basicInfo.bio || '',
504
+ photo:
505
+ typeof practitioner.basicInfo.profileImageUrl === 'string'
506
+ ? practitioner.basicInfo.profileImageUrl
507
+ : '',
508
+ rating: practitioner.reviewInfo?.averageRating || 0,
509
+ services: practitioner.procedures || [],
510
+ };
511
+
512
+ // Construct the new procedure object
513
+ // We copy everything from source, but override specific fields
514
+ const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
515
+ ...sourceProcedure,
516
+ id: newProcedureId,
517
+ practitionerId: targetPractitionerId,
518
+ doctorInfo, // Link to new doctor
519
+
520
+ // Reset review info for the new procedure
521
+ reviewInfo: {
522
+ totalReviews: 0,
523
+ averageRating: 0,
524
+ effectivenessOfTreatment: 0,
525
+ outcomeExplanation: 0,
526
+ painManagement: 0,
527
+ followUpCare: 0,
528
+ valueForMoney: 0,
529
+ recommendationPercentage: 0,
530
+ },
531
+
532
+ // Apply any overrides if provided
533
+ ...(overrides?.price !== undefined && { price: overrides.price }),
534
+ ...(overrides?.duration !== undefined && { duration: overrides.duration }),
535
+ ...(overrides?.description !== undefined && { description: overrides.description }),
536
+
537
+ // Ensure it's active by default unless specified otherwise
538
+ isActive: overrides?.isActive !== undefined ? overrides.isActive : true,
539
+ };
540
+
541
+ // 6. Save to Firestore
542
+ const procedureRef = doc(this.db, PROCEDURES_COLLECTION, newProcedureId);
543
+ await setDoc(procedureRef, {
544
+ ...newProcedure,
545
+ createdAt: serverTimestamp(),
546
+ updatedAt: serverTimestamp(),
547
+ });
548
+
549
+ // 7. Return the new procedure
550
+ const savedDoc = await getDoc(procedureRef);
551
+ return savedDoc.data() as Procedure;
552
+ }
553
+
554
+ /**
555
+ * Clones an existing procedure for multiple target practitioners.
556
+ * This creates new procedure documents with the same data as the source procedure,
557
+ * but linked to each target practitioner.
558
+ *
559
+ * @param sourceProcedureId - The ID of the procedure to clone
560
+ * @param targetPractitionerIds - Array of practitioner IDs to assign the cloned procedure to
561
+ * @param overrides - Optional overrides for the new procedures (e.g. price, duration, isActive)
562
+ * @returns Array of newly created procedures
563
+ */
564
+ async bulkCloneProcedureForPractitioners(
565
+ sourceProcedureId: string,
566
+ targetPractitionerIds: string[],
567
+ overrides?: Partial<CreateProcedureData> & { isActive?: boolean }
568
+ ): Promise<Procedure[]> {
569
+ if (!targetPractitionerIds || targetPractitionerIds.length === 0) {
570
+ throw new Error('At least one target practitioner ID is required');
571
+ }
572
+
573
+ // 1. Fetch source procedure
574
+ const sourceProcedure = await this.getProcedure(sourceProcedureId);
575
+ if (!sourceProcedure) {
576
+ throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
577
+ }
578
+
579
+ // 2. Fetch all target practitioners
580
+ const practitionerPromises = targetPractitionerIds.map(id =>
581
+ getDoc(doc(this.db, PRACTITIONERS_COLLECTION, id))
582
+ );
583
+ const practitionerSnapshots = await Promise.all(practitionerPromises);
584
+
585
+ // 3. Validate all practitioners exist, can perform the procedure, and don't already have the same technology
586
+ const practitioners: Practitioner[] = [];
587
+ const sourceTechnologyId = sourceProcedure.technology?.id;
588
+
589
+ for (let i = 0; i < practitionerSnapshots.length; i++) {
590
+ const snapshot = practitionerSnapshots[i];
591
+ if (!snapshot.exists()) {
592
+ throw new Error(`Target practitioner with ID ${targetPractitionerIds[i]} not found`);
593
+ }
594
+ const practitioner = snapshot.data() as Practitioner;
595
+
596
+ if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
597
+ throw new Error(
598
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
599
+ );
600
+ }
601
+
602
+ // Check if practitioner already has a procedure with the same technology ID in this clinic branch
603
+ const existingProceduresQuery = query(
604
+ collection(this.db, PROCEDURES_COLLECTION),
605
+ where('practitionerId', '==', practitioner.id),
606
+ where('clinicBranchId', '==', sourceProcedure.clinicBranchId),
607
+ where('isActive', '==', true)
608
+ );
609
+ const existingProceduresSnapshot = await getDocs(existingProceduresQuery);
610
+ const existingProcedures = existingProceduresSnapshot.docs.map(doc => doc.data() as Procedure);
611
+
612
+ const hasSameTechnology = existingProcedures.some(
613
+ proc => proc.technology?.id === sourceTechnologyId
614
+ );
615
+ if (hasSameTechnology) {
616
+ throw new Error(
617
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${sourceProcedure.technology?.name || sourceTechnologyId}" in this clinic branch`
618
+ );
619
+ }
620
+
621
+ practitioners.push(practitioner);
622
+ }
623
+
624
+ // 4. Create procedures in batch
625
+ const batch = writeBatch(this.db);
626
+ const newProcedures: Omit<Procedure, 'createdAt' | 'updatedAt'>[] = [];
627
+
628
+ for (const practitioner of practitioners) {
629
+ const newProcedureId = this.generateId();
630
+
631
+ // Create aggregated doctor info for the new procedure
632
+ const doctorInfo = {
633
+ id: practitioner.id,
634
+ name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
635
+ description: practitioner.basicInfo.bio || '',
636
+ photo:
637
+ typeof practitioner.basicInfo.profileImageUrl === 'string'
638
+ ? practitioner.basicInfo.profileImageUrl
639
+ : '',
640
+ rating: practitioner.reviewInfo?.averageRating || 0,
641
+ services: practitioner.procedures || [],
642
+ };
643
+
644
+ // Construct the new procedure object
645
+ const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
646
+ ...sourceProcedure,
647
+ id: newProcedureId,
648
+ practitionerId: practitioner.id,
649
+ doctorInfo,
650
+
651
+ // Reset review info for the new procedure
652
+ reviewInfo: {
653
+ totalReviews: 0,
654
+ averageRating: 0,
655
+ effectivenessOfTreatment: 0,
656
+ outcomeExplanation: 0,
657
+ painManagement: 0,
658
+ followUpCare: 0,
659
+ valueForMoney: 0,
660
+ recommendationPercentage: 0,
661
+ },
662
+
663
+ // Apply any overrides if provided
664
+ ...(overrides?.price !== undefined && { price: overrides.price }),
665
+ ...(overrides?.duration !== undefined && { duration: overrides.duration }),
666
+ ...(overrides?.description !== undefined && { description: overrides.description }),
667
+
668
+ // Ensure it's active by default unless specified otherwise
669
+ isActive: overrides?.isActive !== undefined ? overrides.isActive : true,
670
+ };
671
+
672
+ newProcedures.push(newProcedure);
673
+
674
+ // Add to batch
675
+ const procedureRef = doc(this.db, PROCEDURES_COLLECTION, newProcedureId);
676
+ batch.set(procedureRef, {
677
+ ...newProcedure,
678
+ createdAt: serverTimestamp(),
679
+ updatedAt: serverTimestamp(),
680
+ });
681
+ }
682
+
683
+ // 5. Commit batch
684
+ await batch.commit();
685
+
686
+ // 6. Fetch and return the created procedures
687
+ const createdProcedures = await Promise.all(
688
+ newProcedures.map(p => this.getProcedure(p.id))
689
+ );
690
+
691
+ return createdProcedures.filter((p): p is Procedure => p !== null);
692
+ }
693
+
373
694
  /**
374
695
  * Creates multiple procedures for a list of practitioners based on common data.
375
696
  * This method is optimized for bulk creation to reduce database reads and writes.
@@ -485,7 +806,49 @@ export class ProcedureService extends BaseService {
485
806
  throw new Error(`The following practitioners were not found: ${notFoundIds.join(', ')}`);
486
807
  }
487
808
 
488
- // 5. Use a Firestore batch for atomic creation
809
+ // 5. Check for duplicates across all practitioners before creating any procedures
810
+ const duplicatePractitioners: string[] = [];
811
+ const duplicateChecks = await Promise.all(
812
+ practitionerIds.map(async (practitionerId) => {
813
+ const existingProceduresQuery = query(
814
+ collection(this.db, PROCEDURES_COLLECTION),
815
+ where('practitionerId', '==', practitionerId),
816
+ where('clinicBranchId', '==', validatedData.clinicBranchId),
817
+ where('isActive', '==', true)
818
+ );
819
+ const existingProceduresSnapshot = await getDocs(existingProceduresQuery);
820
+ const existingProcedures = existingProceduresSnapshot.docs.map(doc => doc.data() as Procedure);
821
+
822
+ const hasSameTechnology = existingProcedures.some(
823
+ proc => proc.technology?.id === validatedData.technologyId
824
+ );
825
+
826
+ return { practitionerId, hasSameTechnology };
827
+ })
828
+ );
829
+
830
+ // Collect all practitioners with duplicates
831
+ duplicateChecks.forEach(({ practitionerId, hasSameTechnology }) => {
832
+ if (hasSameTechnology) {
833
+ duplicatePractitioners.push(practitionerId);
834
+ }
835
+ });
836
+
837
+ // If any duplicates found, throw error listing all of them
838
+ if (duplicatePractitioners.length > 0) {
839
+ const duplicateNames = duplicatePractitioners
840
+ .map(id => {
841
+ const practitioner = practitionersMap.get(id)!;
842
+ return `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`;
843
+ })
844
+ .join(', ');
845
+
846
+ throw new Error(
847
+ `The following practitioner(s) already have a procedure with technology "${technology?.name || validatedData.technologyId}" in this clinic branch: ${duplicateNames}. Please remove them from the selection and try again.`
848
+ );
849
+ }
850
+
851
+ // 6. Use a Firestore batch for atomic creation
489
852
  const batch = writeBatch(this.db);
490
853
  const createdProcedureIds: string[] = [];
491
854
  const clinicInfo = {
@@ -585,10 +948,10 @@ export class ProcedureService extends BaseService {
585
948
  });
586
949
  }
587
950
 
588
- // 6. Commit the atomic batch write
951
+ // 7. Commit the atomic batch write
589
952
  await batch.commit();
590
953
 
591
- // 7. Fetch and return the newly created procedures
954
+ // 8. Fetch and return the newly created procedures
592
955
  const fetchedProcedures: Procedure[] = [];
593
956
  for (let i = 0; i < createdProcedureIds.length; i += 30) {
594
957
  const chunk = createdProcedureIds.slice(i, i + 30);