Enhance user service and Salesforce connection handling

- Added 'salesforceHealthy' property to EnhancedUser interface for tracking Salesforce account health.
- Updated toEnhancedUser method to accept 'salesforceHealthy' parameter for improved user data handling.
- Modified user retrieval logic to correctly reflect Salesforce account status.
- Refactored Salesforce connection service to handle session expiration with automatic re-authentication for SObject operations.
This commit is contained in:
T. Narantuya 2025-09-02 14:15:24 +09:00
parent cc2a6a3046
commit 26eb8a7341
3 changed files with 121 additions and 26 deletions

View File

@ -26,6 +26,7 @@ export interface EnhancedUser extends Omit<User, "createdAt" | "updatedAt"> {
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> = {}): EnhancedUser {
private toEnhancedUser(
user: PrismaUser,
extras: Partial<EnhancedUser> = {},
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);
}
}

View File

@ -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<string, unknown>) => Promise<unknown>;
};
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<string, unknown>) => Promise<unknown>;
};
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<string, unknown>) => Promise<SalesforceCreateResult>;
};
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<string, unknown>) => Promise<unknown>;
};
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),

View File

@ -9,7 +9,12 @@ import * as path from "path";
export interface SalesforceSObjectApi {
create: (data: Record<string, unknown>) => Promise<{ id?: string }>;
update?: (data: Record<string, unknown>) => Promise<unknown>;
update?: (data: Record<string, unknown> & { Id: string }) => Promise<unknown>;
}
interface SalesforceRetryableSObjectApi extends SalesforceSObjectApi {
create: (data: Record<string, unknown>) => Promise<{ id?: string }>;
update?: (data: Record<string, unknown> & { Id: string }) => Promise<unknown>;
}
@Injectable()
@ -124,13 +129,101 @@ export class SalesforceConnection {
}
}
// Expose connection methods
// Expose connection methods with automatic re-authentication
async query(soql: string): Promise<unknown> {
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<string, unknown>) => {
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<string, unknown> & { 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 {