@blackcode_sa/metaestetics-api 1.14.29 → 1.14.34

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.
@@ -236,6 +236,7 @@ __export(index_exports, {
236
236
  DocumentManagerAdminService: () => DocumentManagerAdminService,
237
237
  ExistingPractitionerInviteMailingService: () => ExistingPractitionerInviteMailingService,
238
238
  FilledFormsAggregationService: () => FilledFormsAggregationService,
239
+ INVITE_TOKENS_COLLECTION: () => INVITE_TOKENS_COLLECTION,
239
240
  Logger: () => Logger,
240
241
  NOTIFICATIONS_COLLECTION: () => NOTIFICATIONS_COLLECTION,
241
242
  NO_SHOW_ANALYTICS_SUBCOLLECTION: () => NO_SHOW_ANALYTICS_SUBCOLLECTION,
@@ -249,8 +250,10 @@ __export(index_exports, {
249
250
  PROCEDURE_ANALYTICS_SUBCOLLECTION: () => PROCEDURE_ANALYTICS_SUBCOLLECTION,
250
251
  PatientAggregationService: () => PatientAggregationService,
251
252
  PatientInstructionStatus: () => PatientInstructionStatus,
253
+ PatientInviteMailingService: () => PatientInviteMailingService,
252
254
  PatientRequirementOverallStatus: () => PatientRequirementOverallStatus,
253
255
  PatientRequirementsAdminService: () => PatientRequirementsAdminService,
256
+ PatientTokenStatus: () => PatientTokenStatus,
254
257
  PractitionerAggregationService: () => PractitionerAggregationService,
255
258
  PractitionerInviteAggregationService: () => PractitionerInviteAggregationService,
256
259
  PractitionerInviteMailingService: () => PractitionerInviteMailingService,
@@ -455,6 +458,15 @@ var PATIENT_MEDICAL_INFO_COLLECTION = "medical_info";
455
458
  var PATIENTS_COLLECTION = "patients";
456
459
  var PATIENT_SENSITIVE_INFO_COLLECTION = "sensitive-info";
457
460
 
461
+ // src/types/patient/token.types.ts
462
+ var INVITE_TOKENS_COLLECTION = "inviteTokens";
463
+ var PatientTokenStatus = /* @__PURE__ */ ((PatientTokenStatus2) => {
464
+ PatientTokenStatus2["ACTIVE"] = "active";
465
+ PatientTokenStatus2["USED"] = "used";
466
+ PatientTokenStatus2["EXPIRED"] = "expired";
467
+ return PatientTokenStatus2;
468
+ })(PatientTokenStatus || {});
469
+
458
470
  // src/types/clinic/practitioner-invite.types.ts
459
471
  var PractitionerInviteStatus = /* @__PURE__ */ ((PractitionerInviteStatus2) => {
460
472
  PractitionerInviteStatus2["PENDING"] = "pending";
@@ -9099,7 +9111,8 @@ var AnalyticsService = class extends BaseService {
9099
9111
  return metrics;
9100
9112
  }
9101
9113
  }
9102
- const appointments = await this.fetchAppointments(void 0, dateRange);
9114
+ const filters = (options == null ? void 0 : options.clinicBranchId) ? { clinicBranchId: options.clinicBranchId } : void 0;
9115
+ const appointments = await this.fetchAppointments(filters, dateRange);
9103
9116
  const canceled = getCanceledAppointments(appointments);
9104
9117
  if (groupBy === "clinic") {
9105
9118
  return this.groupCancellationsByClinic(canceled, appointments);
@@ -9328,7 +9341,8 @@ var AnalyticsService = class extends BaseService {
9328
9341
  return metrics;
9329
9342
  }
9330
9343
  }
9331
- const appointments = await this.fetchAppointments(void 0, dateRange);
9344
+ const filters = (options == null ? void 0 : options.clinicBranchId) ? { clinicBranchId: options.clinicBranchId } : void 0;
9345
+ const appointments = await this.fetchAppointments(filters, dateRange);
9332
9346
  const noShow = getNoShowAppointments(appointments);
9333
9347
  if (groupBy === "clinic") {
9334
9348
  return this.groupNoShowsByClinic(noShow, appointments);
@@ -13146,6 +13160,363 @@ var ExistingPractitionerInviteMailingService = class extends BaseMailingService
13146
13160
  }
13147
13161
  };
13148
13162
 
13163
+ // src/admin/mailing/patientInvite/templates/invitation.template.ts
13164
+ var patientInvitationTemplate = `
13165
+ <!DOCTYPE html>
13166
+ <html>
13167
+ <head>
13168
+ <meta charset="UTF-8">
13169
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
13170
+ <title>Claim Your Patient Profile at {{clinicName}}</title>
13171
+ <style>
13172
+ body {
13173
+ font-family: Arial, sans-serif;
13174
+ line-height: 1.6;
13175
+ color: #333;
13176
+ margin: 0;
13177
+ padding: 0;
13178
+ }
13179
+ .container {
13180
+ max-width: 600px;
13181
+ margin: 0 auto;
13182
+ padding: 20px;
13183
+ }
13184
+ .header {
13185
+ background-color: #4A90E2;
13186
+ padding: 20px;
13187
+ text-align: center;
13188
+ color: white;
13189
+ }
13190
+ .content {
13191
+ padding: 20px;
13192
+ background-color: #f9f9f9;
13193
+ }
13194
+ .footer {
13195
+ padding: 20px;
13196
+ text-align: center;
13197
+ font-size: 12px;
13198
+ color: #888;
13199
+ }
13200
+ .token {
13201
+ font-size: 24px;
13202
+ font-weight: bold;
13203
+ color: #4A90E2;
13204
+ padding: 10px;
13205
+ background-color: #e9f0f9;
13206
+ border-radius: 4px;
13207
+ display: inline-block;
13208
+ letter-spacing: 2px;
13209
+ margin: 10px 0;
13210
+ }
13211
+ .info-box {
13212
+ background-color: #fff;
13213
+ border-left: 4px solid #4A90E2;
13214
+ padding: 15px;
13215
+ margin: 20px 0;
13216
+ }
13217
+ </style>
13218
+ </head>
13219
+ <body>
13220
+ <div class="container">
13221
+ <div class="header">
13222
+ <h1>Welcome to MetaEsthetics</h1>
13223
+ </div>
13224
+ <div class="content">
13225
+ <p>Hello {{patientName}},</p>
13226
+
13227
+ <p>A patient profile has been created for you at <strong>{{clinicName}}</strong>. You can now claim this profile to access your personal health dashboard and appointment history.</p>
13228
+
13229
+ <div class="info-box">
13230
+ <p><strong>What you'll get access to:</strong></p>
13231
+ <ul>
13232
+ <li>Your complete treatment history</li>
13233
+ <li>Upcoming appointment details</li>
13234
+ <li>Pre and post-treatment instructions</li>
13235
+ <li>Direct messaging with your clinic</li>
13236
+ </ul>
13237
+ </div>
13238
+
13239
+ <p>To claim your profile, use this registration token:</p>
13240
+
13241
+ <div style="text-align: center;">
13242
+ <span class="token">{{inviteToken}}</span>
13243
+ </div>
13244
+
13245
+ <p><strong>Important:</strong> This token will expire on <strong>{{expirationDate}}</strong>.</p>
13246
+
13247
+ <p>To create your account:</p>
13248
+ <ol>
13249
+ <li>Download the <strong>MetaEsthetics</strong> app from the App Store (iOS) or Google Play Store (Android)</li>
13250
+ <li>Open the app and create an account using your email address</li>
13251
+ <li>When prompted, enter the token shown above</li>
13252
+ <li>Your profile will be automatically linked to your new account</li>
13253
+ </ol>
13254
+
13255
+ <p>If you have any questions or didn't expect this email, please contact {{contactName}} at {{contactEmail}}.</p>
13256
+ </div>
13257
+ <div class="footer">
13258
+ <p>This is an automated message from {{clinicName}}. Please do not reply to this email.</p>
13259
+ <p>&copy; {{currentYear}} MetaEsthetics. All rights reserved.</p>
13260
+ </div>
13261
+ </div>
13262
+ </body>
13263
+ </html>
13264
+ `;
13265
+
13266
+ // src/admin/mailing/patientInvite/patientInvite.mailing.ts
13267
+ var PatientInviteMailingService = class extends BaseMailingService {
13268
+ /**
13269
+ * Constructor for PatientInviteMailingService
13270
+ * @param firestore - Firestore instance provided by the caller
13271
+ * @param mailgunClient - Mailgun client instance (mailgun.js v10+) provided by the caller
13272
+ */
13273
+ constructor(firestore19, mailgunClient) {
13274
+ super(firestore19, mailgunClient);
13275
+ this.DEFAULT_REGISTRATION_URL = "https://metaesthetics.net/patient/register";
13276
+ this.DEFAULT_SUBJECT = "Claim Your Patient Profile - MetaEsthetics";
13277
+ this.DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
13278
+ }
13279
+ /**
13280
+ * Sends a patient invitation email
13281
+ * @param data - The patient invitation data
13282
+ * @returns Promise resolved when email is sent
13283
+ */
13284
+ async sendInvitationEmail(data) {
13285
+ var _a, _b, _c, _d, _e, _f;
13286
+ try {
13287
+ Logger.info(
13288
+ "[PatientInviteMailingService] Sending invitation email to",
13289
+ data.token.email
13290
+ );
13291
+ const expirationDate = data.token.expiresAt.toDate().toLocaleDateString("en-US", {
13292
+ weekday: "long",
13293
+ year: "numeric",
13294
+ month: "long",
13295
+ day: "numeric"
13296
+ });
13297
+ const registrationUrl = ((_a = data.options) == null ? void 0 : _a.registrationUrl) || this.DEFAULT_REGISTRATION_URL;
13298
+ const contactName = data.clinic.contactName || "Clinic Administrator";
13299
+ const contactEmail = data.clinic.contactEmail;
13300
+ const subject = ((_b = data.options) == null ? void 0 : _b.customSubject) || this.DEFAULT_SUBJECT;
13301
+ const fromAddress = ((_c = data.options) == null ? void 0 : _c.fromAddress) || `MetaEsthetics <no-reply@${((_d = data.options) == null ? void 0 : _d.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN}>`;
13302
+ const currentYear = (/* @__PURE__ */ new Date()).getFullYear().toString();
13303
+ const patientName = `${data.patient.firstName} ${data.patient.lastName}`;
13304
+ const templateVariables = {
13305
+ clinicName: data.clinic.name,
13306
+ patientName,
13307
+ inviteToken: data.token.token,
13308
+ expirationDate,
13309
+ registrationUrl,
13310
+ contactName,
13311
+ contactEmail,
13312
+ currentYear
13313
+ };
13314
+ Logger.info("[PatientInviteMailingService] Template variables:", {
13315
+ clinicName: templateVariables.clinicName,
13316
+ patientName: templateVariables.patientName,
13317
+ expirationDate: templateVariables.expirationDate,
13318
+ registrationUrl: templateVariables.registrationUrl,
13319
+ contactName: templateVariables.contactName,
13320
+ contactEmail: templateVariables.contactEmail,
13321
+ hasInviteToken: !!templateVariables.inviteToken
13322
+ });
13323
+ const html = this.renderTemplate(
13324
+ patientInvitationTemplate,
13325
+ templateVariables
13326
+ );
13327
+ const mailgunSendData = {
13328
+ to: data.token.email,
13329
+ from: fromAddress,
13330
+ subject,
13331
+ html
13332
+ };
13333
+ const domainToSendFrom = ((_e = data.options) == null ? void 0 : _e.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
13334
+ Logger.info("[PatientInviteMailingService] Sending email with data:", {
13335
+ domain: domainToSendFrom,
13336
+ to: mailgunSendData.to,
13337
+ from: mailgunSendData.from,
13338
+ subject: mailgunSendData.subject,
13339
+ hasHtml: !!mailgunSendData.html
13340
+ });
13341
+ const result = await this.sendEmail(domainToSendFrom, mailgunSendData);
13342
+ await this.logEmailAttempt(
13343
+ {
13344
+ to: data.token.email,
13345
+ subject,
13346
+ templateName: "patient_invitation"
13347
+ },
13348
+ true
13349
+ );
13350
+ return result;
13351
+ } catch (error) {
13352
+ Logger.error(
13353
+ "[PatientInviteMailingService] Error sending invitation email:",
13354
+ {
13355
+ errorMessage: error.message,
13356
+ errorDetails: error.details,
13357
+ errorStatus: error.status,
13358
+ stack: error.stack
13359
+ }
13360
+ );
13361
+ await this.logEmailAttempt(
13362
+ {
13363
+ to: data.token.email,
13364
+ subject: ((_f = data.options) == null ? void 0 : _f.customSubject) || this.DEFAULT_SUBJECT,
13365
+ templateName: "patient_invitation"
13366
+ },
13367
+ false,
13368
+ error
13369
+ );
13370
+ throw error;
13371
+ }
13372
+ }
13373
+ /**
13374
+ * Handles the patient token creation event from Cloud Functions.
13375
+ * Fetches necessary data and sends the invitation email.
13376
+ * @param tokenData - The fully typed token object including its id
13377
+ * @param mailgunConfig - Mailgun configuration (from, domain, optional registrationUrl)
13378
+ * @returns Promise resolved when the email is sent
13379
+ */
13380
+ async handleTokenCreationEvent(tokenData, mailgunConfig) {
13381
+ try {
13382
+ Logger.info(
13383
+ "[PatientInviteMailingService] Handling token creation event for token:",
13384
+ tokenData.id
13385
+ );
13386
+ if (!tokenData || !tokenData.id || !tokenData.token || !tokenData.email) {
13387
+ throw new Error(
13388
+ `Invalid token data: Missing required properties. Token ID: ${tokenData == null ? void 0 : tokenData.id}`
13389
+ );
13390
+ }
13391
+ if (!tokenData.patientId) {
13392
+ throw new Error(
13393
+ `Token ${tokenData.id} is missing patientId reference`
13394
+ );
13395
+ }
13396
+ if (!tokenData.clinicId) {
13397
+ throw new Error(`Token ${tokenData.id} is missing clinicId reference`);
13398
+ }
13399
+ if (!tokenData.expiresAt) {
13400
+ throw new Error(`Token ${tokenData.id} is missing expiration date`);
13401
+ }
13402
+ if (tokenData.status !== "active" /* ACTIVE */ && String(tokenData.status).toLowerCase() !== "active") {
13403
+ Logger.warn(
13404
+ "[PatientInviteMailingService] Token is not active, skipping email.",
13405
+ { tokenId: tokenData.id, status: tokenData.status }
13406
+ );
13407
+ return;
13408
+ }
13409
+ Logger.info(
13410
+ `[PatientInviteMailingService] Token status validation:`,
13411
+ {
13412
+ tokenId: tokenData.id,
13413
+ status: tokenData.status,
13414
+ statusType: typeof tokenData.status
13415
+ }
13416
+ );
13417
+ Logger.info(
13418
+ `[PatientInviteMailingService] Fetching patient data: ${tokenData.patientId}`
13419
+ );
13420
+ const patientRef = this.db.collection(PATIENTS_COLLECTION).doc(tokenData.patientId);
13421
+ const patientDoc = await patientRef.get();
13422
+ if (!patientDoc.exists) {
13423
+ throw new Error(`Patient ${tokenData.patientId} not found`);
13424
+ }
13425
+ const patientData = patientDoc.data();
13426
+ if (!patientData) {
13427
+ throw new Error(
13428
+ `Patient ${tokenData.patientId} has invalid data structure`
13429
+ );
13430
+ }
13431
+ const sensitiveInfoRef = patientRef.collection(PATIENT_SENSITIVE_INFO_COLLECTION).doc(tokenData.patientId);
13432
+ const sensitiveInfoDoc = await sensitiveInfoRef.get();
13433
+ let firstName = "Patient";
13434
+ let lastName = "";
13435
+ if (sensitiveInfoDoc.exists) {
13436
+ const sensitiveInfo = sensitiveInfoDoc.data();
13437
+ firstName = (sensitiveInfo == null ? void 0 : sensitiveInfo.firstName) || "Patient";
13438
+ lastName = (sensitiveInfo == null ? void 0 : sensitiveInfo.lastName) || "";
13439
+ } else {
13440
+ Logger.warn(
13441
+ `[PatientInviteMailingService] No sensitive info found for patient ${tokenData.patientId}, using displayName`
13442
+ );
13443
+ const displayNameParts = (patientData.displayName || "Patient").split(" ");
13444
+ firstName = displayNameParts[0] || "Patient";
13445
+ lastName = displayNameParts.slice(1).join(" ") || "";
13446
+ }
13447
+ Logger.info(
13448
+ `[PatientInviteMailingService] Patient found: ${firstName} ${lastName}`
13449
+ );
13450
+ Logger.info(
13451
+ `[PatientInviteMailingService] Fetching clinic data: ${tokenData.clinicId}`
13452
+ );
13453
+ const clinicRef = this.db.collection(CLINICS_COLLECTION).doc(tokenData.clinicId);
13454
+ const clinicDoc = await clinicRef.get();
13455
+ if (!clinicDoc.exists) {
13456
+ throw new Error(`Clinic ${tokenData.clinicId} not found`);
13457
+ }
13458
+ const clinicData = clinicDoc.data();
13459
+ if (!clinicData || !clinicData.contactInfo) {
13460
+ throw new Error(
13461
+ `Clinic ${tokenData.clinicId} has invalid data structure`
13462
+ );
13463
+ }
13464
+ Logger.info(
13465
+ `[PatientInviteMailingService] Clinic found: ${clinicData.name}`
13466
+ );
13467
+ if (!mailgunConfig.fromAddress) {
13468
+ Logger.warn(
13469
+ "[PatientInviteMailingService] No fromAddress provided, using default"
13470
+ );
13471
+ mailgunConfig.fromAddress = `MetaEsthetics <no-reply@${this.DEFAULT_MAILGUN_DOMAIN}>`;
13472
+ }
13473
+ const emailData = {
13474
+ token: {
13475
+ id: tokenData.id,
13476
+ token: tokenData.token,
13477
+ patientId: tokenData.patientId,
13478
+ email: tokenData.email,
13479
+ clinicId: tokenData.clinicId,
13480
+ expiresAt: tokenData.expiresAt
13481
+ },
13482
+ patient: {
13483
+ firstName,
13484
+ lastName
13485
+ },
13486
+ clinic: {
13487
+ name: clinicData.name || "Medical Clinic",
13488
+ contactEmail: clinicData.contactInfo.email || "contact@clinic.com",
13489
+ contactName: "Clinic Admin"
13490
+ },
13491
+ options: {
13492
+ fromAddress: mailgunConfig.fromAddress,
13493
+ mailgunDomain: mailgunConfig.domain,
13494
+ registrationUrl: mailgunConfig.registrationUrl
13495
+ }
13496
+ };
13497
+ Logger.info(
13498
+ "[PatientInviteMailingService] Email data prepared, sending invitation"
13499
+ );
13500
+ await this.sendInvitationEmail(emailData);
13501
+ Logger.info(
13502
+ "[PatientInviteMailingService] Invitation email sent successfully"
13503
+ );
13504
+ } catch (error) {
13505
+ Logger.error(
13506
+ "[PatientInviteMailingService] Error handling token creation event:",
13507
+ {
13508
+ errorMessage: error.message,
13509
+ errorDetails: error.details,
13510
+ errorStatus: error.status,
13511
+ stack: error.stack,
13512
+ tokenId: tokenData == null ? void 0 : tokenData.id
13513
+ }
13514
+ );
13515
+ throw error;
13516
+ }
13517
+ }
13518
+ };
13519
+
13149
13520
  // src/admin/users/user-profile.admin.ts
13150
13521
  var admin18 = __toESM(require("firebase-admin"));
13151
13522
  var UserProfileAdminService = class {
@@ -13452,6 +13823,7 @@ TimestampUtils.enableServerMode();
13452
13823
  DocumentManagerAdminService,
13453
13824
  ExistingPractitionerInviteMailingService,
13454
13825
  FilledFormsAggregationService,
13826
+ INVITE_TOKENS_COLLECTION,
13455
13827
  Logger,
13456
13828
  NOTIFICATIONS_COLLECTION,
13457
13829
  NO_SHOW_ANALYTICS_SUBCOLLECTION,
@@ -13465,8 +13837,10 @@ TimestampUtils.enableServerMode();
13465
13837
  PROCEDURE_ANALYTICS_SUBCOLLECTION,
13466
13838
  PatientAggregationService,
13467
13839
  PatientInstructionStatus,
13840
+ PatientInviteMailingService,
13468
13841
  PatientRequirementOverallStatus,
13469
13842
  PatientRequirementsAdminService,
13843
+ PatientTokenStatus,
13470
13844
  PractitionerAggregationService,
13471
13845
  PractitionerInviteAggregationService,
13472
13846
  PractitionerInviteMailingService,