/** * 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; }); }