diff --git a/apps/bff/src/users/users.service.ts b/apps/bff/src/users/users.service.ts index 0bdcd698..3bfab8c2 100644 --- a/apps/bff/src/users/users.service.ts +++ b/apps/bff/src/users/users.service.ts @@ -26,6 +26,7 @@ export interface EnhancedUser extends Omit { buildingName?: string | null; roomNumber?: string | null; }; + salesforceHealthy?: boolean; } // Salesforce Account interface based on the data model @@ -81,7 +82,11 @@ export class UsersService { ) {} // Helper function to convert Prisma user to EnhancedUser type - private toEnhancedUser(user: PrismaUser, extras: Partial = {}): EnhancedUser { + private toEnhancedUser( + user: PrismaUser, + extras: Partial = {}, + salesforceHealthy: boolean = true + ): EnhancedUser { return { id: user.id, email: user.email, @@ -93,6 +98,7 @@ export class UsersService { emailVerified: user.emailVerified, createdAt: user.createdAt, updatedAt: user.updatedAt, + salesforceHealthy, ...extras, }; } @@ -194,7 +200,7 @@ export class UsersService { error: getErrorMessage(error), userId: validId, }); - return this.toEnhancedUser(user); + return this.toEnhancedUser(user, {}, false); } } catch (error) { this.logger.error("Failed to find user by ID", { @@ -209,25 +215,29 @@ export class UsersService { if (!user) throw new Error("User not found"); const mapping = await this.mappingsService.findByUserId(userId); - if (!mapping?.sfAccountId) return this.toEnhancedUser(user); + if (!mapping?.sfAccountId) return this.toEnhancedUser(user, {}, true); + let salesforceHealthy = true; try { const account = (await this.salesforceService.getAccount( mapping.sfAccountId )) as SalesforceAccount | null; - if (!account) return this.toEnhancedUser(user); + if (!account) return this.toEnhancedUser(user, undefined, salesforceHealthy); return this.toEnhancedUser(user, { company: account.Name?.trim() || user.company || undefined, email: user.email, // Keep original email for now phone: user.phone || undefined, // Keep original phone for now // Address temporarily disabled until field issues resolved - }); + }, salesforceHealthy); } catch (error) { + salesforceHealthy = false; this.logger.error("Failed to fetch Salesforce account data", { error: getErrorMessage(error), + userId, + sfAccountId: mapping.sfAccountId, }); - return this.toEnhancedUser(user); + return this.toEnhancedUser(user, undefined, salesforceHealthy); } } diff --git a/apps/bff/src/vendors/salesforce/services/salesforce-account.service.ts b/apps/bff/src/vendors/salesforce/services/salesforce-account.service.ts index 2b89f249..b18cf494 100644 --- a/apps/bff/src/vendors/salesforce/services/salesforce-account.service.ts +++ b/apps/bff/src/vendors/salesforce/services/salesforce-account.service.ts @@ -93,11 +93,9 @@ export class SalesforceAccountService { if (!whAccountValue?.trim()) throw new Error("WH Account value is required"); try { - const sobject = this.connection.sobject("Account") as unknown as { - update: (data: Record) => Promise; - }; + const sobject = this.connection.sobject("Account"); - await sobject.update({ + await sobject.update?.({ Id: accountId.trim(), WH_Account__c: whAccountValue.trim(), }); @@ -146,17 +144,13 @@ export class SalesforceAccountService { if (existingAccount.totalSize > 0) { const accountId = existingAccount.records[0].Id; - const sobject = this.connection.sobject("Account") as unknown as { - update: (data: Record) => Promise; - }; - await sobject.update({ Id: accountId, ...sfData }); + const sobject = this.connection.sobject("Account"); + await sobject.update?.({ Id: accountId, ...sfData }); return { id: accountId, created: false }; } else { - const sobject = this.connection.sobject("Account") as unknown as { - create: (data: Record) => Promise; - }; + const sobject = this.connection.sobject("Account"); const result = await sobject.create(sfData); - return { id: result.id, created: true }; + return { id: result.id || '', created: true }; } } catch (error) { this.logger.error("Failed to upsert account", { @@ -189,10 +183,8 @@ export class SalesforceAccountService { const validAccountId = this.validateId(accountId); try { - const sobject = this.connection.sobject("Account") as unknown as { - update: (data: Record) => Promise; - }; - await sobject.update({ Id: validAccountId, ...updates }); + const sobject = this.connection.sobject("Account"); + await sobject.update?.({ Id: validAccountId, ...updates }); } catch (error) { this.logger.error("Failed to update account", { error: getErrorMessage(error), diff --git a/apps/bff/src/vendors/salesforce/services/salesforce-connection.service.ts b/apps/bff/src/vendors/salesforce/services/salesforce-connection.service.ts index 15ce7976..a783731b 100644 --- a/apps/bff/src/vendors/salesforce/services/salesforce-connection.service.ts +++ b/apps/bff/src/vendors/salesforce/services/salesforce-connection.service.ts @@ -9,7 +9,12 @@ import * as path from "path"; export interface SalesforceSObjectApi { create: (data: Record) => Promise<{ id?: string }>; - update?: (data: Record) => Promise; + update?: (data: Record & { Id: string }) => Promise; +} + +interface SalesforceRetryableSObjectApi extends SalesforceSObjectApi { + create: (data: Record) => Promise<{ id?: string }>; + update?: (data: Record & { Id: string }) => Promise; } @Injectable() @@ -124,13 +129,101 @@ export class SalesforceConnection { } } - // Expose connection methods + // Expose connection methods with automatic re-authentication async query(soql: string): Promise { - return await this.connection.query(soql); + try { + return await this.connection.query(soql); + } catch (error: any) { + // Check if this is a session expiration error + if (this.isSessionExpiredError(error)) { + this.logger.warn("Salesforce session expired, attempting to re-authenticate"); + + try { + // Re-authenticate + await this.connect(); + + // Retry the query once + this.logger.debug("Retrying query after re-authentication"); + return await this.connection.query(soql); + } catch (retryError) { + this.logger.error("Failed to re-authenticate or retry query", { + originalError: getErrorMessage(error), + retryError: getErrorMessage(retryError), + }); + throw retryError; + } + } + + // Re-throw other errors as-is + throw error; + } + } + + private isSessionExpiredError(error: any): boolean { + // Check for various session expiration indicators + const errorMessage = getErrorMessage(error).toLowerCase(); + const errorCode = error?.errorCode || error?.name || ''; + + return ( + errorCode === 'INVALID_SESSION_ID' || + errorMessage.includes('session expired') || + errorMessage.includes('invalid session') || + errorMessage.includes('invalid_session_id') || + (error?.status === 401 && errorMessage.includes('unauthorized')) + ); } sobject(type: string): SalesforceSObjectApi { - return this.connection.sobject(type) as unknown as SalesforceSObjectApi; + const originalSObject = this.connection.sobject(type); + + // Return a wrapper that handles session expiration for SObject operations + return { + create: async (data: Record) => { + try { + return await originalSObject.create(data); + } catch (error: any) { + if (this.isSessionExpiredError(error)) { + this.logger.warn("Salesforce session expired during SObject create, attempting to re-authenticate"); + + try { + await this.connect(); + const newSObject = this.connection.sobject(type); + return await newSObject.create(data); + } catch (retryError) { + this.logger.error("Failed to re-authenticate or retry SObject create", { + originalError: getErrorMessage(error), + retryError: getErrorMessage(retryError), + }); + throw retryError; + } + } + throw error; + } + }, + + update: async (data: Record & { Id: string }) => { + try { + return await originalSObject.update(data as any); + } catch (error: any) { + if (this.isSessionExpiredError(error)) { + this.logger.warn("Salesforce session expired during SObject update, attempting to re-authenticate"); + + try { + await this.connect(); + const newSObject = this.connection.sobject(type); + return await newSObject.update(data as any); + } catch (retryError) { + this.logger.error("Failed to re-authenticate or retry SObject update", { + originalError: getErrorMessage(error), + retryError: getErrorMessage(retryError), + }); + throw retryError; + } + } + throw error; + } + } + }; } isConnected(): boolean {