@blackcode_sa/metaestetics-api 1.13.1 → 1.13.2

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
@@ -20137,6 +20137,240 @@ var ProcedureService = class extends BaseService {
20137
20137
  const savedDoc = await (0, import_firestore58.getDoc)(procedureRef);
20138
20138
  return savedDoc.data();
20139
20139
  }
20140
+ /**
20141
+ * Validates if a practitioner can perform a procedure based on certification requirements.
20142
+ *
20143
+ * @param procedure - The procedure to check
20144
+ * @param practitioner - The practitioner to validate
20145
+ * @returns true if practitioner can perform the procedure, false otherwise
20146
+ */
20147
+ canPractitionerPerformProcedure(procedure, practitioner) {
20148
+ if (!practitioner.certification) {
20149
+ return false;
20150
+ }
20151
+ const requiredCert = procedure.certificationRequirement;
20152
+ const practitionerCert = practitioner.certification;
20153
+ const levelOrder = [
20154
+ "aesthetician",
20155
+ "nurse_assistant",
20156
+ "nurse",
20157
+ "nurse_practitioner",
20158
+ "physician_assistant",
20159
+ "doctor",
20160
+ "specialist",
20161
+ "plastic_surgeon"
20162
+ ];
20163
+ const practitionerLevelIndex = levelOrder.indexOf(practitionerCert.level);
20164
+ const requiredLevelIndex = levelOrder.indexOf(requiredCert.minimumLevel);
20165
+ if (practitionerLevelIndex < requiredLevelIndex) {
20166
+ return false;
20167
+ }
20168
+ const requiredSpecialties = requiredCert.requiredSpecialties || [];
20169
+ if (requiredSpecialties.length > 0) {
20170
+ const practitionerSpecialties = practitionerCert.specialties || [];
20171
+ const hasAllRequired = requiredSpecialties.every(
20172
+ (specialty) => practitionerSpecialties.includes(specialty)
20173
+ );
20174
+ if (!hasAllRequired) {
20175
+ return false;
20176
+ }
20177
+ }
20178
+ return true;
20179
+ }
20180
+ /**
20181
+ * Clones an existing procedure for a target practitioner.
20182
+ * This creates a new procedure document with the same data as the source procedure,
20183
+ * but linked to the target practitioner.
20184
+ *
20185
+ * @param sourceProcedureId - The ID of the procedure to clone
20186
+ * @param targetPractitionerId - The ID of the practitioner to assign the cloned procedure to
20187
+ * @param overrides - Optional overrides for the new procedure (e.g. price, duration, isActive)
20188
+ * @returns The newly created procedure
20189
+ */
20190
+ async cloneProcedureForPractitioner(sourceProcedureId, targetPractitionerId, overrides) {
20191
+ var _a, _b, _c;
20192
+ const sourceProcedure = await this.getProcedure(sourceProcedureId);
20193
+ if (!sourceProcedure) {
20194
+ throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
20195
+ }
20196
+ const practitionerRef = (0, import_firestore58.doc)(this.db, PRACTITIONERS_COLLECTION, targetPractitionerId);
20197
+ const practitionerSnapshot = await (0, import_firestore58.getDoc)(practitionerRef);
20198
+ if (!practitionerSnapshot.exists()) {
20199
+ throw new Error(`Target practitioner with ID ${targetPractitionerId} not found`);
20200
+ }
20201
+ const practitioner = practitionerSnapshot.data();
20202
+ if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
20203
+ throw new Error(
20204
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
20205
+ );
20206
+ }
20207
+ const existingProceduresQuery = (0, import_firestore58.query)(
20208
+ (0, import_firestore58.collection)(this.db, PROCEDURES_COLLECTION),
20209
+ (0, import_firestore58.where)("practitionerId", "==", targetPractitionerId),
20210
+ (0, import_firestore58.where)("clinicBranchId", "==", sourceProcedure.clinicBranchId),
20211
+ (0, import_firestore58.where)("isActive", "==", true)
20212
+ );
20213
+ const existingProceduresSnapshot = await (0, import_firestore58.getDocs)(existingProceduresQuery);
20214
+ const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
20215
+ const hasSameTechnology = existingProcedures.some(
20216
+ (proc) => {
20217
+ var _a2, _b2;
20218
+ return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === ((_b2 = sourceProcedure.technology) == null ? void 0 : _b2.id);
20219
+ }
20220
+ );
20221
+ if (hasSameTechnology) {
20222
+ throw new Error(
20223
+ `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`
20224
+ );
20225
+ }
20226
+ const newProcedureId = this.generateId();
20227
+ const doctorInfo = {
20228
+ id: practitioner.id,
20229
+ name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
20230
+ description: practitioner.basicInfo.bio || "",
20231
+ photo: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : "",
20232
+ rating: ((_c = practitioner.reviewInfo) == null ? void 0 : _c.averageRating) || 0,
20233
+ services: practitioner.procedures || []
20234
+ };
20235
+ const newProcedure = {
20236
+ ...sourceProcedure,
20237
+ id: newProcedureId,
20238
+ practitionerId: targetPractitionerId,
20239
+ doctorInfo,
20240
+ // Link to new doctor
20241
+ // Reset review info for the new procedure
20242
+ reviewInfo: {
20243
+ totalReviews: 0,
20244
+ averageRating: 0,
20245
+ effectivenessOfTreatment: 0,
20246
+ outcomeExplanation: 0,
20247
+ painManagement: 0,
20248
+ followUpCare: 0,
20249
+ valueForMoney: 0,
20250
+ recommendationPercentage: 0
20251
+ },
20252
+ // Apply any overrides if provided
20253
+ ...(overrides == null ? void 0 : overrides.price) !== void 0 && { price: overrides.price },
20254
+ ...(overrides == null ? void 0 : overrides.duration) !== void 0 && { duration: overrides.duration },
20255
+ ...(overrides == null ? void 0 : overrides.description) !== void 0 && { description: overrides.description },
20256
+ // Ensure it's active by default unless specified otherwise
20257
+ isActive: (overrides == null ? void 0 : overrides.isActive) !== void 0 ? overrides.isActive : true
20258
+ };
20259
+ const procedureRef = (0, import_firestore58.doc)(this.db, PROCEDURES_COLLECTION, newProcedureId);
20260
+ await (0, import_firestore58.setDoc)(procedureRef, {
20261
+ ...newProcedure,
20262
+ createdAt: (0, import_firestore58.serverTimestamp)(),
20263
+ updatedAt: (0, import_firestore58.serverTimestamp)()
20264
+ });
20265
+ const savedDoc = await (0, import_firestore58.getDoc)(procedureRef);
20266
+ return savedDoc.data();
20267
+ }
20268
+ /**
20269
+ * Clones an existing procedure for multiple target practitioners.
20270
+ * This creates new procedure documents with the same data as the source procedure,
20271
+ * but linked to each target practitioner.
20272
+ *
20273
+ * @param sourceProcedureId - The ID of the procedure to clone
20274
+ * @param targetPractitionerIds - Array of practitioner IDs to assign the cloned procedure to
20275
+ * @param overrides - Optional overrides for the new procedures (e.g. price, duration, isActive)
20276
+ * @returns Array of newly created procedures
20277
+ */
20278
+ async bulkCloneProcedureForPractitioners(sourceProcedureId, targetPractitionerIds, overrides) {
20279
+ var _a, _b, _c;
20280
+ if (!targetPractitionerIds || targetPractitionerIds.length === 0) {
20281
+ throw new Error("At least one target practitioner ID is required");
20282
+ }
20283
+ const sourceProcedure = await this.getProcedure(sourceProcedureId);
20284
+ if (!sourceProcedure) {
20285
+ throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
20286
+ }
20287
+ const practitionerPromises = targetPractitionerIds.map(
20288
+ (id) => (0, import_firestore58.getDoc)((0, import_firestore58.doc)(this.db, PRACTITIONERS_COLLECTION, id))
20289
+ );
20290
+ const practitionerSnapshots = await Promise.all(practitionerPromises);
20291
+ const practitioners = [];
20292
+ const sourceTechnologyId = (_a = sourceProcedure.technology) == null ? void 0 : _a.id;
20293
+ for (let i = 0; i < practitionerSnapshots.length; i++) {
20294
+ const snapshot = practitionerSnapshots[i];
20295
+ if (!snapshot.exists()) {
20296
+ throw new Error(`Target practitioner with ID ${targetPractitionerIds[i]} not found`);
20297
+ }
20298
+ const practitioner = snapshot.data();
20299
+ if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
20300
+ throw new Error(
20301
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
20302
+ );
20303
+ }
20304
+ const existingProceduresQuery = (0, import_firestore58.query)(
20305
+ (0, import_firestore58.collection)(this.db, PROCEDURES_COLLECTION),
20306
+ (0, import_firestore58.where)("practitionerId", "==", practitioner.id),
20307
+ (0, import_firestore58.where)("clinicBranchId", "==", sourceProcedure.clinicBranchId),
20308
+ (0, import_firestore58.where)("isActive", "==", true)
20309
+ );
20310
+ const existingProceduresSnapshot = await (0, import_firestore58.getDocs)(existingProceduresQuery);
20311
+ const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
20312
+ const hasSameTechnology = existingProcedures.some(
20313
+ (proc) => {
20314
+ var _a2;
20315
+ return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === sourceTechnologyId;
20316
+ }
20317
+ );
20318
+ if (hasSameTechnology) {
20319
+ throw new Error(
20320
+ `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`
20321
+ );
20322
+ }
20323
+ practitioners.push(practitioner);
20324
+ }
20325
+ const batch = (0, import_firestore58.writeBatch)(this.db);
20326
+ const newProcedures = [];
20327
+ for (const practitioner of practitioners) {
20328
+ const newProcedureId = this.generateId();
20329
+ const doctorInfo = {
20330
+ id: practitioner.id,
20331
+ name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
20332
+ description: practitioner.basicInfo.bio || "",
20333
+ photo: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : "",
20334
+ rating: ((_c = practitioner.reviewInfo) == null ? void 0 : _c.averageRating) || 0,
20335
+ services: practitioner.procedures || []
20336
+ };
20337
+ const newProcedure = {
20338
+ ...sourceProcedure,
20339
+ id: newProcedureId,
20340
+ practitionerId: practitioner.id,
20341
+ doctorInfo,
20342
+ // Reset review info for the new procedure
20343
+ reviewInfo: {
20344
+ totalReviews: 0,
20345
+ averageRating: 0,
20346
+ effectivenessOfTreatment: 0,
20347
+ outcomeExplanation: 0,
20348
+ painManagement: 0,
20349
+ followUpCare: 0,
20350
+ valueForMoney: 0,
20351
+ recommendationPercentage: 0
20352
+ },
20353
+ // Apply any overrides if provided
20354
+ ...(overrides == null ? void 0 : overrides.price) !== void 0 && { price: overrides.price },
20355
+ ...(overrides == null ? void 0 : overrides.duration) !== void 0 && { duration: overrides.duration },
20356
+ ...(overrides == null ? void 0 : overrides.description) !== void 0 && { description: overrides.description },
20357
+ // Ensure it's active by default unless specified otherwise
20358
+ isActive: (overrides == null ? void 0 : overrides.isActive) !== void 0 ? overrides.isActive : true
20359
+ };
20360
+ newProcedures.push(newProcedure);
20361
+ const procedureRef = (0, import_firestore58.doc)(this.db, PROCEDURES_COLLECTION, newProcedureId);
20362
+ batch.set(procedureRef, {
20363
+ ...newProcedure,
20364
+ createdAt: (0, import_firestore58.serverTimestamp)(),
20365
+ updatedAt: (0, import_firestore58.serverTimestamp)()
20366
+ });
20367
+ }
20368
+ await batch.commit();
20369
+ const createdProcedures = await Promise.all(
20370
+ newProcedures.map((p) => this.getProcedure(p.id))
20371
+ );
20372
+ return createdProcedures.filter((p) => p !== null);
20373
+ }
20140
20374
  /**
20141
20375
  * Creates multiple procedures for a list of practitioners based on common data.
20142
20376
  * This method is optimized for bulk creation to reduce database reads and writes.
package/dist/index.mjs CHANGED
@@ -20373,6 +20373,240 @@ var ProcedureService = class extends BaseService {
20373
20373
  const savedDoc = await getDoc40(procedureRef);
20374
20374
  return savedDoc.data();
20375
20375
  }
20376
+ /**
20377
+ * Validates if a practitioner can perform a procedure based on certification requirements.
20378
+ *
20379
+ * @param procedure - The procedure to check
20380
+ * @param practitioner - The practitioner to validate
20381
+ * @returns true if practitioner can perform the procedure, false otherwise
20382
+ */
20383
+ canPractitionerPerformProcedure(procedure, practitioner) {
20384
+ if (!practitioner.certification) {
20385
+ return false;
20386
+ }
20387
+ const requiredCert = procedure.certificationRequirement;
20388
+ const practitionerCert = practitioner.certification;
20389
+ const levelOrder = [
20390
+ "aesthetician",
20391
+ "nurse_assistant",
20392
+ "nurse",
20393
+ "nurse_practitioner",
20394
+ "physician_assistant",
20395
+ "doctor",
20396
+ "specialist",
20397
+ "plastic_surgeon"
20398
+ ];
20399
+ const practitionerLevelIndex = levelOrder.indexOf(practitionerCert.level);
20400
+ const requiredLevelIndex = levelOrder.indexOf(requiredCert.minimumLevel);
20401
+ if (practitionerLevelIndex < requiredLevelIndex) {
20402
+ return false;
20403
+ }
20404
+ const requiredSpecialties = requiredCert.requiredSpecialties || [];
20405
+ if (requiredSpecialties.length > 0) {
20406
+ const practitionerSpecialties = practitionerCert.specialties || [];
20407
+ const hasAllRequired = requiredSpecialties.every(
20408
+ (specialty) => practitionerSpecialties.includes(specialty)
20409
+ );
20410
+ if (!hasAllRequired) {
20411
+ return false;
20412
+ }
20413
+ }
20414
+ return true;
20415
+ }
20416
+ /**
20417
+ * Clones an existing procedure for a target practitioner.
20418
+ * This creates a new procedure document with the same data as the source procedure,
20419
+ * but linked to the target practitioner.
20420
+ *
20421
+ * @param sourceProcedureId - The ID of the procedure to clone
20422
+ * @param targetPractitionerId - The ID of the practitioner to assign the cloned procedure to
20423
+ * @param overrides - Optional overrides for the new procedure (e.g. price, duration, isActive)
20424
+ * @returns The newly created procedure
20425
+ */
20426
+ async cloneProcedureForPractitioner(sourceProcedureId, targetPractitionerId, overrides) {
20427
+ var _a, _b, _c;
20428
+ const sourceProcedure = await this.getProcedure(sourceProcedureId);
20429
+ if (!sourceProcedure) {
20430
+ throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
20431
+ }
20432
+ const practitionerRef = doc39(this.db, PRACTITIONERS_COLLECTION, targetPractitionerId);
20433
+ const practitionerSnapshot = await getDoc40(practitionerRef);
20434
+ if (!practitionerSnapshot.exists()) {
20435
+ throw new Error(`Target practitioner with ID ${targetPractitionerId} not found`);
20436
+ }
20437
+ const practitioner = practitionerSnapshot.data();
20438
+ if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
20439
+ throw new Error(
20440
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
20441
+ );
20442
+ }
20443
+ const existingProceduresQuery = query33(
20444
+ collection33(this.db, PROCEDURES_COLLECTION),
20445
+ where33("practitionerId", "==", targetPractitionerId),
20446
+ where33("clinicBranchId", "==", sourceProcedure.clinicBranchId),
20447
+ where33("isActive", "==", true)
20448
+ );
20449
+ const existingProceduresSnapshot = await getDocs33(existingProceduresQuery);
20450
+ const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
20451
+ const hasSameTechnology = existingProcedures.some(
20452
+ (proc) => {
20453
+ var _a2, _b2;
20454
+ return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === ((_b2 = sourceProcedure.technology) == null ? void 0 : _b2.id);
20455
+ }
20456
+ );
20457
+ if (hasSameTechnology) {
20458
+ throw new Error(
20459
+ `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`
20460
+ );
20461
+ }
20462
+ const newProcedureId = this.generateId();
20463
+ const doctorInfo = {
20464
+ id: practitioner.id,
20465
+ name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
20466
+ description: practitioner.basicInfo.bio || "",
20467
+ photo: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : "",
20468
+ rating: ((_c = practitioner.reviewInfo) == null ? void 0 : _c.averageRating) || 0,
20469
+ services: practitioner.procedures || []
20470
+ };
20471
+ const newProcedure = {
20472
+ ...sourceProcedure,
20473
+ id: newProcedureId,
20474
+ practitionerId: targetPractitionerId,
20475
+ doctorInfo,
20476
+ // Link to new doctor
20477
+ // Reset review info for the new procedure
20478
+ reviewInfo: {
20479
+ totalReviews: 0,
20480
+ averageRating: 0,
20481
+ effectivenessOfTreatment: 0,
20482
+ outcomeExplanation: 0,
20483
+ painManagement: 0,
20484
+ followUpCare: 0,
20485
+ valueForMoney: 0,
20486
+ recommendationPercentage: 0
20487
+ },
20488
+ // Apply any overrides if provided
20489
+ ...(overrides == null ? void 0 : overrides.price) !== void 0 && { price: overrides.price },
20490
+ ...(overrides == null ? void 0 : overrides.duration) !== void 0 && { duration: overrides.duration },
20491
+ ...(overrides == null ? void 0 : overrides.description) !== void 0 && { description: overrides.description },
20492
+ // Ensure it's active by default unless specified otherwise
20493
+ isActive: (overrides == null ? void 0 : overrides.isActive) !== void 0 ? overrides.isActive : true
20494
+ };
20495
+ const procedureRef = doc39(this.db, PROCEDURES_COLLECTION, newProcedureId);
20496
+ await setDoc27(procedureRef, {
20497
+ ...newProcedure,
20498
+ createdAt: serverTimestamp31(),
20499
+ updatedAt: serverTimestamp31()
20500
+ });
20501
+ const savedDoc = await getDoc40(procedureRef);
20502
+ return savedDoc.data();
20503
+ }
20504
+ /**
20505
+ * Clones an existing procedure for multiple target practitioners.
20506
+ * This creates new procedure documents with the same data as the source procedure,
20507
+ * but linked to each target practitioner.
20508
+ *
20509
+ * @param sourceProcedureId - The ID of the procedure to clone
20510
+ * @param targetPractitionerIds - Array of practitioner IDs to assign the cloned procedure to
20511
+ * @param overrides - Optional overrides for the new procedures (e.g. price, duration, isActive)
20512
+ * @returns Array of newly created procedures
20513
+ */
20514
+ async bulkCloneProcedureForPractitioners(sourceProcedureId, targetPractitionerIds, overrides) {
20515
+ var _a, _b, _c;
20516
+ if (!targetPractitionerIds || targetPractitionerIds.length === 0) {
20517
+ throw new Error("At least one target practitioner ID is required");
20518
+ }
20519
+ const sourceProcedure = await this.getProcedure(sourceProcedureId);
20520
+ if (!sourceProcedure) {
20521
+ throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
20522
+ }
20523
+ const practitionerPromises = targetPractitionerIds.map(
20524
+ (id) => getDoc40(doc39(this.db, PRACTITIONERS_COLLECTION, id))
20525
+ );
20526
+ const practitionerSnapshots = await Promise.all(practitionerPromises);
20527
+ const practitioners = [];
20528
+ const sourceTechnologyId = (_a = sourceProcedure.technology) == null ? void 0 : _a.id;
20529
+ for (let i = 0; i < practitionerSnapshots.length; i++) {
20530
+ const snapshot = practitionerSnapshots[i];
20531
+ if (!snapshot.exists()) {
20532
+ throw new Error(`Target practitioner with ID ${targetPractitionerIds[i]} not found`);
20533
+ }
20534
+ const practitioner = snapshot.data();
20535
+ if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
20536
+ throw new Error(
20537
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
20538
+ );
20539
+ }
20540
+ const existingProceduresQuery = query33(
20541
+ collection33(this.db, PROCEDURES_COLLECTION),
20542
+ where33("practitionerId", "==", practitioner.id),
20543
+ where33("clinicBranchId", "==", sourceProcedure.clinicBranchId),
20544
+ where33("isActive", "==", true)
20545
+ );
20546
+ const existingProceduresSnapshot = await getDocs33(existingProceduresQuery);
20547
+ const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
20548
+ const hasSameTechnology = existingProcedures.some(
20549
+ (proc) => {
20550
+ var _a2;
20551
+ return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === sourceTechnologyId;
20552
+ }
20553
+ );
20554
+ if (hasSameTechnology) {
20555
+ throw new Error(
20556
+ `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`
20557
+ );
20558
+ }
20559
+ practitioners.push(practitioner);
20560
+ }
20561
+ const batch = writeBatch6(this.db);
20562
+ const newProcedures = [];
20563
+ for (const practitioner of practitioners) {
20564
+ const newProcedureId = this.generateId();
20565
+ const doctorInfo = {
20566
+ id: practitioner.id,
20567
+ name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
20568
+ description: practitioner.basicInfo.bio || "",
20569
+ photo: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : "",
20570
+ rating: ((_c = practitioner.reviewInfo) == null ? void 0 : _c.averageRating) || 0,
20571
+ services: practitioner.procedures || []
20572
+ };
20573
+ const newProcedure = {
20574
+ ...sourceProcedure,
20575
+ id: newProcedureId,
20576
+ practitionerId: practitioner.id,
20577
+ doctorInfo,
20578
+ // Reset review info for the new procedure
20579
+ reviewInfo: {
20580
+ totalReviews: 0,
20581
+ averageRating: 0,
20582
+ effectivenessOfTreatment: 0,
20583
+ outcomeExplanation: 0,
20584
+ painManagement: 0,
20585
+ followUpCare: 0,
20586
+ valueForMoney: 0,
20587
+ recommendationPercentage: 0
20588
+ },
20589
+ // Apply any overrides if provided
20590
+ ...(overrides == null ? void 0 : overrides.price) !== void 0 && { price: overrides.price },
20591
+ ...(overrides == null ? void 0 : overrides.duration) !== void 0 && { duration: overrides.duration },
20592
+ ...(overrides == null ? void 0 : overrides.description) !== void 0 && { description: overrides.description },
20593
+ // Ensure it's active by default unless specified otherwise
20594
+ isActive: (overrides == null ? void 0 : overrides.isActive) !== void 0 ? overrides.isActive : true
20595
+ };
20596
+ newProcedures.push(newProcedure);
20597
+ const procedureRef = doc39(this.db, PROCEDURES_COLLECTION, newProcedureId);
20598
+ batch.set(procedureRef, {
20599
+ ...newProcedure,
20600
+ createdAt: serverTimestamp31(),
20601
+ updatedAt: serverTimestamp31()
20602
+ });
20603
+ }
20604
+ await batch.commit();
20605
+ const createdProcedures = await Promise.all(
20606
+ newProcedures.map((p) => this.getProcedure(p.id))
20607
+ );
20608
+ return createdProcedures.filter((p) => p !== null);
20609
+ }
20376
20610
  /**
20377
20611
  * Creates multiple procedures for a list of practitioners based on common data.
20378
20612
  * This method is optimized for bulk creation to reduce database reads and writes.
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.2",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -370,6 +370,308 @@ export class ProcedureService extends BaseService {
370
370
  return savedDoc.data() as Procedure;
371
371
  }
372
372
 
373
+ /**
374
+ * Validates if a practitioner can perform a procedure based on certification requirements.
375
+ *
376
+ * @param procedure - The procedure to check
377
+ * @param practitioner - The practitioner to validate
378
+ * @returns true if practitioner can perform the procedure, false otherwise
379
+ */
380
+ canPractitionerPerformProcedure(procedure: Procedure, practitioner: Practitioner): boolean {
381
+ if (!practitioner.certification) {
382
+ return false;
383
+ }
384
+
385
+ const requiredCert = procedure.certificationRequirement;
386
+ const practitionerCert = practitioner.certification;
387
+
388
+ // Check certification level
389
+ const levelOrder = [
390
+ 'aesthetician',
391
+ 'nurse_assistant',
392
+ 'nurse',
393
+ 'nurse_practitioner',
394
+ 'physician_assistant',
395
+ 'doctor',
396
+ 'specialist',
397
+ 'plastic_surgeon',
398
+ ];
399
+
400
+ const practitionerLevelIndex = levelOrder.indexOf(practitionerCert.level);
401
+ const requiredLevelIndex = levelOrder.indexOf(requiredCert.minimumLevel);
402
+
403
+ if (practitionerLevelIndex < requiredLevelIndex) {
404
+ return false;
405
+ }
406
+
407
+ // Check required specialties
408
+ const requiredSpecialties = requiredCert.requiredSpecialties || [];
409
+ if (requiredSpecialties.length > 0) {
410
+ const practitionerSpecialties = practitionerCert.specialties || [];
411
+ const hasAllRequired = requiredSpecialties.every(specialty =>
412
+ practitionerSpecialties.includes(specialty)
413
+ );
414
+ if (!hasAllRequired) {
415
+ return false;
416
+ }
417
+ }
418
+
419
+ return true;
420
+ }
421
+
422
+ /**
423
+ * Clones an existing procedure for a target practitioner.
424
+ * This creates a new procedure document with the same data as the source procedure,
425
+ * but linked to the target practitioner.
426
+ *
427
+ * @param sourceProcedureId - The ID of the procedure to clone
428
+ * @param targetPractitionerId - The ID of the practitioner to assign the cloned procedure to
429
+ * @param overrides - Optional overrides for the new procedure (e.g. price, duration, isActive)
430
+ * @returns The newly created procedure
431
+ */
432
+ async cloneProcedureForPractitioner(
433
+ sourceProcedureId: string,
434
+ targetPractitionerId: string,
435
+ overrides?: Partial<CreateProcedureData> & { isActive?: boolean }
436
+ ): Promise<Procedure> {
437
+ // 1. Fetch source procedure
438
+ const sourceProcedure = await this.getProcedure(sourceProcedureId);
439
+ if (!sourceProcedure) {
440
+ throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
441
+ }
442
+
443
+ // 2. Fetch target practitioner
444
+ const practitionerRef = doc(this.db, PRACTITIONERS_COLLECTION, targetPractitionerId);
445
+ const practitionerSnapshot = await getDoc(practitionerRef);
446
+ if (!practitionerSnapshot.exists()) {
447
+ throw new Error(`Target practitioner with ID ${targetPractitionerId} not found`);
448
+ }
449
+ const practitioner = practitionerSnapshot.data() as Practitioner;
450
+
451
+ // 3. Validate certification
452
+ if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
453
+ throw new Error(
454
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
455
+ );
456
+ }
457
+
458
+ // 4. Check if practitioner already has a procedure with the same technology ID in this clinic branch
459
+ const existingProceduresQuery = query(
460
+ collection(this.db, PROCEDURES_COLLECTION),
461
+ where('practitionerId', '==', targetPractitionerId),
462
+ where('clinicBranchId', '==', sourceProcedure.clinicBranchId),
463
+ where('isActive', '==', true)
464
+ );
465
+ const existingProceduresSnapshot = await getDocs(existingProceduresQuery);
466
+ const existingProcedures = existingProceduresSnapshot.docs.map(doc => doc.data() as Procedure);
467
+
468
+ const hasSameTechnology = existingProcedures.some(
469
+ proc => proc.technology?.id === sourceProcedure.technology?.id
470
+ );
471
+ if (hasSameTechnology) {
472
+ throw new Error(
473
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${sourceProcedure.technology?.name || sourceProcedure.technology?.id}" in this clinic branch`
474
+ );
475
+ }
476
+
477
+ // 5. Prepare data for new procedure
478
+ const newProcedureId = this.generateId();
479
+
480
+ // Create aggregated doctor info for the new procedure
481
+ const doctorInfo = {
482
+ id: practitioner.id,
483
+ name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
484
+ description: practitioner.basicInfo.bio || '',
485
+ photo:
486
+ typeof practitioner.basicInfo.profileImageUrl === 'string'
487
+ ? practitioner.basicInfo.profileImageUrl
488
+ : '',
489
+ rating: practitioner.reviewInfo?.averageRating || 0,
490
+ services: practitioner.procedures || [],
491
+ };
492
+
493
+ // Construct the new procedure object
494
+ // We copy everything from source, but override specific fields
495
+ const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
496
+ ...sourceProcedure,
497
+ id: newProcedureId,
498
+ practitionerId: targetPractitionerId,
499
+ doctorInfo, // Link to new doctor
500
+
501
+ // Reset review info for the new procedure
502
+ reviewInfo: {
503
+ totalReviews: 0,
504
+ averageRating: 0,
505
+ effectivenessOfTreatment: 0,
506
+ outcomeExplanation: 0,
507
+ painManagement: 0,
508
+ followUpCare: 0,
509
+ valueForMoney: 0,
510
+ recommendationPercentage: 0,
511
+ },
512
+
513
+ // Apply any overrides if provided
514
+ ...(overrides?.price !== undefined && { price: overrides.price }),
515
+ ...(overrides?.duration !== undefined && { duration: overrides.duration }),
516
+ ...(overrides?.description !== undefined && { description: overrides.description }),
517
+
518
+ // Ensure it's active by default unless specified otherwise
519
+ isActive: overrides?.isActive !== undefined ? overrides.isActive : true,
520
+ };
521
+
522
+ // 6. Save to Firestore
523
+ const procedureRef = doc(this.db, PROCEDURES_COLLECTION, newProcedureId);
524
+ await setDoc(procedureRef, {
525
+ ...newProcedure,
526
+ createdAt: serverTimestamp(),
527
+ updatedAt: serverTimestamp(),
528
+ });
529
+
530
+ // 7. Return the new procedure
531
+ const savedDoc = await getDoc(procedureRef);
532
+ return savedDoc.data() as Procedure;
533
+ }
534
+
535
+ /**
536
+ * Clones an existing procedure for multiple target practitioners.
537
+ * This creates new procedure documents with the same data as the source procedure,
538
+ * but linked to each target practitioner.
539
+ *
540
+ * @param sourceProcedureId - The ID of the procedure to clone
541
+ * @param targetPractitionerIds - Array of practitioner IDs to assign the cloned procedure to
542
+ * @param overrides - Optional overrides for the new procedures (e.g. price, duration, isActive)
543
+ * @returns Array of newly created procedures
544
+ */
545
+ async bulkCloneProcedureForPractitioners(
546
+ sourceProcedureId: string,
547
+ targetPractitionerIds: string[],
548
+ overrides?: Partial<CreateProcedureData> & { isActive?: boolean }
549
+ ): Promise<Procedure[]> {
550
+ if (!targetPractitionerIds || targetPractitionerIds.length === 0) {
551
+ throw new Error('At least one target practitioner ID is required');
552
+ }
553
+
554
+ // 1. Fetch source procedure
555
+ const sourceProcedure = await this.getProcedure(sourceProcedureId);
556
+ if (!sourceProcedure) {
557
+ throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
558
+ }
559
+
560
+ // 2. Fetch all target practitioners
561
+ const practitionerPromises = targetPractitionerIds.map(id =>
562
+ getDoc(doc(this.db, PRACTITIONERS_COLLECTION, id))
563
+ );
564
+ const practitionerSnapshots = await Promise.all(practitionerPromises);
565
+
566
+ // 3. Validate all practitioners exist, can perform the procedure, and don't already have the same technology
567
+ const practitioners: Practitioner[] = [];
568
+ const sourceTechnologyId = sourceProcedure.technology?.id;
569
+
570
+ for (let i = 0; i < practitionerSnapshots.length; i++) {
571
+ const snapshot = practitionerSnapshots[i];
572
+ if (!snapshot.exists()) {
573
+ throw new Error(`Target practitioner with ID ${targetPractitionerIds[i]} not found`);
574
+ }
575
+ const practitioner = snapshot.data() as Practitioner;
576
+
577
+ if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
578
+ throw new Error(
579
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
580
+ );
581
+ }
582
+
583
+ // Check if practitioner already has a procedure with the same technology ID in this clinic branch
584
+ const existingProceduresQuery = query(
585
+ collection(this.db, PROCEDURES_COLLECTION),
586
+ where('practitionerId', '==', practitioner.id),
587
+ where('clinicBranchId', '==', sourceProcedure.clinicBranchId),
588
+ where('isActive', '==', true)
589
+ );
590
+ const existingProceduresSnapshot = await getDocs(existingProceduresQuery);
591
+ const existingProcedures = existingProceduresSnapshot.docs.map(doc => doc.data() as Procedure);
592
+
593
+ const hasSameTechnology = existingProcedures.some(
594
+ proc => proc.technology?.id === sourceTechnologyId
595
+ );
596
+ if (hasSameTechnology) {
597
+ throw new Error(
598
+ `Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${sourceProcedure.technology?.name || sourceTechnologyId}" in this clinic branch`
599
+ );
600
+ }
601
+
602
+ practitioners.push(practitioner);
603
+ }
604
+
605
+ // 4. Create procedures in batch
606
+ const batch = writeBatch(this.db);
607
+ const newProcedures: Omit<Procedure, 'createdAt' | 'updatedAt'>[] = [];
608
+
609
+ for (const practitioner of practitioners) {
610
+ const newProcedureId = this.generateId();
611
+
612
+ // Create aggregated doctor info for the new procedure
613
+ const doctorInfo = {
614
+ id: practitioner.id,
615
+ name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
616
+ description: practitioner.basicInfo.bio || '',
617
+ photo:
618
+ typeof practitioner.basicInfo.profileImageUrl === 'string'
619
+ ? practitioner.basicInfo.profileImageUrl
620
+ : '',
621
+ rating: practitioner.reviewInfo?.averageRating || 0,
622
+ services: practitioner.procedures || [],
623
+ };
624
+
625
+ // Construct the new procedure object
626
+ const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
627
+ ...sourceProcedure,
628
+ id: newProcedureId,
629
+ practitionerId: practitioner.id,
630
+ doctorInfo,
631
+
632
+ // Reset review info for the new procedure
633
+ reviewInfo: {
634
+ totalReviews: 0,
635
+ averageRating: 0,
636
+ effectivenessOfTreatment: 0,
637
+ outcomeExplanation: 0,
638
+ painManagement: 0,
639
+ followUpCare: 0,
640
+ valueForMoney: 0,
641
+ recommendationPercentage: 0,
642
+ },
643
+
644
+ // Apply any overrides if provided
645
+ ...(overrides?.price !== undefined && { price: overrides.price }),
646
+ ...(overrides?.duration !== undefined && { duration: overrides.duration }),
647
+ ...(overrides?.description !== undefined && { description: overrides.description }),
648
+
649
+ // Ensure it's active by default unless specified otherwise
650
+ isActive: overrides?.isActive !== undefined ? overrides.isActive : true,
651
+ };
652
+
653
+ newProcedures.push(newProcedure);
654
+
655
+ // Add to batch
656
+ const procedureRef = doc(this.db, PROCEDURES_COLLECTION, newProcedureId);
657
+ batch.set(procedureRef, {
658
+ ...newProcedure,
659
+ createdAt: serverTimestamp(),
660
+ updatedAt: serverTimestamp(),
661
+ });
662
+ }
663
+
664
+ // 5. Commit batch
665
+ await batch.commit();
666
+
667
+ // 6. Fetch and return the created procedures
668
+ const createdProcedures = await Promise.all(
669
+ newProcedures.map(p => this.getProcedure(p.id))
670
+ );
671
+
672
+ return createdProcedures.filter((p): p is Procedure => p !== null);
673
+ }
674
+
373
675
  /**
374
676
  * Creates multiple procedures for a list of practitioners based on common data.
375
677
  * This method is optimized for bulk creation to reduce database reads and writes.