barsa fd5336f499 Implement Case Messaging and Commenting Features in Salesforce Integration
- Added methods in `SalesforceCaseService` to fetch and add case messages, unifying email messages and case comments into a chronological conversation thread.
- Enhanced `SupportService` and `SupportController` to handle case message retrieval and comment addition, integrating new DTOs for request and response validation.
- Updated `SupportCaseDetailView` in the Portal to display conversation messages and allow users to add comments, improving user interaction with support cases.
- Introduced new schemas in the domain for case messages and comments, ensuring robust validation and type safety.
- Refactored Salesforce mappers to transform email messages and case comments into a unified format for display in the portal.
2025-12-29 18:39:13 +09:00

360 lines
10 KiB
TypeScript

/**
* Support Domain - Salesforce Provider Mapper
*
* Transform functions to convert raw Salesforce Case records to domain types.
*/
import {
supportCaseSchema,
caseMessageSchema,
type SupportCase,
type CaseMessage,
} from "../../schema.js";
import type {
SalesforceCaseRecord,
SalesforceEmailMessage,
SalesforceCaseComment,
} from "./raw.types.js";
import {
getStatusDisplayLabel,
getPriorityDisplayLabel,
SALESFORCE_CASE_STATUS,
SALESFORCE_CASE_PRIORITY,
} from "./raw.types.js";
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Safely coerce a value to string or return undefined
*/
function ensureString(value: unknown): string | undefined {
if (typeof value === "string" && value.length > 0) {
return value;
}
return undefined;
}
/**
* Get current ISO timestamp
*/
function nowIsoString(): string {
return new Date().toISOString();
}
// ============================================================================
// Transform Functions
// ============================================================================
/**
* Transform a raw Salesforce Case record to a portal SupportCase.
*
* Converts Salesforce API values (often in Japanese) to portal display labels (English).
*
* @param record - Raw Salesforce Case record from SOQL query
* @returns Validated SupportCase domain object
*/
export function transformSalesforceCaseToSupportCase(record: SalesforceCaseRecord): SupportCase {
// Get raw values
const rawStatus = ensureString(record.Status) ?? SALESFORCE_CASE_STATUS.NEW;
const rawPriority = ensureString(record.Priority) ?? SALESFORCE_CASE_PRIORITY.MEDIUM;
return supportCaseSchema.parse({
id: record.Id,
caseNumber: record.CaseNumber,
subject: ensureString(record.Subject) ?? "",
// Convert Japanese SF values to English display labels
status: getStatusDisplayLabel(rawStatus),
priority: getPriorityDisplayLabel(rawPriority),
category: ensureString(record.Type) ?? null,
description: ensureString(record.Description) ?? "",
createdAt: ensureString(record.CreatedDate) ?? nowIsoString(),
updatedAt: ensureString(record.LastModifiedDate) ?? nowIsoString(),
closedAt: ensureString(record.ClosedDate) ?? null,
});
}
/**
* Transform multiple Salesforce Case records to SupportCase array.
*
* @param records - Array of raw Salesforce Case records
* @returns Array of validated SupportCase domain objects
*/
export function transformSalesforceCasesToSupportCases(
records: SalesforceCaseRecord[]
): SupportCase[] {
return records.map(transformSalesforceCaseToSupportCase);
}
/**
* Build the SOQL SELECT fields for Case queries.
*
* Standard Salesforce Case fields based on org configuration.
* Note: Type field is not accessible via API in this org.
*
* @param additionalFields - Optional additional fields to include
* @returns Array of field names for SOQL SELECT clause
*/
export function buildCaseSelectFields(additionalFields: string[] = []): string[] {
const baseFields = [
// Core identifiers
"Id",
"CaseNumber",
// Case content
"Subject",
"Description",
// Picklist fields
"Status",
"Priority",
"Origin",
// Relationships
"AccountId",
"ContactId",
"OwnerId",
// Timestamps
"CreatedDate",
"LastModifiedDate",
"ClosedDate",
// Flags
"IsEscalated",
];
return [...new Set([...baseFields, ...additionalFields])];
}
/**
* Build a SOQL query for fetching cases for an account.
*
* @param accountId - Salesforce Account ID
* @param origin - Case origin to filter by (e.g., "Portal Support")
* @param additionalFields - Optional additional fields to include
* @returns SOQL query string
*/
export function buildCasesForAccountQuery(
accountId: string,
origin: string,
additionalFields: string[] = []
): string {
const fields = buildCaseSelectFields(additionalFields).join(", ");
return `
SELECT ${fields}
FROM Case
WHERE AccountId = '${accountId}' AND Origin = '${origin}'
ORDER BY CreatedDate DESC
LIMIT 100
`.trim();
}
/**
* Build a SOQL query for fetching a single case by ID.
*
* @param caseId - Salesforce Case ID
* @param accountId - Salesforce Account ID (for ownership validation)
* @param origin - Case origin to filter by
* @param additionalFields - Optional additional fields to include
* @returns SOQL query string
*/
export function buildCaseByIdQuery(
caseId: string,
accountId: string,
origin: string,
additionalFields: string[] = []
): string {
const fields = buildCaseSelectFields(additionalFields).join(", ");
return `
SELECT ${fields}
FROM Case
WHERE Id = '${caseId}' AND AccountId = '${accountId}' AND Origin = '${origin}'
LIMIT 1
`.trim();
}
// ============================================================================
// EmailMessage Transform Functions
// ============================================================================
/**
* Build the SOQL SELECT fields for EmailMessage queries.
*/
export function buildEmailMessageSelectFields(): string[] {
return [
"Id",
"ParentId",
"Subject",
"TextBody",
"HtmlBody",
"FromAddress",
"FromName",
"ToAddress",
"Incoming",
"Status",
"MessageDate",
"CreatedDate",
"HasAttachment",
];
}
/**
* Build SOQL query for fetching EmailMessages for a Case.
*/
export function buildEmailMessagesForCaseQuery(caseId: string): string {
const fields = buildEmailMessageSelectFields().join(", ");
return `
SELECT ${fields}
FROM EmailMessage
WHERE ParentId = '${caseId}'
ORDER BY MessageDate ASC
`.trim();
}
/**
* Transform a Salesforce EmailMessage to a unified CaseMessage.
*
* @param email - Raw Salesforce EmailMessage
* @param customerEmail - Customer's email address for comparison
*/
export function transformEmailMessageToCaseMessage(
email: SalesforceEmailMessage,
customerEmail?: string
): CaseMessage {
const isIncoming = email.Incoming === true;
const fromEmail = ensureString(email.FromAddress);
const fromName = ensureString(email.FromName) ?? fromEmail ?? "Unknown";
// Determine if this is from the customer
// Incoming emails are from customer, or check email match
const isCustomer =
isIncoming ||
(customerEmail ? fromEmail?.toLowerCase() === customerEmail.toLowerCase() : false);
return caseMessageSchema.parse({
id: email.Id,
type: "email",
body: ensureString(email.TextBody) ?? ensureString(email.HtmlBody) ?? "",
author: {
name: fromName,
email: fromEmail ?? null,
isCustomer,
},
createdAt: ensureString(email.MessageDate) ?? ensureString(email.CreatedDate) ?? nowIsoString(),
direction: isIncoming ? "inbound" : "outbound",
});
}
/**
* Transform multiple EmailMessages to CaseMessages.
*/
export function transformEmailMessagesToCaseMessages(
emails: SalesforceEmailMessage[],
customerEmail?: string
): CaseMessage[] {
return emails.map(email => transformEmailMessageToCaseMessage(email, customerEmail));
}
// ============================================================================
// CaseComment Transform Functions
// ============================================================================
/**
* Build the SOQL SELECT fields for CaseComment queries.
*/
export function buildCaseCommentSelectFields(): string[] {
return [
"Id",
"ParentId",
"CommentBody",
"IsPublished",
"CreatedById",
"CreatedBy.Id",
"CreatedBy.Name",
"CreatedBy.Email",
"CreatedDate",
];
}
/**
* Build SOQL query for fetching public CaseComments for a Case.
*/
export function buildCaseCommentsForCaseQuery(caseId: string): string {
const fields = buildCaseCommentSelectFields().join(", ");
return `
SELECT ${fields}
FROM CaseComment
WHERE ParentId = '${caseId}' AND IsPublished = true
ORDER BY CreatedDate ASC
`.trim();
}
/**
* Transform a Salesforce CaseComment to a unified CaseMessage.
*
* @param comment - Raw Salesforce CaseComment
* @param customerContactId - Customer's Salesforce Contact ID for comparison
*/
export function transformCaseCommentToCaseMessage(
comment: SalesforceCaseComment,
customerContactId?: string
): CaseMessage {
const authorName = comment.CreatedBy?.Name ?? "Unknown";
const authorEmail = comment.CreatedBy?.Email ?? null;
// Determine if this is from the customer
// If CreatedById matches customer's contact ID, it's from customer
// Otherwise, we assume it's from an agent (conservative approach)
const isCustomer = customerContactId ? comment.CreatedById === customerContactId : false;
return caseMessageSchema.parse({
id: comment.Id,
type: "comment",
body: ensureString(comment.CommentBody) ?? "",
author: {
name: authorName,
email: authorEmail,
isCustomer,
},
createdAt: ensureString(comment.CreatedDate) ?? nowIsoString(),
direction: null, // Comments don't have direction
});
}
/**
* Transform multiple CaseComments to CaseMessages.
*/
export function transformCaseCommentsToCaseMessages(
comments: SalesforceCaseComment[],
customerContactId?: string
): CaseMessage[] {
return comments.map(comment => transformCaseCommentToCaseMessage(comment, customerContactId));
}
// ============================================================================
// Unified Message Functions
// ============================================================================
/**
* Merge and sort EmailMessages and CaseComments into a unified timeline.
*
* @param emailMessages - Transformed email messages
* @param caseComments - Transformed case comments
* @returns Merged and sorted array of CaseMessages
*/
export function mergeAndSortCaseMessages(
emailMessages: CaseMessage[],
caseComments: CaseMessage[]
): CaseMessage[] {
const allMessages = [...emailMessages, ...caseComments];
// Sort by createdAt ascending (oldest first)
return allMessages.sort((a, b) => {
const dateA = new Date(a.createdAt).getTime();
const dateB = new Date(b.createdAt).getTime();
return dateA - dateB;
});
}