@blackcode_sa/metaestetics-api 1.5.44 → 1.5.46

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.5.44",
4
+ "version": "1.5.46",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.mjs",
@@ -100,6 +100,9 @@
100
100
  "geofire-common": "^6.0.0",
101
101
  "zod": "^3.24.1"
102
102
  },
103
+ "optionalDependencies": {
104
+ "firebase-functions": "^6.2.0"
105
+ },
103
106
  "jest": {
104
107
  "preset": "ts-jest",
105
108
  "testEnvironment": "node",
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Cloud Functions-compatible logger with fallback for other environments
3
+ *
4
+ * This logger automatically detects if it's running in a Cloud Functions environment
5
+ * and uses the appropriate logging method. It falls back to console methods in other environments.
6
+ */
7
+
8
+ // Try to import Firebase Functions logger, but don't cause errors if it's not available
9
+ let firebaseFunctionsLogger: any;
10
+ try {
11
+ // Use dynamic import to avoid requiring firebase-functions in non-Functions environments
12
+ firebaseFunctionsLogger = require("firebase-functions/logger");
13
+ // Import the compatibility module for console.log support if available
14
+ require("firebase-functions/logger/compat");
15
+ } catch (e) {
16
+ // Firebase Functions logger not available, will use fallback
17
+ }
18
+
19
+ /**
20
+ * Logger class that uses Firebase Functions logger when available
21
+ * with fallback to console methods when not in a Cloud Functions environment
22
+ */
23
+ export class Logger {
24
+ /**
25
+ * Log an error message
26
+ * @param message Message to log
27
+ * @param data Optional data to include
28
+ */
29
+ static error(message: string, data?: any): void {
30
+ if (firebaseFunctionsLogger) {
31
+ firebaseFunctionsLogger.error(message, data);
32
+ } else {
33
+ console.error(message, data !== undefined ? data : "");
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Log a warning message
39
+ * @param message Message to log
40
+ * @param data Optional data to include
41
+ */
42
+ static warn(message: string, data?: any): void {
43
+ if (firebaseFunctionsLogger) {
44
+ firebaseFunctionsLogger.warn(message, data);
45
+ } else {
46
+ console.warn(message, data !== undefined ? data : "");
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Log an info message
52
+ * @param message Message to log
53
+ * @param data Optional data to include
54
+ */
55
+ static info(message: string, data?: any): void {
56
+ if (firebaseFunctionsLogger) {
57
+ firebaseFunctionsLogger.info(message, data);
58
+ } else {
59
+ console.info(message, data !== undefined ? data : "");
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Log a debug message
65
+ * @param message Message to log
66
+ * @param data Optional data to include
67
+ */
68
+ static debug(message: string, data?: any): void {
69
+ if (firebaseFunctionsLogger) {
70
+ firebaseFunctionsLogger.debug(message, data);
71
+ } else {
72
+ console.debug(message, data !== undefined ? data : "");
73
+ }
74
+ }
75
+ }
76
+
77
+ // Default export for easier importing
78
+ export default Logger;
@@ -1,11 +1,6 @@
1
1
  import * as mailgun from "mailgun-js";
2
2
  import * as admin from "firebase-admin";
3
- // Configuration is no longer read here
4
- // import {
5
- // getMailgunConfig,
6
- // createMailgunClient,
7
- // MailgunConfig,
8
- // } from "./mailgun.config";
3
+ import { Logger } from "../logger";
9
4
 
10
5
  /**
11
6
  * Base mailing service class that provides common functionality for all mailing services
@@ -14,7 +9,11 @@ export class BaseMailingService {
14
9
  protected db: FirebaseFirestore.Firestore;
15
10
  protected mailgunClient: mailgun.Mailgun;
16
11
  // Removed config property as it's no longer managed here
17
- // protected config: MailgunConfig;
12
+ // import {
13
+ // getMailgunConfig,
14
+ // createMailgunClient,
15
+ // MailgunConfig,
16
+ // } from "./mailgun.config";
18
17
 
19
18
  /**
20
19
  * Constructor for BaseMailingService
@@ -29,9 +28,20 @@ export class BaseMailingService {
29
28
  // Use provided instances
30
29
  this.db = firestore;
31
30
  this.mailgunClient = mailgunClient;
32
- // Removed internal config reading and client creation
33
- // this.config = mailgunConfig || getMailgunConfig();
34
- // this.mailgunClient = createMailgunClient(this.config);
31
+
32
+ // Validate instances
33
+ if (!this.db) {
34
+ Logger.error("[BaseMailingService] No Firestore instance provided");
35
+ throw new Error("Firestore instance is required");
36
+ }
37
+
38
+ if (!this.mailgunClient) {
39
+ Logger.error("[BaseMailingService] No Mailgun client provided");
40
+ throw new Error("Mailgun client is required");
41
+ }
42
+
43
+ // Log successful initialization
44
+ Logger.info("[BaseMailingService] Service initialized successfully");
35
45
  }
36
46
 
37
47
  /**
@@ -43,37 +53,81 @@ export class BaseMailingService {
43
53
  data: mailgun.messages.SendData // Caller must provide 'from'
44
54
  ): Promise<mailgun.messages.SendResponse> {
45
55
  try {
56
+ // Validate email data fields
57
+ if (!data) {
58
+ throw new Error("Email data object is required");
59
+ }
60
+
61
+ // Ensure all required fields are provided
62
+ if (!data.to) {
63
+ throw new Error("Email 'to' address is required");
64
+ }
65
+
46
66
  // Ensure 'from' field is provided by the caller
47
67
  if (!data.from) {
48
68
  throw new Error(
49
69
  "Email 'from' address must be provided in sendEmail data."
50
70
  );
51
71
  }
52
- // Removed fallback to internal config.from
53
- // const emailData = {
54
- // ...data,
55
- // from: data.from || this.config.from,
56
- // };
72
+
73
+ if (!data.subject) {
74
+ throw new Error("Email 'subject' is required");
75
+ }
76
+
77
+ if (!data.html && !data.text) {
78
+ throw new Error("Email must have either 'html' or 'text' content");
79
+ }
80
+
81
+ Logger.info("[BaseMailingService] Sending email via Mailgun", {
82
+ to: data.to,
83
+ from: data.from,
84
+ subject: data.subject,
85
+ hasHtml: !!data.html,
86
+ hasText: !!data.text,
87
+ });
57
88
 
58
89
  // Send the email
59
90
  return await new Promise<mailgun.messages.SendResponse>(
60
91
  (resolve, reject) => {
61
- this.mailgunClient.messages().send(data, (error, body) => {
62
- if (error) {
63
- console.error("[BaseMailingService] Error sending email:", error);
64
- reject(error);
65
- } else {
66
- console.log(
67
- "[BaseMailingService] Email sent successfully:",
68
- body
69
- );
70
- resolve(body);
92
+ try {
93
+ const messagesApi = this.mailgunClient.messages();
94
+ if (!messagesApi) {
95
+ throw new Error("Could not get Mailgun messages API");
71
96
  }
72
- });
97
+
98
+ messagesApi.send(data, (error, body) => {
99
+ if (error) {
100
+ Logger.error("[BaseMailingService] Mailgun API error:", {
101
+ error: error instanceof Error ? error.message : error,
102
+ stack: error instanceof Error ? error.stack : undefined,
103
+ });
104
+ reject(error);
105
+ } else {
106
+ Logger.info(
107
+ "[BaseMailingService] Email sent successfully:",
108
+ body
109
+ );
110
+ resolve(body);
111
+ }
112
+ });
113
+ } catch (sendError) {
114
+ Logger.error(
115
+ "[BaseMailingService] Error in mailgun.messages().send():",
116
+ {
117
+ error:
118
+ sendError instanceof Error ? sendError.message : sendError,
119
+ stack: sendError instanceof Error ? sendError.stack : undefined,
120
+ }
121
+ );
122
+ reject(sendError);
123
+ }
73
124
  }
74
125
  );
75
126
  } catch (error) {
76
- console.error("[BaseMailingService] Error in sendEmail:", error);
127
+ Logger.error("[BaseMailingService] Error in sendEmail:", {
128
+ error: error instanceof Error ? error.message : error,
129
+ stack: error instanceof Error ? error.stack : undefined,
130
+ });
77
131
  throw error;
78
132
  }
79
133
  }
@@ -96,14 +150,22 @@ export class BaseMailingService {
96
150
  subject: emailData.subject,
97
151
  templateName: emailData.templateName,
98
152
  success,
99
- error: error ? JSON.stringify(error) : null,
153
+ error: error
154
+ ? error instanceof Error
155
+ ? { message: error.message, stack: error.stack }
156
+ : JSON.stringify(error)
157
+ : null,
100
158
  sentAt: admin.firestore.FieldValue.serverTimestamp(),
101
159
  });
102
- } catch (logError) {
103
- console.error(
104
- "[BaseMailingService] Error logging email attempt:",
105
- logError
160
+
161
+ Logger.info(
162
+ `[BaseMailingService] Email log recorded. Success: ${success}`
106
163
  );
164
+ } catch (logError) {
165
+ Logger.error("[BaseMailingService] Error logging email attempt:", {
166
+ error: logError instanceof Error ? logError.message : logError,
167
+ stack: logError instanceof Error ? logError.stack : undefined,
168
+ });
107
169
  // Don't throw here to prevent disrupting the main flow
108
170
  }
109
171
  }
@@ -118,14 +180,29 @@ export class BaseMailingService {
118
180
  template: string,
119
181
  variables: Record<string, string>
120
182
  ): string {
121
- let rendered = template;
183
+ if (!template) {
184
+ throw new Error("Email template is required");
185
+ }
186
+
187
+ try {
188
+ let rendered = template;
122
189
 
123
- // Replace template variables (format: {{variable_name}})
124
- Object.entries(variables).forEach(([key, value]) => {
125
- const regex = new RegExp(`{{\\s*${key}\\s*}}`, "g");
126
- rendered = rendered.replace(regex, value);
127
- });
190
+ // Replace template variables (format: {{variable_name}})
191
+ Object.entries(variables).forEach(([key, value]) => {
192
+ const regex = new RegExp(`{{\\s*${key}\\s*}}`, "g");
193
+ rendered = rendered.replace(regex, value || "");
194
+ });
128
195
 
129
- return rendered;
196
+ return rendered;
197
+ } catch (renderError) {
198
+ Logger.error("[BaseMailingService] Error rendering template:", {
199
+ error: renderError instanceof Error ? renderError.message : renderError,
200
+ });
201
+ throw new Error(
202
+ `Template rendering failed: ${
203
+ renderError instanceof Error ? renderError.message : "Unknown error"
204
+ }`
205
+ );
206
+ }
130
207
  }
131
208
  }
@@ -2,7 +2,7 @@ import * as admin from "firebase-admin";
2
2
  import * as mailgun from "mailgun-js";
3
3
  import { BaseMailingService } from "../base.mailing.service";
4
4
  import { practitionerInvitationTemplate } from "./templates/invitation.template";
5
-
5
+ import { Logger } from "../../logger";
6
6
  // Import specific types and collection constants
7
7
  import {
8
8
  Practitioner,
@@ -51,11 +51,11 @@ export interface PractitionerInviteEmailData {
51
51
  */
52
52
  export class PractitionerInviteMailingService extends BaseMailingService {
53
53
  private readonly DEFAULT_REGISTRATION_URL =
54
- "https://app.medclinic.com/register";
54
+ "https://metaestetics.net/register";
55
55
  private readonly DEFAULT_SUBJECT =
56
56
  "You've Been Invited to Join as a Practitioner";
57
57
  private readonly DEFAULT_FROM_ADDRESS =
58
- "MedClinic <no-reply@your-domain.com>";
58
+ "MedClinic <no-reply@mg.metaestetics.net>";
59
59
 
60
60
  /**
61
61
  * Constructor for PractitionerInviteMailingService
@@ -78,7 +78,7 @@ export class PractitionerInviteMailingService extends BaseMailingService {
78
78
  data: PractitionerInviteEmailData
79
79
  ): Promise<mailgun.messages.SendResponse> {
80
80
  try {
81
- console.log(
81
+ Logger.info(
82
82
  "[PractitionerInviteMailingService] Sending invitation email to",
83
83
  data.token.email
84
84
  );
@@ -126,6 +126,18 @@ export class PractitionerInviteMailingService extends BaseMailingService {
126
126
  currentYear,
127
127
  };
128
128
 
129
+ // Debug log for template variables (excluding token for security)
130
+ Logger.info("[PractitionerInviteMailingService] Template variables:", {
131
+ clinicName: templateVariables.clinicName,
132
+ practitionerName: templateVariables.practitionerName,
133
+ expirationDate: templateVariables.expirationDate,
134
+ registrationUrl: templateVariables.registrationUrl,
135
+ contactName: templateVariables.contactName,
136
+ contactEmail: templateVariables.contactEmail,
137
+ // Don't log the invite token for security
138
+ hasInviteToken: !!templateVariables.inviteToken,
139
+ });
140
+
129
141
  // Render HTML email
130
142
  const html = this.renderTemplate(
131
143
  practitionerInvitationTemplate,
@@ -140,6 +152,16 @@ export class PractitionerInviteMailingService extends BaseMailingService {
140
152
  html,
141
153
  };
142
154
 
155
+ Logger.info(
156
+ "[PractitionerInviteMailingService] Sending email with data:",
157
+ {
158
+ to: emailData.to,
159
+ from: emailData.from,
160
+ subject: emailData.subject,
161
+ hasHtml: !!emailData.html,
162
+ }
163
+ );
164
+
143
165
  const result = await this.sendEmail(emailData);
144
166
 
145
167
  // Log success
@@ -154,9 +176,12 @@ export class PractitionerInviteMailingService extends BaseMailingService {
154
176
 
155
177
  return result;
156
178
  } catch (error) {
157
- console.error(
179
+ Logger.error(
158
180
  "[PractitionerInviteMailingService] Error sending invitation email:",
159
- error
181
+ {
182
+ error: error instanceof Error ? error.message : error,
183
+ stack: error instanceof Error ? error.stack : undefined,
184
+ }
160
185
  );
161
186
 
162
187
  // Log failure
@@ -187,12 +212,36 @@ export class PractitionerInviteMailingService extends BaseMailingService {
187
212
  fromAddress: string
188
213
  ): Promise<void> {
189
214
  try {
190
- console.log(
215
+ Logger.info(
191
216
  "[PractitionerInviteMailingService] Handling token creation event for token:",
192
217
  tokenData.id
193
218
  );
194
219
 
220
+ // Validate token data
221
+ if (!tokenData || !tokenData.id || !tokenData.token || !tokenData.email) {
222
+ throw new Error(
223
+ `Invalid token data: Missing required properties. Token ID: ${tokenData?.id}`
224
+ );
225
+ }
226
+
227
+ if (!tokenData.practitionerId) {
228
+ throw new Error(
229
+ `Token ${tokenData.id} is missing practitionerId reference`
230
+ );
231
+ }
232
+
233
+ if (!tokenData.clinicId) {
234
+ throw new Error(`Token ${tokenData.id} is missing clinicId reference`);
235
+ }
236
+
237
+ if (!tokenData.expiresAt) {
238
+ throw new Error(`Token ${tokenData.id} is missing expiration date`);
239
+ }
240
+
195
241
  // Get practitioner data using constant and type
242
+ Logger.info(
243
+ `[PractitionerInviteMailingService] Fetching practitioner data: ${tokenData.practitionerId}`
244
+ );
196
245
  const practitionerRef = this.db
197
246
  .collection(PRACTITIONERS_COLLECTION)
198
247
  .doc(tokenData.practitionerId);
@@ -203,8 +252,20 @@ export class PractitionerInviteMailingService extends BaseMailingService {
203
252
  }
204
253
 
205
254
  const practitionerData = practitionerDoc.data() as Practitioner;
255
+ if (!practitionerData || !practitionerData.basicInfo) {
256
+ throw new Error(
257
+ `Practitioner ${tokenData.practitionerId} has invalid data structure`
258
+ );
259
+ }
260
+
261
+ Logger.info(
262
+ `[PractitionerInviteMailingService] Practitioner found: ${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`
263
+ );
206
264
 
207
265
  // Get clinic data using constant and type
266
+ Logger.info(
267
+ `[PractitionerInviteMailingService] Fetching clinic data: ${tokenData.clinicId}`
268
+ );
208
269
  const clinicRef = this.db
209
270
  .collection(CLINICS_COLLECTION)
210
271
  .doc(tokenData.clinicId);
@@ -215,6 +276,26 @@ export class PractitionerInviteMailingService extends BaseMailingService {
215
276
  }
216
277
 
217
278
  const clinicData = clinicDoc.data() as Clinic;
279
+ if (!clinicData || !clinicData.contactInfo) {
280
+ throw new Error(
281
+ `Clinic ${tokenData.clinicId} has invalid data structure`
282
+ );
283
+ }
284
+
285
+ Logger.info(
286
+ `[PractitionerInviteMailingService] Clinic found: ${clinicData.name}`
287
+ );
288
+
289
+ // Clinic model doesn't have contactPerson, only contactInfo
290
+ // So we'll use simple contact information from contactInfo
291
+
292
+ // Validate fromAddress
293
+ if (!fromAddress) {
294
+ Logger.warn(
295
+ "[PractitionerInviteMailingService] No fromAddress provided, using default"
296
+ );
297
+ fromAddress = this.DEFAULT_FROM_ADDRESS;
298
+ }
218
299
 
219
300
  // Prepare email data using typed data
220
301
  const emailData: PractitionerInviteEmailData = {
@@ -233,22 +314,31 @@ export class PractitionerInviteMailingService extends BaseMailingService {
233
314
  clinic: {
234
315
  name: clinicData.name || "Medical Clinic",
235
316
  contactEmail: clinicData.contactInfo.email || "contact@medclinic.com",
317
+ // Since there's no contactPerson in the Clinic model, we'll just use "Clinic Admin"
318
+ contactName: "Clinic Admin",
236
319
  },
237
320
  options: {
238
321
  fromAddress: fromAddress,
239
322
  },
240
323
  };
241
324
 
325
+ Logger.info(
326
+ "[PractitionerInviteMailingService] Email data prepared, sending invitation"
327
+ );
328
+
242
329
  // Send the invitation email
243
330
  await this.sendInvitationEmail(emailData);
244
331
 
245
- console.log(
332
+ Logger.info(
246
333
  "[PractitionerInviteMailingService] Invitation email sent successfully"
247
334
  );
248
335
  } catch (error) {
249
- console.error(
336
+ Logger.error(
250
337
  "[PractitionerInviteMailingService] Error handling token creation event:",
251
- error
338
+ {
339
+ error: error instanceof Error ? error.message : error,
340
+ stack: error instanceof Error ? error.stack : undefined,
341
+ }
252
342
  );
253
343
  throw error;
254
344
  }