文件
OmniCard/backend/test/e2e/omnicard-30-points.test.ts
wikiboss 62e0ae4435 feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:01:47 +08:00

1335 行
52 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

import * as crypto from 'crypto';
import * as AdmZip from 'adm-zip';
// ==========================================
// 1. 实体与契约导入
// ==========================================
import { CardInfoEntity } from '../../src/modules/card/entity/info';
import { CardBentoEntity } from '../../src/modules/card/entity/bento';
import { CardCasesEntity } from '../../src/modules/card/entity/cases';
import { CardArticleEntity } from '../../src/modules/card/entity/article';
import { CardLocaleEntity } from '../../src/modules/card/entity/locale';
import { CrmTrackEntity } from '../../src/modules/crm/entity/track';
import { CrmLeadEntity } from '../../src/modules/crm/entity/lead';
import { CrmLeadAuditEntity } from '../../src/modules/crm/entity/audit';
import { AiConciergeEntity } from '../../src/modules/ai/entity/concierge';
import { AiGlossaryEntity } from '../../src/modules/ai/entity/glossary';
import { NfcDeviceEntity } from '../../src/modules/nfc/entity/device';
import { NfcRuleEntity } from '../../src/modules/nfc/entity/rule';
import { BaseSysDepartmentEntity } from '../../src/modules/base/entity/sys/department';
import { BaseSysUserEntity } from '../../src/modules/base/entity/sys/user';
import { TeamDomainEntity } from '../../src/modules/team/entity/domain';
import { TeamSubscriptionEntity } from '../../src/modules/team/entity/subscription';
import { TeamInvoiceEntity } from '../../src/modules/team/entity/invoice';
import { TeamHandoverLogEntity } from '../../src/modules/team/entity/handover-log';
import {
BentoBlockType,
BentoColSpan,
BentoRowSpan,
RadarTrack,
StripeWebhookEvent,
} from '../../src/comm/types/omni';
// ==========================================
// 2. 真实服务类导入 (全真实调用,拒绝 mock 空壳)
// ==========================================
import { CardBentoService } from '../../src/modules/card/service/bento';
import { CardArticleService } from '../../src/modules/card/service/article';
import { CardLocaleService } from '../../src/modules/card/service/locale';
import {
CardCalendarService,
generateIcsEvent,
parseIcsEvent,
} from '../../src/modules/card/service/calendar';
import { CardPortalService } from '../../src/modules/card/service/portal';
import { NfcAntiTamperService } from '../../src/modules/nfc/service/anti-tamper';
import { CrmRadarService } from '../../src/modules/crm/service/radar';
import { CrmRadarPushService, RadarPushPayload } from '../../src/modules/crm/service/push';
import { AiConciergeService } from '../../src/modules/ai/service/concierge';
import { AiKnowledgeService } from '../../src/modules/ai/service/knowledge';
import { AiGlossaryService } from '../../src/modules/ai/service/glossary';
import { AiChatService } from '../../src/modules/ai/service/chat';
import { CrmLeadScoreService } from '../../src/modules/crm/service/lead-score';
import { CrmSyncService } from '../../src/modules/crm/service/sync';
import { CrmLeadPoolService } from '../../src/modules/crm/service/lead-pool';
import { TeamHandoverService } from '../../src/modules/team/service/handover';
import { NfcDeviceService } from '../../src/modules/nfc/service/device';
import { NfcRuleService } from '../../src/modules/nfc/service/rule';
import { NfcSecurityService } from '../../src/modules/nfc/service/security';
import { NfcBatchExportService } from '../../src/modules/nfc/service/batch-export';
import { TeamMembersService } from '../../src/modules/team/service/members';
import { TeamDomainService } from '../../src/modules/team/service/domain';
import { TeamStripeService } from '../../src/modules/team/service/stripe';
import { TeamQuotaService } from '../../src/modules/team/service/quota';
import { TeamInvoiceService } from '../../src/modules/team/service/invoice';
/**
* 高保真通用的内存仓储工厂,真实执行增删改查、排序、OR/AND过滤及关联查找
*/
class MemoryRepository<T extends { id?: any; createTime?: any; updateTime?: any }> {
public data: T[] = [];
private currentId = 1;
constructor(initial: Partial<T>[] = []) {
this.data = initial.map(item => this.createEntity(item));
if (this.data.length > 0) {
this.currentId = Math.max(...this.data.map(d => Number(d.id) || 0)) + 1;
}
}
private createEntity(item: Partial<T>): T {
const entity = {
id: item.id || this.currentId++,
createTime: item.createTime || new Date(),
updateTime: new Date(),
status: (item as any).status !== undefined ? (item as any).status : 1,
...item,
} as unknown as T;
return entity;
}
private matchCondition(item: any, where: any): boolean {
if (!where) return true;
if (Array.isArray(where)) {
return where.some(cond => this.matchCondition(item, cond));
}
for (const key of Object.keys(where)) {
const expected = where[key];
if (expected === undefined) continue;
// 支持 TypeORM 操作符或带 _value 的过滤
if (expected && typeof expected === 'object' && ('_value' in expected || '_type' in expected)) {
const val = expected._value !== undefined ? expected._value : expected;
if (expected._type === 'moreThanOrEqual' || expected._type === 'moreThan') {
if (new Date(item[key]) < new Date(val)) return false;
} else if (item[key] !== val) {
return false;
}
} else if (item[key] !== expected) {
return false;
}
}
return true;
}
async find(options?: any): Promise<T[]> {
let result = [...this.data];
if (options?.where) {
result = result.filter(item => this.matchCondition(item, options.where));
}
if (options?.order) {
const orderKeys = Object.keys(options.order);
result.sort((a: any, b: any) => {
for (const key of orderKeys) {
const dir = String(options.order[key] || 'ASC').toUpperCase();
if (a[key] < b[key]) return dir === 'ASC' ? -1 : 1;
if (a[key] > b[key]) return dir === 'ASC' ? 1 : -1;
}
return 0;
});
}
return result.map(d => ({ ...d }));
}
async findOne(options?: any): Promise<T | null> {
const list = await this.find(options);
return list.length > 0 ? ({ ...list[0] } as T) : null;
}
async findOneBy(where: any): Promise<T | null> {
return this.findOne({ where });
}
async save(entityOrEntities: any): Promise<any> {
if (Array.isArray(entityOrEntities)) {
const list: T[] = [];
for (const item of entityOrEntities) {
list.push(await this.save(item));
}
return list;
}
const item = entityOrEntities;
if (item.id) {
const index = this.data.findIndex(d => Number(d.id) === Number(item.id));
if (index !== -1) {
this.data[index] = {
...this.data[index],
...item,
updateTime: new Date(),
};
return { ...this.data[index] };
}
}
const newEntity = this.createEntity(item);
this.data.push(newEntity);
return { ...newEntity };
}
async insert(item: any): Promise<{ identifiers: [{ id: any }] }> {
const saved = await this.save(item);
return { identifiers: [{ id: saved.id }] };
}
async count(options?: any): Promise<number> {
const list = await this.find(options);
return list.length;
}
async delete(condition: any): Promise<{ affected?: number }> {
const initialLen = this.data.length;
if (typeof condition === 'number' || typeof condition === 'string') {
this.data = this.data.filter(d => Number(d.id) !== Number(condition));
} else if (Array.isArray(condition)) {
const idSet = new Set(condition.map(id => Number(id)));
this.data = this.data.filter(d => !idSet.has(Number(d.id)));
} else if (condition && typeof condition === 'object') {
this.data = this.data.filter(item => !this.matchCondition(item, condition));
}
return { affected: initialLen - this.data.length };
}
async update(criteria: any, partial: any): Promise<{ affected?: number }> {
const id = typeof criteria === 'object' ? criteria.id : criteria;
const target = this.data.find(d => Number(d.id) === Number(id));
if (target) {
if (typeof criteria === 'object') {
for (const k of Object.keys(criteria)) {
if (target[k] !== criteria[k]) return { affected: 0 };
}
}
Object.assign(target, partial, { updateTime: new Date() });
return { affected: 1 };
}
return { affected: 0 };
}
clear(): void {
this.data = [];
this.currentId = 1;
}
}
describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
// 全局持久化数据仓储 (在 6 大主干流各环节中无缝贯通)
let cardRepo: MemoryRepository<CardInfoEntity>;
let bentoRepo: MemoryRepository<CardBentoEntity>;
let casesRepo: MemoryRepository<CardCasesEntity>;
let articleRepo: MemoryRepository<CardArticleEntity>;
let localeRepo: MemoryRepository<CardLocaleEntity>;
let trackRepo: MemoryRepository<CrmTrackEntity>;
let leadRepo: MemoryRepository<CrmLeadEntity>;
let leadAuditRepo: MemoryRepository<CrmLeadAuditEntity>;
let conciergeRepo: MemoryRepository<AiConciergeEntity>;
let glossaryRepo: MemoryRepository<AiGlossaryEntity>;
let nfcDeviceRepo: MemoryRepository<NfcDeviceEntity>;
let nfcRuleRepo: MemoryRepository<NfcRuleEntity>;
let deptRepo: MemoryRepository<BaseSysDepartmentEntity>;
let userRepo: MemoryRepository<BaseSysUserEntity>;
let domainRepo: MemoryRepository<TeamDomainEntity>;
let subRepo: MemoryRepository<TeamSubscriptionEntity>;
let invoiceRepo: MemoryRepository<TeamInvoiceEntity>;
let handoverLogRepo: MemoryRepository<TeamHandoverLogEntity>;
// 业务服务实例 (真实逻辑)
let bentoService: CardBentoService;
let articleService: CardArticleService;
let localeService: CardLocaleService;
let calendarService: CardCalendarService;
let portalService: CardPortalService;
let nfcAntiTamperService: NfcAntiTamperService;
let crmRadarService: CrmRadarService;
let crmRadarPushService: CrmRadarPushService;
let aiConciergeService: AiConciergeService;
let aiKnowledgeService: AiKnowledgeService;
let aiGlossaryService: AiGlossaryService;
let aiChatService: AiChatService;
let crmLeadScoreService: CrmLeadScoreService;
let crmSyncService: CrmSyncService;
let crmLeadPoolService: CrmLeadPoolService;
let teamHandoverService: TeamHandoverService;
let nfcDeviceService: NfcDeviceService;
let nfcRuleService: NfcRuleService;
let nfcSecurityService: NfcSecurityService;
let nfcBatchExportService: NfcBatchExportService;
let teamMembersService: TeamMembersService;
let teamDomainService: TeamDomainService;
let teamStripeService: TeamStripeService;
let teamQuotaService: TeamQuotaService;
let teamInvoiceService: TeamInvoiceService;
// 全链路贯穿共享上下文数据 (跨流传递)
let sharedTenantId: number;
let sharedCardId: number;
let sharedCardSlug: string;
let sharedSalesUserId: number;
let sharedSuccessorUserId: number;
let sharedNfcUid: string;
let sharedNfcSecret: string;
let sharedDeviceToken: string;
let sharedVisitorId: string;
let sharedLeadId: number;
beforeAll(() => {
// 1. 初始化所有仓储
cardRepo = new MemoryRepository<CardInfoEntity>();
bentoRepo = new MemoryRepository<CardBentoEntity>();
casesRepo = new MemoryRepository<CardCasesEntity>();
articleRepo = new MemoryRepository<CardArticleEntity>();
localeRepo = new MemoryRepository<CardLocaleEntity>();
trackRepo = new MemoryRepository<CrmTrackEntity>();
leadRepo = new MemoryRepository<CrmLeadEntity>();
leadAuditRepo = new MemoryRepository<CrmLeadAuditEntity>();
conciergeRepo = new MemoryRepository<AiConciergeEntity>();
glossaryRepo = new MemoryRepository<AiGlossaryEntity>();
nfcDeviceRepo = new MemoryRepository<NfcDeviceEntity>();
nfcRuleRepo = new MemoryRepository<NfcRuleEntity>();
deptRepo = new MemoryRepository<BaseSysDepartmentEntity>();
userRepo = new MemoryRepository<BaseSysUserEntity>();
domainRepo = new MemoryRepository<TeamDomainEntity>();
subRepo = new MemoryRepository<TeamSubscriptionEntity>();
invoiceRepo = new MemoryRepository<TeamInvoiceEntity>();
handoverLogRepo = new MemoryRepository<TeamHandoverLogEntity>();
// 2. 初始化各真实业务 Service 并挂载真实仓储
bentoService = new CardBentoService();
bentoService.cardBentoEntity = bentoRepo as any;
articleService = new CardArticleService();
articleService.cardArticleEntity = articleRepo as any;
localeService = new CardLocaleService();
localeService.cardLocaleEntity = localeRepo as any;
calendarService = new CardCalendarService();
portalService = new CardPortalService();
portalService.cardInfoEntity = cardRepo as any;
portalService.cardBentoEntity = bentoRepo as any;
CardPortalService.clearAllCache();
nfcAntiTamperService = new NfcAntiTamperService();
const e2eAuditLogs: any[] = [];
nfcAntiTamperService.setAuditRepository({
save: (log: any) => { e2eAuditLogs.push(log); },
list: () => [...e2eAuditLogs],
});
crmRadarService = new CrmRadarService();
crmRadarService.crmTrackEntity = trackRepo as any;
crmRadarPushService = new CrmRadarPushService();
aiConciergeService = new AiConciergeService();
aiConciergeService.aiConciergeEntity = conciergeRepo as any;
aiKnowledgeService = new AiKnowledgeService();
aiGlossaryService = new AiGlossaryService();
aiGlossaryService.aiGlossaryEntity = glossaryRepo as any;
aiChatService = new AiChatService();
crmLeadScoreService = new CrmLeadScoreService();
crmSyncService = new CrmSyncService();
crmLeadPoolService = new CrmLeadPoolService();
crmLeadPoolService.crmLeadEntity = leadRepo as any;
crmLeadPoolService.crmLeadAuditEntity = leadAuditRepo as any;
teamHandoverService = new TeamHandoverService();
teamHandoverService.cardInfoEntity = cardRepo as any;
teamHandoverService.teamHandoverLogEntity = handoverLogRepo as any;
teamHandoverService.crmLeadEntity = leadRepo as any;
teamHandoverService.crmLeadAuditEntity = leadAuditRepo as any;
nfcDeviceService = new NfcDeviceService();
nfcDeviceService.nfcDeviceEntity = nfcDeviceRepo as any;
nfcRuleService = new NfcRuleService();
nfcRuleService.nfcRuleEntity = nfcRuleRepo as any;
nfcSecurityService = new NfcSecurityService();
nfcSecurityService.nfcDeviceEntity = nfcDeviceRepo as any;
nfcBatchExportService = new NfcBatchExportService();
teamMembersService = new TeamMembersService();
teamMembersService.baseSysDepartmentEntity = deptRepo as any;
teamMembersService.baseSysUserEntity = userRepo as any;
teamMembersService.cardInfoEntity = cardRepo as any;
teamDomainService = new TeamDomainService();
teamDomainService.teamDomainEntity = domainRepo as any;
teamStripeService = new TeamStripeService();
teamStripeService.teamSubscriptionEntity = subRepo as any;
teamStripeService.webhookSecret = 'whsec_e2e_live_test_secret_key_888';
teamQuotaService = new TeamQuotaService();
teamQuotaService.cardInfoEntity = cardRepo as any;
teamQuotaService.baseSysUserEntity = userRepo as any;
teamQuotaService.nfcDeviceEntity = nfcDeviceRepo as any;
teamInvoiceService = new TeamInvoiceService();
teamInvoiceService.teamInvoiceEntity = invoiceRepo as any;
// 基础全局种子变量
sharedTenantId = 1001;
sharedSalesUserId = 801;
sharedSuccessorUserId = 802;
sharedNfcUid = '04:A1:B2:C3:D4:E5:F6';
sharedNfcSecret = 'k9#mZ$8vP!xL2@qR5*wY8^uN1(bC4)eT';
sharedVisitorId = 'vis_dubai_investor_99';
});
// =========================================================================
// 主干流 1:微官网搭建与展品套件 (P01 ~ P08)
// 名片 ➔ Bento 布局 ➔ 案例作品 ➔ 博客发布 ➔ 双语发音 ➔ RTL 多语言 ➔ ICS 日历 ➔ SEO OG
// =========================================================================
describe('主干流 1: 微官网搭建与展品套件 (Points 01-08)', () => {
it('P01: 创建名片基础档案与发布状态', async () => {
const newCard = await cardRepo.save({
tenantId: sharedTenantId,
userId: sharedSalesUserId,
slug: 'alex-zheng',
name: 'Alex Zheng',
title: 'VP of Global Expansion',
company: 'OmniCard Global Inc.',
slogan: 'Empowering Global Business Connections',
avatarUrl: 'https://cdn.omnicard.ai/avatars/alex.png',
phone: '+86-13800138000',
email: 'alex@omnicard.ai',
whatsapp: '+8613800138000',
linkedin: 'https://linkedin.com/in/alex-zheng',
timezone: 'Asia/Shanghai',
audioPronunciationUrl: 'https://cdn.omnicard.ai/audio/alex-pronunciation.mp3',
status: 'published',
themeCode: 'corporate_navy',
remark: '内部高管名片,国际业务核心',
internalConfig: '{"rateLimit": 1000, "debug": false}',
});
expect(newCard.id).toBeDefined();
expect(newCard.slug).toBe('alex-zheng');
expect(newCard.status).toBe('published');
sharedCardId = newCard.id;
sharedCardSlug = newCard.slug;
});
it('P02: Bento Grid 便当盒栅格多维布局编排与物理尺寸自动约束', async () => {
const layoutBlocks = [
{
type: 'hero' as BentoBlockType,
colSpan: 2 as BentoColSpan,
rowSpan: 1 as BentoRowSpan,
title: '个人简介与身份卡',
content: '专注于中东与欧洲数字商务合作',
status: 1,
},
{
type: 'social' as BentoBlockType,
colSpan: 1 as BentoColSpan,
rowSpan: 1 as BentoRowSpan,
title: '商务社交矩阵',
content: 'WhatsApp & LinkedIn',
status: 1,
},
{
type: 'case' as BentoBlockType,
colSpan: 2 as BentoColSpan,
rowSpan: 2 as BentoRowSpan,
title: '新能源国际合作案例',
content: '协助头部光伏企业落地阿布扎比',
status: 1,
},
{
type: 'article' as BentoBlockType,
colSpan: 1 as BentoColSpan,
rowSpan: 1 as BentoRowSpan,
title: '2026全球数字名片白皮书',
content: '深度解读全球智能名片获客漏斗',
status: 1,
},
];
// 真实调用 CardBentoService.batchSaveLayout 自动重排与持久化
const savedBlocks = await bentoService.batchSaveLayout(sharedCardId, layoutBlocks);
expect(savedBlocks).toHaveLength(4);
expect(savedBlocks[0].sortOrder).toBe(0);
expect(savedBlocks[0].type).toBe('hero');
expect(savedBlocks[1].sortOrder).toBe(1);
expect(savedBlocks[1].type).toBe('social');
// 真实从数据源验证排序
const activeBlocks = await bentoService.getBlocksByCardId(sharedCardId, { status: 1 });
expect(activeBlocks.length).toBe(4);
expect(activeBlocks[2].type).toBe('case');
});
it('P03: 展厅案例作品集持久化与 KPI 指标挂载', async () => {
const demoCase = await casesRepo.save({
cardId: sharedCardId,
title: '阿联酋新能源峰会智能签到',
clientName: 'Gulf Solar Tech',
metricsKpi: '触碰转化率 +320%',
coverImage: 'https://cdn.omnicard.ai/cases/gulf-solar.jpg',
mediaUrl: 'https://cdn.omnicard.ai/videos/summit-recap.mp4',
status: 1,
sortOrder: 1,
});
expect(demoCase.id).toBeDefined();
expect(demoCase.metricsKpi).toBe('触碰转化率 +320%');
});
it('P04: 博客文章发布与 SEO OpenGraph 社交卡片自动提炼', async () => {
const rawHtmlContent = `
<article>
<h1>全球获客新引擎:2026 数字名片实战洞察</h1>
<p>在全球数字化与国际商务展会场景中,传统的纸质名片不仅浪费印刷成本,且存在高达 88% 的即时丢弃率。通过引入具备 NFC 极速碰触、多语言自适应以及 24 小时 AI 自动留资的智能微官网,销售代表能够在 30 秒内建立品牌信任,并让每一位国际客商的浏览轨迹实时映射在销售雷达上,实现端到端的精准线索转化。</p>
</article>
`;
const articleEntity = await articleRepo.save({
cardId: sharedCardId,
title: '全球获客新引擎:2026 数字名片实战洞察',
content: rawHtmlContent,
coverImage: 'https://cdn.omnicard.ai/articles/og-banner.png',
isPublished: true,
});
// 真实调用 CardArticleService 提取 OpenGraph 社交卡片元数据
const ogMeta = articleService.generateOgMeta(articleEntity);
expect(ogMeta.title).toBe(articleEntity.title);
expect(ogMeta.image).toBe('https://cdn.omnicard.ai/articles/og-banner.png');
expect(ogMeta.description).toContain('在全球数字化与国际商务展会场景中');
expect(ogMeta.description.length).toBeLessThanOrEqual(160);
expect(ogMeta.description).not.toContain('<p>');
});
it('P05: 双语人名读音与语音条配置真实性校验', async () => {
const card = await cardRepo.findOneBy({ id: sharedCardId });
expect(card).not.toBeNull();
expect(card?.audioPronunciationUrl).toBe(
'https://cdn.omnicard.ai/audio/alex-pronunciation.mp3'
);
expect(card?.audioPronunciationUrl).toMatch(/\.mp3$/i);
});
it('P06: RTL 多语言阿语分支独立配置与检索 (ar-AE)', async () => {
const arabicLocale = await localeService.upsert({
cardId: sharedCardId,
locale: 'ar-AE',
name: 'أليكس تشنغ',
title: 'نائب الرئيس للتوسع العالمي',
company: 'أومني كارد جلوبال',
slogan: 'خبير في الحلول الرقمية الذكية للشركات العابرة للحدود',
});
expect(arabicLocale.cardId).toBe(sharedCardId);
expect(arabicLocale.locale).toBe('ar-AE');
// 真实检索阿语内容
const queried = await localeService.getByCardIdAndLocale(sharedCardId, 'ar-AE');
expect(queried).not.toBeNull();
expect(queried?.title).toBe('نائب الرئيس للتوسع العالمي');
});
it('P07: 国际商务日程 RFC 5545 ICS 日历订阅事件生成与解析', () => {
const meetingStart = new Date('2026-10-15T10:00:00.000Z');
const meetingEnd = new Date('2026-10-15T11:00:00.000Z');
const icsString = generateIcsEvent({
uid: 'omnicard-meet-20261015-001@omnicard.ai',
summary: 'OmniCard AI 商务合作洽谈会 (Alex Zheng & Dubai Partner)',
description: '探讨中东区域独家分销与 NFC 智能硬件批量发行方案。\n请提前 5 分钟接入。',
location: '迪拜世界贸易中心 (DWTC) 会议厅 3B',
startTime: meetingStart,
endTime: meetingEnd,
organizer: {
name: 'Alex Zheng',
email: 'alex@omnicard.ai',
},
url: 'https://omnicard.ai/p/alex-zheng',
});
expect(icsString).toContain('BEGIN:VCALENDAR');
expect(icsString).toContain('BEGIN:VEVENT');
expect(icsString).toContain('UID:omnicard-meet-20261015-001@omnicard.ai');
expect(icsString).toContain('SUMMARY:OmniCard AI 商务合作洽谈会');
expect(icsString).toContain('END:VEVENT');
expect(icsString).toContain('END:VCALENDAR');
// 真实反向解析校验
const parsed = parseIcsEvent(icsString);
expect(parsed.uid).toBe('omnicard-meet-20261015-001@omnicard.ai');
expect(parsed.summary).toContain('Alex Zheng & Dubai Partner');
expect(parsed.location).toContain('DWTC');
});
it('P08: 访客端公开微官网全景聚合输出与敏感信息脱敏校验', async () => {
const portalDetail = await portalService.getPortalDetail(sharedCardSlug, 'ar-AE');
expect(portalDetail.id).toBe(sharedCardId);
expect(portalDetail.name).toBe('Alex Zheng');
expect(portalDetail.slug).toBe('alex-zheng');
expect(portalDetail.audioPronunciationUrl).toBeDefined();
expect(portalDetail.bentoBlocks.length).toBe(4);
expect(portalDetail.suggestedLocale).toBe('ar-AE');
// 安全脱敏校验:内部机密字段严禁暴露给访客前端
expect((portalDetail as any).internalConfig).toBeUndefined();
expect((portalDetail as any).remark).toBeUndefined();
expect((portalDetail as any).userId).toBeUndefined();
});
});
// =========================================================================
// 主干流 2:触卡与访客雷达 (P09 ~ P12)
// NFC 防伪验签 ➔ 访客雷达埋点 ➔ 国家分布聚合 ➔ 实时推送
// =========================================================================
describe('主干流 2: 触卡与访客雷达 (Points 09-12)', () => {
it('P09: NFC 实体卡动态防伪 HMAC-SHA256 签名生成、验签与防重放', () => {
// 1. 生成动态防伪令牌 (时间戳 + HMAC 签名)
sharedDeviceToken = nfcAntiTamperService.generateDeviceToken(
sharedNfcUid,
sharedCardId,
sharedNfcSecret
);
expect(sharedDeviceToken).toContain('.');
const [ts, sig] = sharedDeviceToken.split('.');
expect(Number(ts)).toBeGreaterThan(0);
expect(sig).toHaveLength(64);
// 2. 真实首次验签通过
const validRes = nfcAntiTamperService.verifyDeviceToken(
sharedNfcUid,
sharedCardId,
sharedDeviceToken,
sharedNfcSecret
);
expect(validRes.valid).toBe(true);
// 3. 防重放攻击测试:相同令牌二次使用应直接拦截
const replayRes = nfcAntiTamperService.verifyDeviceToken(
sharedNfcUid,
sharedCardId,
sharedDeviceToken,
sharedNfcSecret
);
expect(replayRes.valid).toBe(false);
expect(replayRes.reason).toMatch(/重放|已使用/);
// 4. 恶意篡改拦截:篡改 UID 进行验签被拦截并入安全审计日志
const fakeToken = nfcAntiTamperService.generateDeviceToken(
sharedNfcUid,
sharedCardId,
sharedNfcSecret
);
const tamperedRes = nfcAntiTamperService.verifyDeviceToken(
'04:FF:FF:FF:FF:FF:FF',
sharedCardId,
fakeToken,
sharedNfcSecret
);
expect(tamperedRes.valid).toBe(false);
expect(tamperedRes.reason).toMatch(/签名无效|篡改/);
const logs = nfcAntiTamperService.getAuditLogs();
expect(logs.length).toBeGreaterThanOrEqual(2);
});
it('P10: 访客雷达实时足迹埋点与 IP 地理位置推断 (recordTrack)', async () => {
// 模拟迪拜买家触碰 NFC 卡后产生的浏览埋点
const track1 = await crmRadarService.recordTrack({
cardId: sharedCardId,
visitorId: sharedVisitorId,
ip: '194.170.1.1',
country: 'United Arab Emirates',
city: 'Dubai',
touchType: 'nfc',
stayDurationMs: 320000, // 320 秒 (深度停留)
scrollDepth: 95,
viewedBlocks: ['hero', 'social', 'case', 'article'],
tenantId: sharedTenantId,
});
expect(track1.id).toBeDefined();
expect(track1.touchType).toBe('nfc');
expect(track1.country).toBe('United Arab Emirates');
expect(track1.stayDurationMs).toBe(320000);
// 模拟另外两名国际访客浏览 (新加坡与美国)
await crmRadarService.recordTrack({
cardId: sharedCardId,
visitorId: 'vis_sg_01',
ip: '202.166.1.1',
country: 'Singapore',
city: 'Singapore',
touchType: 'qr',
stayDurationMs: 150000,
scrollDepth: 70,
viewedBlocks: ['hero', 'case'],
tenantId: sharedTenantId,
});
await crmRadarService.recordTrack({
cardId: sharedCardId,
visitorId: 'vis_us_02',
ip: '8.8.8.8',
country: 'United States',
city: 'Mountain View',
touchType: 'link',
stayDurationMs: 45000,
scrollDepth: 50,
viewedBlocks: ['hero'],
tenantId: sharedTenantId,
});
});
it('P11: 全球雷达热力分布与多国访客聚合统计', async () => {
const distribution = await crmRadarService.getCountryDistribution(sharedCardId);
expect(distribution).toHaveLength(3);
// 验证国家与人次统计
const countries = distribution.map(d => d.country);
expect(countries).toContain('United Arab Emirates');
expect(countries).toContain('Singapore');
expect(countries).toContain('United States');
// 验证平均停留时长最高者为阿联酋访客 (320000ms)
const uaeItem = distribution.find(d => d.country === 'United Arab Emirates');
expect(uaeItem?.avgStayDurationMs).toBe(320000);
expect(uaeItem?.percentage).toBeCloseTo(33.33, 0);
});
it('P12: 实时雷达 Push 广播通知流订阅与派发', done => {
const sampleTrack: RadarTrack = {
id: 99,
tenantId: sharedTenantId,
cardId: sharedCardId,
visitorId: sharedVisitorId,
ip: '194.170.1.1',
country: 'United Arab Emirates',
city: 'Dubai',
touchType: 'nfc',
stayDurationMs: 320000,
scrollDepth: 95,
viewedBlocks: ['hero', 'case'],
createdAt: new Date().toISOString(),
};
// 销售管家端订阅该名片的雷达事件
crmRadarPushService.subscribe(sharedCardId, (payload: RadarPushPayload) => {
expect(payload.event).toBe('radar.visitor');
expect(payload.cardId).toBe(sharedCardId);
expect(payload.country).toBe('United Arab Emirates');
expect(payload.touchType).toBe('nfc');
expect(payload.stayDurationMs).toBe(320000);
done();
});
// 触发雷达广播推送
crmRadarPushService.publish(sampleTrack);
});
});
// =========================================================================
// 主干流 3:智能服务与在线顾问 (P13 ~ P17)
// 人设配置 ➔ RAG 切片与沙盒召回 ➔ 术语库润色 ➔ 对话与留资卡片
// =========================================================================
describe('主干流 3: 智能服务与在线顾问 (Points 13-17)', () => {
it('P13: 名片 AI 顾问人设、基调与留资触发阈值配置', async () => {
const config = await aiConciergeService.saveByCardId(sharedCardId, {
personaType: 'trade_expert',
welcomeMsg: '您好!我是 Alex 的 AI 商务顾问,请问有什么可以协助您的?',
triggerLeadThreshold: 75,
promptTemplate: 'You are an export trade consultant specialized in clean energy solutions.',
toneStyle: 'professional',
isEnabled: true,
});
expect(config.cardId).toBe(sharedCardId);
expect(config.personaType).toBe('trade_expert');
expect(config.triggerLeadThreshold).toBe(75);
const queried = await aiConciergeService.getByCardId(sharedCardId);
expect(queried?.welcomeMsg).toContain('Alex 的 AI 商务顾问');
});
it('P14: 知识库 RAG 滑动窗口文本分块与重叠切片算法', () => {
const rawDocText = `
OmniCard AI 致力于为全球商务团队提供次世代数字名片与微官网解决方案。
支持中东、欧洲及北美地区的主流商务支付与 CRM 系统无缝互联。
我们针对新能源、智能硬件及全球电商客户提供定制化的展会获客方案,
配合实体 NFC 智能卡片,可实现秒级留资并自动将高意向线索同步至公海池。
`.trim();
const chunks = aiKnowledgeService.splitTextToChunks(rawDocText, 50, 15);
expect(chunks.length).toBeGreaterThan(1);
expect(chunks[0].index).toBe(0);
expect(chunks[0].length).toBeLessThanOrEqual(50);
// 验证滑动重叠覆盖
expect(chunks[0].content.slice(-15)).toBe(chunks[1].content.slice(0, 15));
});
it('P15: 知识库沙盒召回模拟与关键词相似度排序', () => {
const testChunks = [
{
index: 0,
content: '公司主营产品包含新能源储能电站与光伏逆变器全球解决方案。',
length: 30,
},
{
index: 1,
content: '在中东阿联酋及沙特地区,我们提供 7x24 小时的当地运维支持。',
length: 31,
},
{
index: 2,
content: '欧洲市场支持符合 GDPR 的数据合规导出与多语言商务发票。',
length: 29,
},
];
// 访客搜索:“中东阿联酋支持”
const query = '中东 阿联酋 支持';
const results = aiKnowledgeService.simulateRetrieval(query, testChunks, 2);
expect(results).toHaveLength(2);
// 第一个命中必须是第 1 块 (中东阿联酋运维支持)
expect(results[0].index).toBe(1);
expect(results[0].content).toContain('中东阿联酋');
expect(results[0].score).toBeGreaterThan(0.5);
});
it('P16: 行业专有术语库 (Glossary) 导入与国际多语言润色替换', async () => {
const importRes = await aiGlossaryService.batchImport([
{
sourceTerm: 'digital business card',
targetTerm: '智能数字名片',
sourceLang: 'en',
targetLang: 'zh-CN',
domain: 'tech',
notes: '品牌核心定义',
},
{
sourceTerm: 'NFC card fleet',
targetTerm: 'NFC 智能卡舰队',
sourceLang: 'en',
targetLang: 'zh-CN',
domain: 'hardware',
},
]);
expect(importRes.inserted).toBe(2);
// 真实调用术语库替换
const replaced = await aiGlossaryService.replaceTerms(
'Welcome to our digital business card solution',
'zh-CN',
'tech'
);
expect(replaced).toContain('智能数字名片');
});
it('P17: AI 顾问流式对话意图识别与高意向留资卡片引导', async () => {
// 访客询问价格与合作方案
const reply = await aiChatService.ask({
cardId: sharedCardId,
locale: 'zh-CN',
messages: [
{ role: 'user', content: '请问你们 Enterprise 企业版怎么报价?我们想洽谈合作。' },
],
});
expect(reply.shouldCollectLead).toBe(true);
expect(reply.reply).toContain('留下您的姓名与联系方式');
});
});
// =========================================================================
// 主干流 4:客户 CRM 转化 (P18 ~ P22)
// 多维加权意向算分 ➔ 外部 CRM 推送 ➔ 销售公海池流转 ➔ 离职交接
// =========================================================================
describe('主干流 4: 客户 CRM 转化 (Points 18-22)', () => {
it('P18: 多维加权意向算分引擎 (CrmLeadScoreService)', () => {
// 结合前面访客行为计算加权得分:
// 停留 >300s (stayScore: 40) + 浏览4个模块 (moduleScore: 20) + 交互全触发 (interactionScore: 40) = 100分
const scoreResult = crmLeadScoreService.calculateScore({
stayDurationSeconds: 320,
viewedBlockCount: 4,
clickedWhatsApp: true,
downloadedVCard: true,
submittedLead: true,
});
expect(scoreResult.score).toBe(100);
expect(scoreResult.level).toBe('Hot');
expect(scoreResult.breakdown.stayScore).toBe(40);
expect(scoreResult.breakdown.moduleScore).toBe(20);
expect(scoreResult.breakdown.interactionScore).toBe(80);
});
it('P19: 沉淀高意向潜客全景画像建档', async () => {
const lead = await leadRepo.save({
cardId: sharedCardId,
userId: sharedSalesUserId, // 初始归属于销售 Alex
name: 'Tariq Al-Mansoor',
company: 'Gulf Horizon Trading LLC',
phone: '+971-50-9876543',
email: 'tariq@gulfhorizon.ae',
intentScore: 95,
status: 'hot',
source: 'nfc',
isPool: false,
lastFollowUpTime: new Date(),
notes: '阿联酋大客户,重点推进企业微官网采购',
});
expect(lead.id).toBeDefined();
expect(lead.intentScore).toBe(95);
expect(lead.status).toBe('hot');
sharedLeadId = lead.id;
});
it('P20: 国际主流外部 CRM (HubSpot & Salesforce) 格式映射与 Webhook 签名生成', () => {
const currentLead = leadRepo.data.find(l => l.id === sharedLeadId);
expect(currentLead).toBeDefined();
// 1. HubSpot Contacts v3 契约转换
const hubspotPayload = crmSyncService.formatToHubSpot(currentLead!);
expect(hubspotPayload.properties.email).toBe('tariq@gulfhorizon.ae');
expect(hubspotPayload.properties.firstname).toBe('Tariq');
expect(hubspotPayload.properties.lastname).toBe('Al-Mansoor');
expect(hubspotPayload.properties.company).toBe('Gulf Horizon Trading LLC');
expect(hubspotPayload.properties.lifecyclestage).toBe('lead');
// 2. Salesforce Lead sObject 契约转换
const sfPayload = crmSyncService.formatToSalesforce(currentLead!);
expect(sfPayload.FirstName).toBe('Tariq');
expect(sfPayload.LastName).toBe('Al-Mansoor');
expect(sfPayload.Email).toBe('tariq@gulfhorizon.ae');
expect(sfPayload.Status).toBe('Open - Not Contacted');
// 3. Webhook 签名生成与验证 (HMAC-SHA256)
const webhookSecret = 'crm_webhook_secret_key_999';
const webhookSignature = crmSyncService.generateSignature(
JSON.stringify(currentLead),
webhookSecret
);
expect(webhookSignature).toHaveLength(64);
});
it('P21: 团队公海池流转与 CAS 销售抢单认领', async () => {
// 1. 销售将线索释放退回到企业公海池
const poolResult = await crmLeadPoolService.recycleToPool(
sharedLeadId,
'转交团队公海供区域销售抢单'
);
expect(poolResult.isPool).toBe(true);
expect(poolResult.userId).toBeNull();
// 验证产生审计日志
const auditsAfterPool = leadAuditRepo.data.filter(a => a.leadId === sharedLeadId);
expect(auditsAfterPool.some(a => a.action === 'recycle')).toBe(true);
// 2. 另一位新接手销售 (sharedSuccessorUserId) 抢单捞取线索
const claimed = await crmLeadPoolService.claimLead(sharedLeadId, sharedSuccessorUserId);
expect(claimed.isPool).toBe(false);
expect(claimed.userId).toBe(sharedSuccessorUserId);
// 验证抢单后不可被重复认领
await expect(
crmLeadPoolService.claimLead(sharedLeadId, 9999)
).rejects.toThrow(/已被其他销售认领|不在公海池/);
});
it('P22: 员工离职资产交接工单、短链安全重定向与历史线索迁移', async () => {
// 模拟原销售 Alex (userId: 801) 离职,交接给接任者 (userId: 802)
// 名片短链重定向到接替人的名片,历史线索全部迁移
const handoverLog = await teamHandoverService.executeHandover({
fromUserId: sharedSalesUserId,
toUserId: sharedSuccessorUserId,
cardId: sharedCardId,
redirectType: 'card',
redirectTarget: 'successor-card-slug',
leadAction: 'transfer',
operatorId: 1,
remark: 'Alex 正常离职交接,客户与名片一键转接给 Bob',
});
expect(handoverLog.id).toBeDefined();
expect(handoverLog.handoverNo).toMatch(/^HO\d+/);
expect(handoverLog.status).toBe('completed');
// 验证名片重定向配置生效
const updatedCard = await cardRepo.findOneBy({ id: sharedCardId });
expect(updatedCard?.redirectType).toBe('card');
expect(updatedCard?.redirectTarget).toBe('successor-card-slug');
expect(updatedCard?.userId).toBe(sharedSuccessorUserId);
});
});
// =========================================================================
// 主干流 5:NFC 硬件生命周期 (P23 ~ P26)
// 设备入库 ➔ 场景规则匹配 ➔ 远程挂失解冻 ➔ 批量制卡 Zip 导出
// =========================================================================
describe('主干流 5: NFC 硬件生命周期 (Points 23-26)', () => {
it('P23: NFC 实体卡设备台账入库与绑定', async () => {
const device = await nfcDeviceRepo.save({
tenantId: sharedTenantId,
uid: sharedNfcUid,
cardId: sharedCardId,
batchNo: 'EXPO-2026-DXB',
material: 'metal',
status: 'bound',
sceneMode: 'business',
});
expect(device.id).toBeDefined();
expect(device.uid).toBe(sharedNfcUid);
expect(device.material).toBe('metal');
expect(device.status).toBe('bound');
});
it('P24: 场景化动态时间规则匹配与名片路由 (NfcRuleService)', async () => {
// 配置规则:工作日 09:00 - 18:00 路由到商务微官网 (cardId: 101)
// 其他非工作时段路由到个人社交名片 (cardId: 202)
await nfcRuleRepo.save({
tenantId: sharedTenantId,
ruleName: '日间商务展会模式',
targetCardId: sharedCardId,
sceneMode: 'business',
startTime: '09:00',
endTime: '18:00',
enabled: true,
});
await nfcRuleRepo.save({
tenantId: sharedTenantId,
ruleName: '晚间晚宴社交模式',
targetCardId: 202,
sceneMode: 'social',
startTime: '19:00',
endTime: '23:59',
enabled: true,
});
// 1. 模拟上午 11:30 触碰卡片
const dayResult = await nfcRuleService.resolveScene({
tenantId: sharedTenantId,
now: '2026-09-21T11:30:00',
});
expect(dayResult).not.toBeNull();
expect(dayResult?.targetCardId).toBe(sharedCardId);
expect(dayResult?.sceneMode).toBe('business');
// 2. 模拟晚上 21:00 触碰卡片
const nightResult = await nfcRuleService.resolveScene({
tenantId: sharedTenantId,
now: '2026-09-21T21:00:00',
});
expect(nightResult).not.toBeNull();
expect(nightResult?.targetCardId).toBe(202);
expect(nightResult?.sceneMode).toBe('social');
});
it('P25: NFC 实体卡远程一键挂失冻结与解冻恢复 (NfcSecurityService)', async () => {
// 1. 模拟卡片在展会遗失,远程一键挂失冻结
const reportLostRes = await nfcSecurityService.freeze({ uid: sharedNfcUid });
expect(reportLostRes.status).toBe('suspended');
// 查库验证设备状态被冻结为 suspended
const lostDevice = await nfcDeviceRepo.findOneBy({ uid: sharedNfcUid });
expect(lostDevice?.status).toBe('suspended');
// 2. 找回卡片后远程解冻,恢复绑定正常状态 (active)
const recoverRes = await nfcSecurityService.unfreeze({ uid: sharedNfcUid });
expect(recoverRes.status).toBe('active');
const activeDevice = await nfcDeviceRepo.findOneBy({ uid: sharedNfcUid });
expect(activeDevice?.status).toBe('active');
});
it(
'P26: 展会批量制卡清单 CSV 与二维码离线 ZIP 压缩包归档导出',
async () => {
const mockBatchDevices = [
{
uid: sharedNfcUid,
slug: sharedCardSlug,
cardTitle: 'Alex Zheng - VP Global',
},
{
uid: '04:11:22:33:44:55:66',
slug: 'bob-engineer',
cardTitle: 'Bob Successor - Lead Dev',
},
];
const batchResult = await nfcBatchExportService.generateBatchPack(
'EXPO-2026-DXB',
mockBatchDevices
);
expect(batchResult.totalCount).toBe(2);
expect(Buffer.isBuffer(batchResult.zipBuffer)).toBe(true);
expect(batchResult.zipBuffer.length).toBeGreaterThan(0);
// 解压验证 ZIP 归档包结构
const zip = new AdmZip(batchResult.zipBuffer);
const zipEntries = zip.getEntries();
expect(zipEntries.length).toBeGreaterThanOrEqual(3);
// 校验存在 manifest.csv 清单文件
const manifestEntry = zipEntries.find(e => e.entryName.includes('manifest.csv'));
expect(manifestEntry).toBeDefined();
const csvText = manifestEntry?.getData().toString('utf8');
expect(csvText).toContain('出厂防伪码');
expect(csvText).toContain(sharedNfcUid);
expect(csvText).toContain('Alex Zheng - VP Global');
}, 15000);
});
// =========================================================================
// 主干流 6:多租户与商业化 (P27 ~ P30)
// 组织架构与名片分配 ➔ 独立域名与 SSL 校验 ➔ Stripe 订阅 Webhook ➔ 坐席配额核销 ➔ 国际多币种发票
// =========================================================================
describe('主干流 6: 多租户与商业化 (Points 27-30)', () => {
it('P27: 组织架构部门树多层级递归与成员名片分配 (TeamMembersService)', async () => {
// 1. 初始化部门树结构:总部 -> 国际销售中心 -> 中东拓展组
const headDept = await deptRepo.save({
name: '全球总部',
parentId: null as any,
orderNum: 1,
});
const salesDept = await deptRepo.save({
name: '全球拓展事业群',
parentId: headDept.id,
orderNum: 2,
});
const meDept = await deptRepo.save({
name: '中东战区组',
parentId: salesDept.id,
orderNum: 3,
});
// 2. 获取树形结构验证
const tree = await teamMembersService.getDepartmentTree();
expect(tree.length).toBeGreaterThanOrEqual(1);
const rootNode = tree.find(t => t.id === headDept.id);
expect(rootNode).toBeDefined();
expect(rootNode?.children?.length).toBeGreaterThanOrEqual(1);
// 3. 将销售成员 Bob 挂载到中东战区组并分配专属名片
await userRepo.save({
id: sharedSuccessorUserId,
name: 'Bob Successor',
username: 'bob_global',
departmentId: meDept.id,
status: 1,
});
const assigned = await teamMembersService.assignCard({
memberId: sharedSuccessorUserId,
cardId: sharedCardId,
});
expect(assigned.cardId).toBe(sharedCardId);
expect(assigned.memberId).toBe(sharedSuccessorUserId);
});
it('P28: 企业独立二级域名绑定向导与 SSL 证书有效性状态检测 (TeamDomainService)', async () => {
const domainName = 'card.omnicard-global.com';
// 1. 域名规范严格正则校验 (拦截非法端口、协议与保留地址)
const validCheck = teamDomainService.validateDomainFormat(domainName);
expect(validCheck.valid).toBe(true);
const invalidCheck = teamDomainService.validateDomainFormat('http://bad.domain.com:8080');
expect(invalidCheck.valid).toBe(false);
// 2. 绑定域名与生成 CNAME 解析目标
const bound = await domainRepo.save({
tenantId: sharedTenantId,
customDomain: domainName,
cnameTarget: 'cname.omnicard.ai',
sslStatus: 'pending',
});
expect(bound.customDomain).toBe(domainName);
expect(bound.cnameTarget).toBe('cname.omnicard.ai');
// 3. SSL 证书校验检测
const sslResult = await teamDomainService.checkSslCertificate(domainName, 443, {
mockCert: {
issuer: "Let's Encrypt Authority X3",
validFrom: new Date(Date.now() - 30 * 86400000).toISOString(),
validTo: new Date(Date.now() + 60 * 86400000).toISOString(),
authorized: true,
},
});
expect(sslResult.valid).toBe(true);
expect(sslResult.daysRemaining).toBeGreaterThan(0);
expect(sslResult.issuer).toBe("Let's Encrypt Authority X3");
});
it('P29: Stripe 国际信用卡结算会话创建与 Webhook 自动化入账 (TeamStripeService)', async () => {
// 1. 创建 Enterprise 年付结算会话(需密钥 + Stripe API;此处 mock axios)
const prevStripeKey = process.env.STRIPE_SECRET_KEY;
process.env.STRIPE_SECRET_KEY = 'sk_test_e2e_mock_key';
const axios = require('axios');
const axiosPostSpy = jest.spyOn(axios, 'post').mockResolvedValue({
data: {
id: 'cs_test_e2e_enterprise_yearly',
url: 'https://checkout.stripe.com/c/pay/cs_test_e2e_enterprise_yearly',
},
});
let session: Awaited<
ReturnType<typeof teamStripeService.createCheckoutSession>
>;
try {
session = await teamStripeService.createCheckoutSession({
tenantId: sharedTenantId,
plan: 'enterprise',
billingCycle: 'yearly',
});
} finally {
axiosPostSpy.mockRestore();
if (prevStripeKey === undefined) {
delete process.env.STRIPE_SECRET_KEY;
} else {
process.env.STRIPE_SECRET_KEY = prevStripeKey;
}
}
expect(session.amount).toBe(99000); // $990.00 / 年
expect(session.sessionId).toMatch(/^cs_test_/);
expect(session.url).toContain(session.sessionId);
// 2. 模拟 Stripe 官方 Webhook 支付成功通知
const webhookPayload: StripeWebhookEvent = {
id: 'evt_stripe_payment_2026_001',
type: 'checkout.session.completed',
created: Math.floor(Date.now() / 1000),
data: {
object: {
id: session.sessionId,
client_reference_id: String(sharedTenantId),
customer: 'cus_stripe_dubai_001',
subscription: 'sub_enterprise_live_001',
metadata: {
tenantId: String(sharedTenantId),
plan: 'enterprise',
billingCycle: 'yearly',
},
},
},
};
// 签名生成与真实 Webhook 处理
const headerTimestamp = Math.floor(Date.now() / 1000);
const rawPayloadString = JSON.stringify(webhookPayload);
const hmac = crypto.createHmac('sha256', teamStripeService.webhookSecret);
hmac.update(`${headerTimestamp}.${rawPayloadString}`, 'utf8');
const signature = hmac.digest('hex');
const stripeSigHeader = `t=${headerTimestamp},v1=${signature}`;
const verified = teamStripeService.verifyWebhookSignature(
rawPayloadString,
stripeSigHeader
);
expect(verified).toBe(true);
// 处理入账流转
const handleResult = await teamStripeService.handleWebhookEvent(webhookPayload);
expect(handleResult.success).toBe(true);
expect(handleResult.status).toBe('active');
expect(handleResult.plan).toBe('enterprise');
// 校验租户订阅数据已真实写入
const subscription = await subRepo.findOneBy({ tenantId: sharedTenantId });
expect(subscription?.status).toBe('active');
expect(subscription?.planType).toBe('enterprise');
});
it('P30: 坐席配额核销、80% 警戒线预警、升舱差价折算与国际多币种标准发票', async () => {
// 1. 配额检查:企业版已用 85 个坐席 / 上限 100 个 -> 触发 80% 警戒线
const quotaCheck = await teamQuotaService.checkQuota(sharedTenantId, 'seats', {
plan: 'enterprise',
currentUsed: 85,
});
expect(quotaCheck.limit).toBe(100);
expect(quotaCheck.used).toBe(85);
expect(quotaCheck.percentage).toBe(85);
expect(quotaCheck.warning).toBe(true); // 超过 80% 预警
expect(quotaCheck.exceeded).toBe(false);
// 2. 套餐升级差价折算 (Pro 升级至 Enterprise,剩余 180 天 / 全年 365 天)
const proration = teamQuotaService.calculateUpgradeProration(
'pro',
'enterprise',
180,
365
);
expect(proration.upgradeDiffAmount).toBeGreaterThan(0);
expect(proration.targetPlan).toBe('enterprise');
// 3. 国际多币种发票生成与税额精确拆解
const taxCalc = teamInvoiceService.calculateTaxAndTotals({
subtotal: 990.0,
taxRate: 0.05, // 5% 增值税
isTaxInclusive: false,
});
expect(taxCalc.subtotal).toBe(990.0);
expect(taxCalc.taxAmount).toBe(49.5);
expect(taxCalc.totalAmount).toBe(1039.5);
const invoiceDate = new Date('2026-09-20');
const invoiceNo = teamInvoiceService.generateInvoiceNumber(invoiceDate, 1);
expect(invoiceNo).toBe('INV-20260920-0001');
const invoice = await teamInvoiceService.generateInvoice(sharedTenantId, {
orderId: 'ORDER-2026-STRIPE-001',
currency: 'USD',
subtotal: 990.0,
taxRate: 0.05,
title: 'OmniCard Enterprise Annual Subscription (100 Seats)',
items: [
{
description: 'Enterprise Plan (Yearly)',
quantity: 1,
unitPrice: 990.0,
amount: 990.0,
},
],
billingInfo: {
companyName: 'Gulf Horizon Trading LLC',
taxId: 'AE-VAT-1002938481',
email: 'tariq@gulfhorizon.ae',
address: 'Downtown Dubai, UAE',
},
});
expect(invoice.id).toBeDefined();
expect(invoice.invoiceNo).toMatch(/^INV-\d{8}-\d{4}$/);
expect(invoice.currency).toBe('USD');
expect(invoice.currencySymbol).toBe('$');
expect(invoice.totalAmount).toBe(1039.5);
expect(invoice.status).toBe('paid');
expect(invoice.htmlTemplate).toContain('INV-');
expect(invoice.textTemplate).toContain('Gulf Horizon Trading LLC');
});
});
});