@blackcode_sa/metaestetics-api 1.14.29 → 1.14.32

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