Files
trade-message-center/apps/server/src/onetalk/read-projection.ts
T

208 lines
7.7 KiB
TypeScript

// 投影已验证的 OneTalk 中心读取响应
import {
isOneTalkCenterMessageContent,
isOneTalkMessageContent,
isOneTalkRenderedCardContent,
type OneTalkHttpConversation,
type OneTalkCenterMessage,
type OneTalkMessage,
type OneTalkCenterMessageContent,
type OneTalkRenderedCardContent,
} from "@trade-message-center/onetalk-contract";
import type {
CenterConversation,
OneTalkReadConversation,
OneTalkReadConversationRow,
OneTalkReadCustomerProfile,
OneTalkReadMessageRow,
} from "./read-model.ts";
/** 归一化 repository 会话行供传输边界继续投影。 */
export const projectReadConversation = (
row: OneTalkReadConversationRow,
): OneTalkReadConversation => {
return {
conversationId: row.conversationId,
conversationType: "direct",
name: row.name,
avatarUrl: row.avatarUrl,
customerProfile: {
name: row.name,
avatarUrl: row.avatarUrl,
buyerTags:
row.buyerTags === null || row.buyerTags === undefined ? null : [...row.buyerTags],
buyerFeatures:
row.buyerFeatures === null || row.buyerFeatures === undefined
? null
: [...row.buyerFeatures],
email: row.email ?? null,
registrationDate: row.registrationDate ?? null,
companyWebsite: row.companyWebsite ?? null,
countryCode: row.countryCode ?? null,
companyName: row.companyName ?? null,
},
lastContactTimeLong: row.lastContactTimeMs,
messagePreview: row.messagePreview,
};
};
/** 保持 WebSocket 公开会话契约不含 HTTP 客户资料包装。 */
export const toCenterConversation = (conversation: OneTalkReadConversation): CenterConversation => {
return {
conversationId: conversation.conversationId,
conversationType: conversation.conversationType,
name: conversation.name,
avatarUrl: conversation.avatarUrl,
lastContactTimeLong: conversation.lastContactTimeLong,
messagePreview: conversation.messagePreview,
};
};
/** 投影 HTTP 会话,不暴露扁平客户字段或同步内部字段。 */
export const toHttpConversation = (
conversation: OneTalkReadConversation,
): OneTalkHttpConversation => {
const customerProfile = conversation.customerProfile;
if (!customerProfile) throw new Error("Missing HTTP customer profile");
return {
conversationId: conversation.conversationId,
conversationType: conversation.conversationType,
customerProfile: {
name: customerProfile.name,
avatarUrl: customerProfile.avatarUrl,
buyer_tags: customerProfile.buyerTags === null ? null : [...customerProfile.buyerTags],
buyer_features:
customerProfile.buyerFeatures === null ? null : [...customerProfile.buyerFeatures],
email: customerProfile.email,
registration_date: customerProfile.registrationDate,
company_website: customerProfile.companyWebsite,
country_code: customerProfile.countryCode,
company_name: customerProfile.companyName,
},
lastContactTimeLong: conversation.lastContactTimeLong,
messagePreview: conversation.messagePreview,
};
};
type OneTalkCenterMessageInput = Pick<
OneTalkMessage,
| "messageId"
| "conversationId"
| "senderId"
| "participantIds"
| "direction"
| "sentAtMs"
| "content"
>;
const supplementMessageContent = (
content: OneTalkMessage["content"],
renderedCardContent: OneTalkRenderedCardContent | undefined,
): OneTalkCenterMessageContent => {
if (!isOneTalkMessageContent(content)) {
throw new Error("Invalid persisted OneTalk message content");
}
if (renderedCardContent === undefined) return content;
if (!isOneTalkRenderedCardContent(renderedCardContent)) {
throw new Error("Invalid persisted OneTalk rendered-card supplement");
}
if (renderedCardContent.kind === "rendered_inquiry") {
if (content.kind !== "inquiry") {
throw new Error("OneTalk rendered-card supplement kind does not match base message");
}
return {
...content,
product: renderedCardContent.product,
purchaseQuantity: renderedCardContent.purchaseQuantity,
requirementText: renderedCardContent.requirementText,
inquiryReference: renderedCardContent.inquiryReference,
actions: renderedCardContent.actions,
};
}
if (renderedCardContent.kind === "rendered_product") {
if (content.kind !== "product") {
throw new Error("OneTalk rendered-card supplement kind does not match base message");
}
if (
content.sourceUrl !== renderedCardContent.product.sourceUrl ||
content.productId !== renderedCardContent.product.productId
) {
throw new Error(
"OneTalk rendered-product supplement identity does not match base message",
);
}
return {
...content,
title: renderedCardContent.product.title,
imageUrl: renderedCardContent.product.imageUrl,
priceDisplay: renderedCardContent.priceDisplay,
minimumOrder: renderedCardContent.minimumOrder,
serviceBadges: renderedCardContent.serviceBadges,
};
}
if (content.kind !== "order") {
throw new Error("OneTalk rendered-card supplement kind does not match base message");
}
return {
...content,
title: renderedCardContent.title,
products: renderedCardContent.products,
productCount: renderedCardContent.productCount,
status: renderedCardContent.status,
payment: renderedCardContent.payment,
delivery: renderedCardContent.delivery,
action: renderedCardContent.action,
};
};
const projectMessageContent = (
content: OneTalkCenterMessageContent,
customerProfile?: Pick<
OneTalkReadCustomerProfile,
"name" | "avatarUrl" | "countryCode" | "companyName"
>,
): OneTalkCenterMessage["content"] => {
if (content.kind !== "business_card" || customerProfile === undefined) return { ...content };
return {
...content,
contactName: customerProfile?.name ?? null,
companyName: customerProfile?.companyName ?? null,
countryCode: customerProfile?.countryCode ?? null,
avatarUrl: customerProfile?.avatarUrl ?? null,
};
};
/** 从 normalized JSONB 事实和会话客户资料投影 Mind-facing 消息。 */
export const toOneTalkCenterMessage = (
message: OneTalkCenterMessageInput,
renderedCardContent?: OneTalkRenderedCardContent,
customerProfile?: Pick<
OneTalkReadCustomerProfile,
"name" | "avatarUrl" | "countryCode" | "companyName"
>,
): OneTalkCenterMessage => {
const content = supplementMessageContent(message.content, renderedCardContent);
if (!isOneTalkCenterMessageContent(content)) {
throw new Error("Invalid persisted OneTalk message content");
}
return {
messageId: message.messageId,
conversationId: message.conversationId,
senderId: message.senderId,
participantIds: [...message.participantIds],
direction: message.direction,
sentAtMs: message.sentAtMs,
content: projectMessageContent(content, customerProfile),
};
};
export const projectCenterMessage = (
row: OneTalkReadMessageRow,
customerProfile?: Pick<
OneTalkReadCustomerProfile,
"name" | "avatarUrl" | "countryCode" | "companyName"
>,
): OneTalkCenterMessage => toOneTalkCenterMessage(row, row.renderedCardContent, customerProfile);