@blackcode_sa/metaestetics-api 1.14.28 → 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";
@@ -11188,17 +11200,27 @@ var BookingAdmin = class {
11188
11200
  startTime: start.toDate().toISOString(),
11189
11201
  endTime: end.toDate().toISOString()
11190
11202
  });
11191
- const MAX_EVENT_DURATION_MS = 24 * 60 * 60 * 1e3;
11203
+ const MAX_EVENT_DURATION_MS = 30 * 24 * 60 * 60 * 1e3;
11192
11204
  const queryStart = admin16.firestore.Timestamp.fromMillis(
11193
11205
  start.toMillis() - MAX_EVENT_DURATION_MS
11194
11206
  );
11195
- const eventsRef = this.db.collection(`clinics/${clinicId}/calendar`).where("eventTime.start", ">=", queryStart).where("eventTime.start", "<", end).orderBy("eventTime.start");
11207
+ const eventsRef = this.db.collection(`clinics/${clinicId}/calendar`).where("eventTime.start", ">=", queryStart).where("eventTime.start", "<=", end).orderBy("eventTime.start");
11196
11208
  const snapshot = await eventsRef.get();
11197
11209
  const events = snapshot.docs.map((doc3) => ({
11198
11210
  ...doc3.data(),
11199
11211
  id: doc3.id
11200
11212
  })).filter((event) => {
11201
- return event.eventTime.end.toMillis() > start.toMillis();
11213
+ const overlaps = event.eventTime.end.toMillis() > start.toMillis();
11214
+ if (!overlaps) {
11215
+ Logger.debug("[BookingAdmin] Filtered out non-overlapping event", {
11216
+ eventId: event.id,
11217
+ eventStart: event.eventTime.start.toDate().toISOString(),
11218
+ eventEnd: event.eventTime.end.toDate().toISOString(),
11219
+ queryStart: start.toDate().toISOString(),
11220
+ queryEnd: end.toDate().toISOString()
11221
+ });
11222
+ }
11223
+ return overlaps;
11202
11224
  });
11203
11225
  Logger.debug("[BookingAdmin] Retrieved clinic calendar events", {
11204
11226
  clinicId,
@@ -11234,17 +11256,27 @@ var BookingAdmin = class {
11234
11256
  startTime: start.toDate().toISOString(),
11235
11257
  endTime: end.toDate().toISOString()
11236
11258
  });
11237
- const MAX_EVENT_DURATION_MS = 24 * 60 * 60 * 1e3;
11259
+ const MAX_EVENT_DURATION_MS = 30 * 24 * 60 * 60 * 1e3;
11238
11260
  const queryStart = admin16.firestore.Timestamp.fromMillis(
11239
11261
  start.toMillis() - MAX_EVENT_DURATION_MS
11240
11262
  );
11241
- const eventsRef = this.db.collection(`practitioners/${practitionerId}/calendar`).where("eventTime.start", ">=", queryStart).where("eventTime.start", "<", end).orderBy("eventTime.start");
11263
+ const eventsRef = this.db.collection(`practitioners/${practitionerId}/calendar`).where("eventTime.start", ">=", queryStart).where("eventTime.start", "<=", end).orderBy("eventTime.start");
11242
11264
  const snapshot = await eventsRef.get();
11243
11265
  const events = snapshot.docs.map((doc3) => ({
11244
11266
  ...doc3.data(),
11245
11267
  id: doc3.id
11246
11268
  })).filter((event) => {
11247
- return event.eventTime.end.toMillis() > start.toMillis();
11269
+ const overlaps = event.eventTime.end.toMillis() > start.toMillis();
11270
+ if (!overlaps) {
11271
+ Logger.debug("[BookingAdmin] Filtered out non-overlapping practitioner event", {
11272
+ eventId: event.id,
11273
+ eventStart: event.eventTime.start.toDate().toISOString(),
11274
+ eventEnd: event.eventTime.end.toDate().toISOString(),
11275
+ queryStart: start.toDate().toISOString(),
11276
+ queryEnd: end.toDate().toISOString()
11277
+ });
11278
+ }
11279
+ return overlaps;
11248
11280
  });
11249
11281
  Logger.debug("[BookingAdmin] Retrieved practitioner calendar events", {
11250
11282
  practitionerId,
@@ -13126,6 +13158,378 @@ var ExistingPractitionerInviteMailingService = class extends BaseMailingService
13126
13158
  }
13127
13159
  };
13128
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
+
13129
13533
  // src/admin/users/user-profile.admin.ts
13130
13534
  var admin18 = __toESM(require("firebase-admin"));
13131
13535
  var UserProfileAdminService = class {
@@ -13432,6 +13836,7 @@ TimestampUtils.enableServerMode();
13432
13836
  DocumentManagerAdminService,
13433
13837
  ExistingPractitionerInviteMailingService,
13434
13838
  FilledFormsAggregationService,
13839
+ INVITE_TOKENS_COLLECTION,
13435
13840
  Logger,
13436
13841
  NOTIFICATIONS_COLLECTION,
13437
13842
  NO_SHOW_ANALYTICS_SUBCOLLECTION,
@@ -13445,8 +13850,10 @@ TimestampUtils.enableServerMode();
13445
13850
  PROCEDURE_ANALYTICS_SUBCOLLECTION,
13446
13851
  PatientAggregationService,
13447
13852
  PatientInstructionStatus,
13853
+ PatientInviteMailingService,
13448
13854
  PatientRequirementOverallStatus,
13449
13855
  PatientRequirementsAdminService,
13856
+ PatientTokenStatus,
13450
13857
  PractitionerAggregationService,
13451
13858
  PractitionerInviteAggregationService,
13452
13859
  PractitionerInviteMailingService,