@blackcode_sa/metaestetics-api 1.12.16 → 1.12.19
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/admin/index.d.mts +1 -1
- package/dist/admin/index.d.ts +1 -1
- package/dist/admin/index.js +27 -10
- package/dist/admin/index.mjs +27 -10
- package/dist/backoffice/index.js +2 -2
- package/dist/backoffice/index.mjs +2 -2
- package/dist/index.js +35 -4
- package/dist/index.mjs +36 -4
- package/package.json +1 -1
- package/src/admin/free-consultation/README.md +82 -0
- package/src/admin/free-consultation/free-consultation-utils.admin.ts +70 -54
- package/src/backoffice/services/technology.service.ts +2 -2
- package/src/services/auth/auth.service.ts +41 -6
- package/src/services/user/user.service.ts +8 -1
package/dist/admin/index.d.mts
CHANGED
|
@@ -3388,7 +3388,7 @@ declare class DocumentManagerAdminService {
|
|
|
3388
3388
|
|
|
3389
3389
|
/**
|
|
3390
3390
|
* Ensures that the free consultation infrastructure exists in the database
|
|
3391
|
-
* Creates category, subcategory, and
|
|
3391
|
+
* Creates category, subcategory, technology, and product if they don't exist
|
|
3392
3392
|
* @param db - Firestore database instance (optional, defaults to admin.firestore())
|
|
3393
3393
|
* @returns Promise<boolean> - Always returns true after ensuring infrastructure exists
|
|
3394
3394
|
*/
|
package/dist/admin/index.d.ts
CHANGED
|
@@ -3388,7 +3388,7 @@ declare class DocumentManagerAdminService {
|
|
|
3388
3388
|
|
|
3389
3389
|
/**
|
|
3390
3390
|
* Ensures that the free consultation infrastructure exists in the database
|
|
3391
|
-
* Creates category, subcategory, and
|
|
3391
|
+
* Creates category, subcategory, technology, and product if they don't exist
|
|
3392
3392
|
* @param db - Firestore database instance (optional, defaults to admin.firestore())
|
|
3393
3393
|
* @returns Promise<boolean> - Always returns true after ensuring infrastructure exists
|
|
3394
3394
|
*/
|
package/dist/admin/index.js
CHANGED
|
@@ -7383,13 +7383,14 @@ var SUBCATEGORIES_COLLECTION = "subcategories";
|
|
|
7383
7383
|
// src/backoffice/types/technology.types.ts
|
|
7384
7384
|
var TECHNOLOGIES_COLLECTION = "technologies";
|
|
7385
7385
|
|
|
7386
|
+
// src/backoffice/types/product.types.ts
|
|
7387
|
+
var PRODUCTS_COLLECTION = "products";
|
|
7388
|
+
|
|
7386
7389
|
// src/admin/free-consultation/free-consultation-utils.admin.ts
|
|
7387
7390
|
async function freeConsultationInfrastructure(db) {
|
|
7388
7391
|
const firestore18 = db || admin16.firestore();
|
|
7389
7392
|
try {
|
|
7390
|
-
console.log(
|
|
7391
|
-
"[freeConsultationInfrastructure] Checking free consultation infrastructure..."
|
|
7392
|
-
);
|
|
7393
|
+
console.log("[freeConsultationInfrastructure] Checking free consultation infrastructure...");
|
|
7393
7394
|
const technologyRef = firestore18.collection(TECHNOLOGIES_COLLECTION).doc("free-consultation-tech");
|
|
7394
7395
|
const technologyDoc = await technologyRef.get();
|
|
7395
7396
|
if (technologyDoc.exists) {
|
|
@@ -7398,19 +7399,14 @@ async function freeConsultationInfrastructure(db) {
|
|
|
7398
7399
|
);
|
|
7399
7400
|
return true;
|
|
7400
7401
|
}
|
|
7401
|
-
console.log(
|
|
7402
|
-
"[freeConsultationInfrastructure] Creating free consultation infrastructure..."
|
|
7403
|
-
);
|
|
7402
|
+
console.log("[freeConsultationInfrastructure] Creating free consultation infrastructure...");
|
|
7404
7403
|
await createFreeConsultationInfrastructure(firestore18);
|
|
7405
7404
|
console.log(
|
|
7406
7405
|
"[freeConsultationInfrastructure] Successfully created free consultation infrastructure"
|
|
7407
7406
|
);
|
|
7408
7407
|
return true;
|
|
7409
7408
|
} catch (error) {
|
|
7410
|
-
console.error(
|
|
7411
|
-
"[freeConsultationInfrastructure] Error ensuring infrastructure:",
|
|
7412
|
-
error
|
|
7413
|
-
);
|
|
7409
|
+
console.error("[freeConsultationInfrastructure] Error ensuring infrastructure:", error);
|
|
7414
7410
|
throw error;
|
|
7415
7411
|
}
|
|
7416
7412
|
}
|
|
@@ -7474,11 +7470,32 @@ async function createFreeConsultationInfrastructure(db) {
|
|
|
7474
7470
|
updatedAt: now
|
|
7475
7471
|
};
|
|
7476
7472
|
batch.set(technologyRef, technologyData);
|
|
7473
|
+
const productRef = db.collection(TECHNOLOGIES_COLLECTION).doc("free-consultation-tech").collection(PRODUCTS_COLLECTION).doc("free-consultation-product");
|
|
7474
|
+
const productData = {
|
|
7475
|
+
id: "free-consultation-product",
|
|
7476
|
+
name: "Free Consultation Service",
|
|
7477
|
+
description: "No physical product required for consultation services",
|
|
7478
|
+
brandId: "consultation-brand",
|
|
7479
|
+
brandName: "Consultation Services",
|
|
7480
|
+
technologyId: "free-consultation-tech",
|
|
7481
|
+
technologyName: "Free Consultation Technology",
|
|
7482
|
+
categoryId: "consultation",
|
|
7483
|
+
subcategoryId: "free-consultation",
|
|
7484
|
+
isActive: true,
|
|
7485
|
+
createdAt: now,
|
|
7486
|
+
updatedAt: now,
|
|
7487
|
+
technicalDetails: "Virtual consultation service - no physical product required",
|
|
7488
|
+
warnings: [],
|
|
7489
|
+
indications: ["Initial patient assessment", "Treatment planning", "Patient education"],
|
|
7490
|
+
contraindications: []
|
|
7491
|
+
};
|
|
7492
|
+
batch.set(productRef, productData);
|
|
7477
7493
|
await batch.commit();
|
|
7478
7494
|
console.log("[freeConsultationInfrastructure] Successfully created:");
|
|
7479
7495
|
console.log(" \u2713 Category: consultation");
|
|
7480
7496
|
console.log(" \u2713 Subcategory: free-consultation");
|
|
7481
7497
|
console.log(" \u2713 Technology: free-consultation-tech");
|
|
7498
|
+
console.log(" \u2713 Product: free-consultation-product");
|
|
7482
7499
|
}
|
|
7483
7500
|
|
|
7484
7501
|
// src/admin/mailing/practitionerInvite/templates/invitation.template.ts
|
package/dist/admin/index.mjs
CHANGED
|
@@ -7321,13 +7321,14 @@ var SUBCATEGORIES_COLLECTION = "subcategories";
|
|
|
7321
7321
|
// src/backoffice/types/technology.types.ts
|
|
7322
7322
|
var TECHNOLOGIES_COLLECTION = "technologies";
|
|
7323
7323
|
|
|
7324
|
+
// src/backoffice/types/product.types.ts
|
|
7325
|
+
var PRODUCTS_COLLECTION = "products";
|
|
7326
|
+
|
|
7324
7327
|
// src/admin/free-consultation/free-consultation-utils.admin.ts
|
|
7325
7328
|
async function freeConsultationInfrastructure(db) {
|
|
7326
7329
|
const firestore18 = db || admin16.firestore();
|
|
7327
7330
|
try {
|
|
7328
|
-
console.log(
|
|
7329
|
-
"[freeConsultationInfrastructure] Checking free consultation infrastructure..."
|
|
7330
|
-
);
|
|
7331
|
+
console.log("[freeConsultationInfrastructure] Checking free consultation infrastructure...");
|
|
7331
7332
|
const technologyRef = firestore18.collection(TECHNOLOGIES_COLLECTION).doc("free-consultation-tech");
|
|
7332
7333
|
const technologyDoc = await technologyRef.get();
|
|
7333
7334
|
if (technologyDoc.exists) {
|
|
@@ -7336,19 +7337,14 @@ async function freeConsultationInfrastructure(db) {
|
|
|
7336
7337
|
);
|
|
7337
7338
|
return true;
|
|
7338
7339
|
}
|
|
7339
|
-
console.log(
|
|
7340
|
-
"[freeConsultationInfrastructure] Creating free consultation infrastructure..."
|
|
7341
|
-
);
|
|
7340
|
+
console.log("[freeConsultationInfrastructure] Creating free consultation infrastructure...");
|
|
7342
7341
|
await createFreeConsultationInfrastructure(firestore18);
|
|
7343
7342
|
console.log(
|
|
7344
7343
|
"[freeConsultationInfrastructure] Successfully created free consultation infrastructure"
|
|
7345
7344
|
);
|
|
7346
7345
|
return true;
|
|
7347
7346
|
} catch (error) {
|
|
7348
|
-
console.error(
|
|
7349
|
-
"[freeConsultationInfrastructure] Error ensuring infrastructure:",
|
|
7350
|
-
error
|
|
7351
|
-
);
|
|
7347
|
+
console.error("[freeConsultationInfrastructure] Error ensuring infrastructure:", error);
|
|
7352
7348
|
throw error;
|
|
7353
7349
|
}
|
|
7354
7350
|
}
|
|
@@ -7412,11 +7408,32 @@ async function createFreeConsultationInfrastructure(db) {
|
|
|
7412
7408
|
updatedAt: now
|
|
7413
7409
|
};
|
|
7414
7410
|
batch.set(technologyRef, technologyData);
|
|
7411
|
+
const productRef = db.collection(TECHNOLOGIES_COLLECTION).doc("free-consultation-tech").collection(PRODUCTS_COLLECTION).doc("free-consultation-product");
|
|
7412
|
+
const productData = {
|
|
7413
|
+
id: "free-consultation-product",
|
|
7414
|
+
name: "Free Consultation Service",
|
|
7415
|
+
description: "No physical product required for consultation services",
|
|
7416
|
+
brandId: "consultation-brand",
|
|
7417
|
+
brandName: "Consultation Services",
|
|
7418
|
+
technologyId: "free-consultation-tech",
|
|
7419
|
+
technologyName: "Free Consultation Technology",
|
|
7420
|
+
categoryId: "consultation",
|
|
7421
|
+
subcategoryId: "free-consultation",
|
|
7422
|
+
isActive: true,
|
|
7423
|
+
createdAt: now,
|
|
7424
|
+
updatedAt: now,
|
|
7425
|
+
technicalDetails: "Virtual consultation service - no physical product required",
|
|
7426
|
+
warnings: [],
|
|
7427
|
+
indications: ["Initial patient assessment", "Treatment planning", "Patient education"],
|
|
7428
|
+
contraindications: []
|
|
7429
|
+
};
|
|
7430
|
+
batch.set(productRef, productData);
|
|
7415
7431
|
await batch.commit();
|
|
7416
7432
|
console.log("[freeConsultationInfrastructure] Successfully created:");
|
|
7417
7433
|
console.log(" \u2713 Category: consultation");
|
|
7418
7434
|
console.log(" \u2713 Subcategory: free-consultation");
|
|
7419
7435
|
console.log(" \u2713 Technology: free-consultation-tech");
|
|
7436
|
+
console.log(" \u2713 Product: free-consultation-product");
|
|
7420
7437
|
}
|
|
7421
7438
|
|
|
7422
7439
|
// src/admin/mailing/practitionerInvite/templates/invitation.template.ts
|
package/dist/backoffice/index.js
CHANGED
|
@@ -2276,8 +2276,8 @@ var TechnologyService = class extends BaseService {
|
|
|
2276
2276
|
* console.log(allowedTechnologies.subcategories); // ["subcategory1", "subcategory2"]
|
|
2277
2277
|
*/
|
|
2278
2278
|
async getAllowedTechnologies(practitioner) {
|
|
2279
|
-
const allTechnologies = await this.
|
|
2280
|
-
const allowedTechnologies = allTechnologies.
|
|
2279
|
+
const allTechnologies = await this.getAllForFilter();
|
|
2280
|
+
const allowedTechnologies = allTechnologies.filter(
|
|
2281
2281
|
(technology) => this.validateCertification(technology.certificationRequirement, practitioner.certification)
|
|
2282
2282
|
);
|
|
2283
2283
|
const families = [...new Set(allowedTechnologies.map((t) => t.family))];
|
|
@@ -2292,8 +2292,8 @@ var TechnologyService = class extends BaseService {
|
|
|
2292
2292
|
* console.log(allowedTechnologies.subcategories); // ["subcategory1", "subcategory2"]
|
|
2293
2293
|
*/
|
|
2294
2294
|
async getAllowedTechnologies(practitioner) {
|
|
2295
|
-
const allTechnologies = await this.
|
|
2296
|
-
const allowedTechnologies = allTechnologies.
|
|
2295
|
+
const allTechnologies = await this.getAllForFilter();
|
|
2296
|
+
const allowedTechnologies = allTechnologies.filter(
|
|
2297
2297
|
(technology) => this.validateCertification(technology.certificationRequirement, practitioner.certification)
|
|
2298
2298
|
);
|
|
2299
2299
|
const families = [...new Set(allowedTechnologies.map((t) => t.family))];
|
package/dist/index.js
CHANGED
|
@@ -7406,9 +7406,12 @@ var UserService = class extends BaseService {
|
|
|
7406
7406
|
if ((await this.getUserById(userId)).patientProfile || patientProfile2.userRef) {
|
|
7407
7407
|
throw new Error("User already has a patient profile.");
|
|
7408
7408
|
}
|
|
7409
|
+
const sensitiveInfo = await patientService.getSensitiveInfo(patientProfile2.id, userId);
|
|
7410
|
+
const fullDisplayName = sensitiveInfo ? `${sensitiveInfo.firstName} ${sensitiveInfo.lastName}` : patientProfile2.displayName;
|
|
7409
7411
|
await patientService.updatePatientProfile(patientProfile2.id, {
|
|
7410
7412
|
userRef: userId,
|
|
7411
|
-
isManual: false
|
|
7413
|
+
isManual: false,
|
|
7414
|
+
displayName: fullDisplayName
|
|
7412
7415
|
});
|
|
7413
7416
|
await patientService.markPatientTokenAsUsed(token.id, token.patientId, userId);
|
|
7414
7417
|
profiles.patientProfile = patientProfile2.id;
|
|
@@ -10352,6 +10355,34 @@ var AuthService = class extends BaseService {
|
|
|
10352
10355
|
async signInWithGoogleIdToken(idToken, initialRole = "patient" /* PATIENT */) {
|
|
10353
10356
|
try {
|
|
10354
10357
|
console.log("[AUTH] Signing in with Google ID Token");
|
|
10358
|
+
let email;
|
|
10359
|
+
try {
|
|
10360
|
+
const payloadBase64 = idToken.split(".")[1];
|
|
10361
|
+
const payloadJson = globalThis.atob ? globalThis.atob(payloadBase64) : Buffer.from(payloadBase64, "base64").toString("utf8");
|
|
10362
|
+
const payload = JSON.parse(payloadJson);
|
|
10363
|
+
email = payload.email;
|
|
10364
|
+
} catch (decodeError) {
|
|
10365
|
+
console.warn("[AUTH] Failed to decode email from Google ID token:", decodeError);
|
|
10366
|
+
}
|
|
10367
|
+
if (!email) {
|
|
10368
|
+
throw new AuthError(
|
|
10369
|
+
"Unable to read email from Google token. Please try again or use another sign-in method.",
|
|
10370
|
+
"AUTH/INVALID_GOOGLE_TOKEN",
|
|
10371
|
+
400
|
|
10372
|
+
);
|
|
10373
|
+
}
|
|
10374
|
+
const methods = await (0, import_auth7.fetchSignInMethodsForEmail)(this.auth, email);
|
|
10375
|
+
console.log("[AUTH] Fetch sign in methods for email:", email, methods);
|
|
10376
|
+
const hasGoogleMethod = methods.includes(import_auth7.GoogleAuthProvider.GOOGLE_SIGN_IN_METHOD);
|
|
10377
|
+
console.log("[AUTH] Has Google method:", hasGoogleMethod);
|
|
10378
|
+
if (!hasGoogleMethod) {
|
|
10379
|
+
console.log("[AUTH] No existing Google credential for email, aborting login:", email);
|
|
10380
|
+
throw new AuthError(
|
|
10381
|
+
"No account found for this Google user. Please complete registration first.",
|
|
10382
|
+
"AUTH/USER_NOT_FOUND",
|
|
10383
|
+
404
|
|
10384
|
+
);
|
|
10385
|
+
}
|
|
10355
10386
|
const credential = import_auth7.GoogleAuthProvider.credential(idToken);
|
|
10356
10387
|
const { user: firebaseUser } = await (0, import_auth7.signInWithCredential)(this.auth, credential);
|
|
10357
10388
|
console.log("[AUTH] Firebase user signed in:", firebaseUser.uid);
|
|
@@ -10360,7 +10391,7 @@ var AuthService = class extends BaseService {
|
|
|
10360
10391
|
console.log("[AUTH] Existing user found, returning profile:", existingUser.uid);
|
|
10361
10392
|
return existingUser;
|
|
10362
10393
|
}
|
|
10363
|
-
console.log("[AUTH] No existing user
|
|
10394
|
+
console.log("[AUTH] No existing MetaEstetics user for Google account \u2013 signing out.");
|
|
10364
10395
|
await (0, import_auth7.signOut)(this.auth);
|
|
10365
10396
|
throw new AuthError(
|
|
10366
10397
|
'No account found. Please complete registration by starting with "Get Started".',
|
|
@@ -17270,8 +17301,8 @@ var TechnologyService = class extends BaseService {
|
|
|
17270
17301
|
* console.log(allowedTechnologies.subcategories); // ["subcategory1", "subcategory2"]
|
|
17271
17302
|
*/
|
|
17272
17303
|
async getAllowedTechnologies(practitioner) {
|
|
17273
|
-
const allTechnologies = await this.
|
|
17274
|
-
const allowedTechnologies = allTechnologies.
|
|
17304
|
+
const allTechnologies = await this.getAllForFilter();
|
|
17305
|
+
const allowedTechnologies = allTechnologies.filter(
|
|
17275
17306
|
(technology) => this.validateCertification(technology.certificationRequirement, practitioner.certification)
|
|
17276
17307
|
);
|
|
17277
17308
|
const families = [...new Set(allowedTechnologies.map((t) => t.family))];
|
package/dist/index.mjs
CHANGED
|
@@ -1798,6 +1798,7 @@ import {
|
|
|
1798
1798
|
sendPasswordResetEmail,
|
|
1799
1799
|
verifyPasswordResetCode,
|
|
1800
1800
|
confirmPasswordReset,
|
|
1801
|
+
fetchSignInMethodsForEmail as fetchSignInMethodsForEmail2,
|
|
1801
1802
|
signInWithCredential
|
|
1802
1803
|
} from "firebase/auth";
|
|
1803
1804
|
import {
|
|
@@ -7446,9 +7447,12 @@ var UserService = class extends BaseService {
|
|
|
7446
7447
|
if ((await this.getUserById(userId)).patientProfile || patientProfile2.userRef) {
|
|
7447
7448
|
throw new Error("User already has a patient profile.");
|
|
7448
7449
|
}
|
|
7450
|
+
const sensitiveInfo = await patientService.getSensitiveInfo(patientProfile2.id, userId);
|
|
7451
|
+
const fullDisplayName = sensitiveInfo ? `${sensitiveInfo.firstName} ${sensitiveInfo.lastName}` : patientProfile2.displayName;
|
|
7449
7452
|
await patientService.updatePatientProfile(patientProfile2.id, {
|
|
7450
7453
|
userRef: userId,
|
|
7451
|
-
isManual: false
|
|
7454
|
+
isManual: false,
|
|
7455
|
+
displayName: fullDisplayName
|
|
7452
7456
|
});
|
|
7453
7457
|
await patientService.markPatientTokenAsUsed(token.id, token.patientId, userId);
|
|
7454
7458
|
profiles.patientProfile = patientProfile2.id;
|
|
@@ -10456,6 +10460,34 @@ var AuthService = class extends BaseService {
|
|
|
10456
10460
|
async signInWithGoogleIdToken(idToken, initialRole = "patient" /* PATIENT */) {
|
|
10457
10461
|
try {
|
|
10458
10462
|
console.log("[AUTH] Signing in with Google ID Token");
|
|
10463
|
+
let email;
|
|
10464
|
+
try {
|
|
10465
|
+
const payloadBase64 = idToken.split(".")[1];
|
|
10466
|
+
const payloadJson = globalThis.atob ? globalThis.atob(payloadBase64) : Buffer.from(payloadBase64, "base64").toString("utf8");
|
|
10467
|
+
const payload = JSON.parse(payloadJson);
|
|
10468
|
+
email = payload.email;
|
|
10469
|
+
} catch (decodeError) {
|
|
10470
|
+
console.warn("[AUTH] Failed to decode email from Google ID token:", decodeError);
|
|
10471
|
+
}
|
|
10472
|
+
if (!email) {
|
|
10473
|
+
throw new AuthError(
|
|
10474
|
+
"Unable to read email from Google token. Please try again or use another sign-in method.",
|
|
10475
|
+
"AUTH/INVALID_GOOGLE_TOKEN",
|
|
10476
|
+
400
|
|
10477
|
+
);
|
|
10478
|
+
}
|
|
10479
|
+
const methods = await fetchSignInMethodsForEmail2(this.auth, email);
|
|
10480
|
+
console.log("[AUTH] Fetch sign in methods for email:", email, methods);
|
|
10481
|
+
const hasGoogleMethod = methods.includes(GoogleAuthProvider.GOOGLE_SIGN_IN_METHOD);
|
|
10482
|
+
console.log("[AUTH] Has Google method:", hasGoogleMethod);
|
|
10483
|
+
if (!hasGoogleMethod) {
|
|
10484
|
+
console.log("[AUTH] No existing Google credential for email, aborting login:", email);
|
|
10485
|
+
throw new AuthError(
|
|
10486
|
+
"No account found for this Google user. Please complete registration first.",
|
|
10487
|
+
"AUTH/USER_NOT_FOUND",
|
|
10488
|
+
404
|
|
10489
|
+
);
|
|
10490
|
+
}
|
|
10459
10491
|
const credential = GoogleAuthProvider.credential(idToken);
|
|
10460
10492
|
const { user: firebaseUser } = await signInWithCredential(this.auth, credential);
|
|
10461
10493
|
console.log("[AUTH] Firebase user signed in:", firebaseUser.uid);
|
|
@@ -10464,7 +10496,7 @@ var AuthService = class extends BaseService {
|
|
|
10464
10496
|
console.log("[AUTH] Existing user found, returning profile:", existingUser.uid);
|
|
10465
10497
|
return existingUser;
|
|
10466
10498
|
}
|
|
10467
|
-
console.log("[AUTH] No existing user
|
|
10499
|
+
console.log("[AUTH] No existing MetaEstetics user for Google account \u2013 signing out.");
|
|
10468
10500
|
await firebaseSignOut(this.auth);
|
|
10469
10501
|
throw new AuthError(
|
|
10470
10502
|
'No account found. Please complete registration by starting with "Get Started".',
|
|
@@ -17589,8 +17621,8 @@ var TechnologyService = class extends BaseService {
|
|
|
17589
17621
|
* console.log(allowedTechnologies.subcategories); // ["subcategory1", "subcategory2"]
|
|
17590
17622
|
*/
|
|
17591
17623
|
async getAllowedTechnologies(practitioner) {
|
|
17592
|
-
const allTechnologies = await this.
|
|
17593
|
-
const allowedTechnologies = allTechnologies.
|
|
17624
|
+
const allTechnologies = await this.getAllForFilter();
|
|
17625
|
+
const allowedTechnologies = allTechnologies.filter(
|
|
17594
17626
|
(technology) => this.validateCertification(technology.certificationRequirement, practitioner.certification)
|
|
17595
17627
|
);
|
|
17596
17628
|
const families = [...new Set(allowedTechnologies.map((t) => t.family))];
|
package/package.json
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Free Consultation Infrastructure
|
|
2
|
+
|
|
3
|
+
This module manages the infrastructure required for free consultation procedures in the MetaEstetics system.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The free consultation feature allows practitioners to offer complimentary initial consultations to patients. This requires specific infrastructure components to be present in the database.
|
|
8
|
+
|
|
9
|
+
## Infrastructure Components
|
|
10
|
+
|
|
11
|
+
The system creates the following components when `freeConsultationInfrastructure()` is called:
|
|
12
|
+
|
|
13
|
+
### 1. Category: "consultation"
|
|
14
|
+
- **ID**: `consultation`
|
|
15
|
+
- **Name**: Consultation
|
|
16
|
+
- **Description**: Professional consultation services for treatment planning and assessment
|
|
17
|
+
- **Family**: AESTHETICS
|
|
18
|
+
|
|
19
|
+
### 2. Subcategory: "free-consultation"
|
|
20
|
+
- **ID**: `free-consultation`
|
|
21
|
+
- **Name**: Free Consultation
|
|
22
|
+
- **Description**: Complimentary initial consultation to discuss treatment options and assess patient needs
|
|
23
|
+
- **Parent Category**: consultation
|
|
24
|
+
|
|
25
|
+
### 3. Technology: "free-consultation-tech"
|
|
26
|
+
- **ID**: `free-consultation-tech`
|
|
27
|
+
- **Name**: Free Consultation Technology
|
|
28
|
+
- **Description**: Technology framework for providing free initial consultations to patients
|
|
29
|
+
- **Requirements**: No pre/post requirements
|
|
30
|
+
- **Certification**: Minimum level AESTHETICIAN
|
|
31
|
+
|
|
32
|
+
### 4. Product: "free-consultation-product"
|
|
33
|
+
- **ID**: `free-consultation-product`
|
|
34
|
+
- **Name**: Free Consultation Service
|
|
35
|
+
- **Description**: No physical product required for consultation services
|
|
36
|
+
- **Brand**: Consultation Services
|
|
37
|
+
- **Type**: Virtual service
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Ensuring Infrastructure Exists
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { freeConsultationInfrastructure } from './free-consultation-utils.admin';
|
|
45
|
+
|
|
46
|
+
// Ensure infrastructure exists before creating consultation procedures
|
|
47
|
+
const infrastructureExists = await freeConsultationInfrastructure();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Creating Free Consultation Procedures
|
|
51
|
+
|
|
52
|
+
The infrastructure is automatically checked and created when practitioners enable free consultations through the `PractitionerService.EnableFreeConsultation()` method.
|
|
53
|
+
|
|
54
|
+
## Error Handling
|
|
55
|
+
|
|
56
|
+
If you encounter the error:
|
|
57
|
+
```
|
|
58
|
+
Product with ID free-consultation-product not found for technology free-consultation-tech
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This indicates that the infrastructure was not properly created. The system will automatically attempt to create the missing infrastructure when the error occurs.
|
|
62
|
+
|
|
63
|
+
## Database Structure
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
categories/
|
|
67
|
+
consultation/
|
|
68
|
+
subcategories/
|
|
69
|
+
free-consultation/
|
|
70
|
+
|
|
71
|
+
technologies/
|
|
72
|
+
free-consultation-tech/
|
|
73
|
+
products/
|
|
74
|
+
free-consultation-product/
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Notes
|
|
78
|
+
|
|
79
|
+
- The infrastructure is created atomically using Firestore batch operations
|
|
80
|
+
- All components are created with `isActive: true`
|
|
81
|
+
- The system checks for existing infrastructure before attempting to create new components
|
|
82
|
+
- No physical products are required for consultation procedures
|
|
@@ -1,80 +1,72 @@
|
|
|
1
|
-
import * as admin from
|
|
2
|
-
import { ProcedureFamily } from
|
|
1
|
+
import * as admin from 'firebase-admin';
|
|
2
|
+
import { ProcedureFamily } from '../../backoffice/types/static/procedure-family.types';
|
|
3
3
|
import {
|
|
4
4
|
CertificationLevel,
|
|
5
5
|
CertificationSpecialty,
|
|
6
|
-
} from
|
|
7
|
-
import { CATEGORIES_COLLECTION } from
|
|
8
|
-
import { SUBCATEGORIES_COLLECTION } from
|
|
9
|
-
import { TECHNOLOGIES_COLLECTION } from
|
|
6
|
+
} from '../../backoffice/types/static/certification.types';
|
|
7
|
+
import { CATEGORIES_COLLECTION } from '../../backoffice/types/category.types';
|
|
8
|
+
import { SUBCATEGORIES_COLLECTION } from '../../backoffice/types/subcategory.types';
|
|
9
|
+
import { TECHNOLOGIES_COLLECTION } from '../../backoffice/types/technology.types';
|
|
10
|
+
import { PRODUCTS_COLLECTION } from '../../backoffice/types/product.types';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Ensures that the free consultation infrastructure exists in the database
|
|
13
|
-
* Creates category, subcategory, and
|
|
14
|
+
* Creates category, subcategory, technology, and product if they don't exist
|
|
14
15
|
* @param db - Firestore database instance (optional, defaults to admin.firestore())
|
|
15
16
|
* @returns Promise<boolean> - Always returns true after ensuring infrastructure exists
|
|
16
17
|
*/
|
|
17
18
|
export async function freeConsultationInfrastructure(
|
|
18
|
-
db?: admin.firestore.Firestore
|
|
19
|
+
db?: admin.firestore.Firestore,
|
|
19
20
|
): Promise<boolean> {
|
|
20
21
|
const firestore = db || admin.firestore();
|
|
21
22
|
|
|
22
23
|
try {
|
|
23
|
-
console.log(
|
|
24
|
-
"[freeConsultationInfrastructure] Checking free consultation infrastructure..."
|
|
25
|
-
);
|
|
24
|
+
console.log('[freeConsultationInfrastructure] Checking free consultation infrastructure...');
|
|
26
25
|
|
|
27
26
|
// Check if the technology already exists
|
|
28
27
|
const technologyRef = firestore
|
|
29
28
|
.collection(TECHNOLOGIES_COLLECTION)
|
|
30
|
-
.doc(
|
|
29
|
+
.doc('free-consultation-tech');
|
|
31
30
|
|
|
32
31
|
const technologyDoc = await technologyRef.get();
|
|
33
32
|
|
|
34
33
|
if (technologyDoc.exists) {
|
|
35
34
|
console.log(
|
|
36
|
-
|
|
35
|
+
'[freeConsultationInfrastructure] Free consultation infrastructure already exists',
|
|
37
36
|
);
|
|
38
37
|
return true;
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
console.log(
|
|
42
|
-
"[freeConsultationInfrastructure] Creating free consultation infrastructure..."
|
|
43
|
-
);
|
|
40
|
+
console.log('[freeConsultationInfrastructure] Creating free consultation infrastructure...');
|
|
44
41
|
|
|
45
|
-
// Create the infrastructure in the correct order: Category → Subcategory → Technology
|
|
42
|
+
// Create the infrastructure in the correct order: Category → Subcategory → Technology → Product
|
|
46
43
|
await createFreeConsultationInfrastructure(firestore);
|
|
47
44
|
|
|
48
45
|
console.log(
|
|
49
|
-
|
|
46
|
+
'[freeConsultationInfrastructure] Successfully created free consultation infrastructure',
|
|
50
47
|
);
|
|
51
48
|
return true;
|
|
52
49
|
} catch (error) {
|
|
53
|
-
console.error(
|
|
54
|
-
"[freeConsultationInfrastructure] Error ensuring infrastructure:",
|
|
55
|
-
error
|
|
56
|
-
);
|
|
50
|
+
console.error('[freeConsultationInfrastructure] Error ensuring infrastructure:', error);
|
|
57
51
|
throw error;
|
|
58
52
|
}
|
|
59
53
|
}
|
|
60
54
|
|
|
61
55
|
/**
|
|
62
56
|
* Creates the complete free consultation infrastructure
|
|
57
|
+
* Creates category, subcategory, technology, and product in the correct order
|
|
63
58
|
* @param db - Firestore database instance
|
|
64
59
|
*/
|
|
65
|
-
async function createFreeConsultationInfrastructure(
|
|
66
|
-
db: admin.firestore.Firestore
|
|
67
|
-
): Promise<void> {
|
|
60
|
+
async function createFreeConsultationInfrastructure(db: admin.firestore.Firestore): Promise<void> {
|
|
68
61
|
const batch = db.batch();
|
|
69
62
|
const now = new Date();
|
|
70
63
|
|
|
71
64
|
// 1. Create Category: "consultation"
|
|
72
|
-
const categoryRef = db.collection(CATEGORIES_COLLECTION).doc(
|
|
65
|
+
const categoryRef = db.collection(CATEGORIES_COLLECTION).doc('consultation');
|
|
73
66
|
const categoryData = {
|
|
74
|
-
id:
|
|
75
|
-
name:
|
|
76
|
-
description:
|
|
77
|
-
"Professional consultation services for treatment planning and assessment",
|
|
67
|
+
id: 'consultation',
|
|
68
|
+
name: 'Consultation',
|
|
69
|
+
description: 'Professional consultation services for treatment planning and assessment',
|
|
78
70
|
family: ProcedureFamily.AESTHETICS,
|
|
79
71
|
isActive: true,
|
|
80
72
|
createdAt: now,
|
|
@@ -85,16 +77,16 @@ async function createFreeConsultationInfrastructure(
|
|
|
85
77
|
// 2. Create Subcategory: "free-consultation"
|
|
86
78
|
const subcategoryRef = db
|
|
87
79
|
.collection(CATEGORIES_COLLECTION)
|
|
88
|
-
.doc(
|
|
80
|
+
.doc('consultation')
|
|
89
81
|
.collection(SUBCATEGORIES_COLLECTION)
|
|
90
|
-
.doc(
|
|
82
|
+
.doc('free-consultation');
|
|
91
83
|
|
|
92
84
|
const subcategoryData = {
|
|
93
|
-
id:
|
|
94
|
-
name:
|
|
85
|
+
id: 'free-consultation',
|
|
86
|
+
name: 'Free Consultation',
|
|
95
87
|
description:
|
|
96
|
-
|
|
97
|
-
categoryId:
|
|
88
|
+
'Complimentary initial consultation to discuss treatment options and assess patient needs',
|
|
89
|
+
categoryId: 'consultation',
|
|
98
90
|
isActive: true,
|
|
99
91
|
createdAt: now,
|
|
100
92
|
updatedAt: now,
|
|
@@ -102,19 +94,15 @@ async function createFreeConsultationInfrastructure(
|
|
|
102
94
|
batch.set(subcategoryRef, subcategoryData);
|
|
103
95
|
|
|
104
96
|
// 3. Create Technology: "free-consultation-tech"
|
|
105
|
-
const technologyRef = db
|
|
106
|
-
.collection(TECHNOLOGIES_COLLECTION)
|
|
107
|
-
.doc("free-consultation-tech");
|
|
97
|
+
const technologyRef = db.collection(TECHNOLOGIES_COLLECTION).doc('free-consultation-tech');
|
|
108
98
|
const technologyData = {
|
|
109
|
-
id:
|
|
110
|
-
name:
|
|
111
|
-
description:
|
|
112
|
-
"Technology framework for providing free initial consultations to patients",
|
|
99
|
+
id: 'free-consultation-tech',
|
|
100
|
+
name: 'Free Consultation Technology',
|
|
101
|
+
description: 'Technology framework for providing free initial consultations to patients',
|
|
113
102
|
family: ProcedureFamily.AESTHETICS,
|
|
114
|
-
categoryId:
|
|
115
|
-
subcategoryId:
|
|
116
|
-
technicalDetails:
|
|
117
|
-
"Standard consultation protocol for initial patient assessment",
|
|
103
|
+
categoryId: 'consultation',
|
|
104
|
+
subcategoryId: 'free-consultation',
|
|
105
|
+
technicalDetails: 'Standard consultation protocol for initial patient assessment',
|
|
118
106
|
requirements: {
|
|
119
107
|
pre: [], // No pre-requirements for consultation
|
|
120
108
|
post: [], // No post-requirements for consultation
|
|
@@ -122,9 +110,9 @@ async function createFreeConsultationInfrastructure(
|
|
|
122
110
|
blockingConditions: [], // No blocking conditions for consultation
|
|
123
111
|
contraindications: [], // No contraindications for consultation
|
|
124
112
|
benefits: [
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
113
|
+
'IMPROVED_PATIENT_UNDERSTANDING',
|
|
114
|
+
'BETTER_TREATMENT_PLANNING',
|
|
115
|
+
'ENHANCED_PATIENT_CONFIDENCE',
|
|
128
116
|
],
|
|
129
117
|
certificationRequirement: {
|
|
130
118
|
minimumLevel: CertificationLevel.AESTHETICIAN,
|
|
@@ -137,11 +125,39 @@ async function createFreeConsultationInfrastructure(
|
|
|
137
125
|
};
|
|
138
126
|
batch.set(technologyRef, technologyData);
|
|
139
127
|
|
|
128
|
+
// 4. Create Product: "free-consultation-product"
|
|
129
|
+
const productRef = db
|
|
130
|
+
.collection(TECHNOLOGIES_COLLECTION)
|
|
131
|
+
.doc('free-consultation-tech')
|
|
132
|
+
.collection(PRODUCTS_COLLECTION)
|
|
133
|
+
.doc('free-consultation-product');
|
|
134
|
+
|
|
135
|
+
const productData = {
|
|
136
|
+
id: 'free-consultation-product',
|
|
137
|
+
name: 'Free Consultation Service',
|
|
138
|
+
description: 'No physical product required for consultation services',
|
|
139
|
+
brandId: 'consultation-brand',
|
|
140
|
+
brandName: 'Consultation Services',
|
|
141
|
+
technologyId: 'free-consultation-tech',
|
|
142
|
+
technologyName: 'Free Consultation Technology',
|
|
143
|
+
categoryId: 'consultation',
|
|
144
|
+
subcategoryId: 'free-consultation',
|
|
145
|
+
isActive: true,
|
|
146
|
+
createdAt: now,
|
|
147
|
+
updatedAt: now,
|
|
148
|
+
technicalDetails: 'Virtual consultation service - no physical product required',
|
|
149
|
+
warnings: [],
|
|
150
|
+
indications: ['Initial patient assessment', 'Treatment planning', 'Patient education'],
|
|
151
|
+
contraindications: [],
|
|
152
|
+
};
|
|
153
|
+
batch.set(productRef, productData);
|
|
154
|
+
|
|
140
155
|
// Commit all changes atomically
|
|
141
156
|
await batch.commit();
|
|
142
157
|
|
|
143
|
-
console.log(
|
|
144
|
-
console.log(
|
|
145
|
-
console.log(
|
|
146
|
-
console.log(
|
|
158
|
+
console.log('[freeConsultationInfrastructure] Successfully created:');
|
|
159
|
+
console.log(' ✓ Category: consultation');
|
|
160
|
+
console.log(' ✓ Subcategory: free-consultation');
|
|
161
|
+
console.log(' ✓ Technology: free-consultation-tech');
|
|
162
|
+
console.log(' ✓ Product: free-consultation-product');
|
|
147
163
|
}
|
|
@@ -693,10 +693,10 @@ export class TechnologyService extends BaseService implements ITechnologyService
|
|
|
693
693
|
subcategories: string[];
|
|
694
694
|
}> {
|
|
695
695
|
// Get all active technologies
|
|
696
|
-
const allTechnologies = await this.
|
|
696
|
+
const allTechnologies = await this.getAllForFilter();
|
|
697
697
|
|
|
698
698
|
// Filter technologies based on certification requirements
|
|
699
|
-
const allowedTechnologies = allTechnologies.
|
|
699
|
+
const allowedTechnologies = allTechnologies.filter(technology =>
|
|
700
700
|
this.validateCertification(technology.certificationRequirement, practitioner.certification),
|
|
701
701
|
);
|
|
702
702
|
|
|
@@ -883,23 +883,58 @@ export class AuthService extends BaseService {
|
|
|
883
883
|
): Promise<User> {
|
|
884
884
|
try {
|
|
885
885
|
console.log('[AUTH] Signing in with Google ID Token');
|
|
886
|
+
|
|
887
|
+
// 1) Extract the email claim from the raw JWT so we can check Auth records *before* sign-in
|
|
888
|
+
let email: string | undefined;
|
|
889
|
+
try {
|
|
890
|
+
const payloadBase64 = idToken.split('.')[1];
|
|
891
|
+
const payloadJson = globalThis.atob
|
|
892
|
+
? globalThis.atob(payloadBase64)
|
|
893
|
+
: Buffer.from(payloadBase64, 'base64').toString('utf8');
|
|
894
|
+
const payload = JSON.parse(payloadJson);
|
|
895
|
+
email = payload.email as string | undefined;
|
|
896
|
+
} catch (decodeError) {
|
|
897
|
+
console.warn('[AUTH] Failed to decode email from Google ID token:', decodeError);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (!email) {
|
|
901
|
+
throw new AuthError(
|
|
902
|
+
'Unable to read email from Google token. Please try again or use another sign-in method.',
|
|
903
|
+
'AUTH/INVALID_GOOGLE_TOKEN',
|
|
904
|
+
400,
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// 2) Check if this email already has a Google credential in Firebase Auth.
|
|
909
|
+
// If not, abort early so we *never* create an unwanted Auth user.
|
|
910
|
+
const methods = await fetchSignInMethodsForEmail(this.auth, email);
|
|
911
|
+
console.log('[AUTH] Fetch sign in methods for email:', email, methods);
|
|
912
|
+
const hasGoogleMethod = methods.includes(GoogleAuthProvider.GOOGLE_SIGN_IN_METHOD);
|
|
913
|
+
console.log('[AUTH] Has Google method:', hasGoogleMethod);
|
|
914
|
+
if (!hasGoogleMethod) {
|
|
915
|
+
console.log('[AUTH] No existing Google credential for email, aborting login:', email);
|
|
916
|
+
throw new AuthError(
|
|
917
|
+
'No account found for this Google user. Please complete registration first.',
|
|
918
|
+
'AUTH/USER_NOT_FOUND',
|
|
919
|
+
404,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// 3) Safe to sign-in – we know the credential belongs to an existing Auth account.
|
|
886
924
|
const credential = GoogleAuthProvider.credential(idToken);
|
|
887
925
|
const { user: firebaseUser } = await signInWithCredential(this.auth, credential);
|
|
888
926
|
console.log('[AUTH] Firebase user signed in:', firebaseUser.uid);
|
|
889
927
|
|
|
890
|
-
//
|
|
928
|
+
// 4) Load our domain user document.
|
|
891
929
|
const existingUser = await this.userService.getUserById(firebaseUser.uid);
|
|
892
930
|
if (existingUser) {
|
|
893
931
|
console.log('[AUTH] Existing user found, returning profile:', existingUser.uid);
|
|
894
932
|
return existingUser;
|
|
895
933
|
}
|
|
896
934
|
|
|
897
|
-
// If no profile exists
|
|
898
|
-
console.log('[AUTH] No existing user
|
|
899
|
-
|
|
900
|
-
// Sign out the Firebase user since we don't allow auto-registration
|
|
935
|
+
// 5) If no profile exists we sign out immediately and error – but crucially no phantom user was created.
|
|
936
|
+
console.log('[AUTH] No existing MetaEstetics user for Google account – signing out.');
|
|
901
937
|
await firebaseSignOut(this.auth);
|
|
902
|
-
|
|
903
938
|
throw new AuthError(
|
|
904
939
|
'No account found. Please complete registration by starting with "Get Started".',
|
|
905
940
|
'AUTH/USER_NOT_FOUND',
|
|
@@ -178,10 +178,17 @@ export class UserService extends BaseService {
|
|
|
178
178
|
throw new Error('User already has a patient profile.');
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
// Claim the profile: link userRef
|
|
181
|
+
// Claim the profile: link userRef, set isManual to false, and update displayName
|
|
182
|
+
// Get sensitive info to construct full display name
|
|
183
|
+
const sensitiveInfo = await patientService.getSensitiveInfo(patientProfile.id, userId);
|
|
184
|
+
const fullDisplayName = sensitiveInfo
|
|
185
|
+
? `${sensitiveInfo.firstName} ${sensitiveInfo.lastName}`
|
|
186
|
+
: patientProfile.displayName;
|
|
187
|
+
|
|
182
188
|
await patientService.updatePatientProfile(patientProfile.id, {
|
|
183
189
|
userRef: userId,
|
|
184
190
|
isManual: false,
|
|
191
|
+
displayName: fullDisplayName,
|
|
185
192
|
});
|
|
186
193
|
|
|
187
194
|
// Mark the token as used
|