diff --git a/backend/test/e2e/omnicard-30-points.test.ts b/backend/test/e2e/omnicard-30-points.test.ts new file mode 100644 index 0000000..1cf3ac9 --- /dev/null +++ b/backend/test/e2e/omnicard-30-points.test.ts @@ -0,0 +1,1306 @@ +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 { + public data: T[] = []; + private currentId = 1; + + constructor(initial: Partial[] = []) { + 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 { + 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 { + 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 { + const list = await this.find(options); + return list.length > 0 ? ({ ...list[0] } as T) : null; + } + + async findOneBy(where: any): Promise { + return this.findOne({ where }); + } + + async save(entityOrEntities: any): Promise { + 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 { + 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; + let bentoRepo: MemoryRepository; + let casesRepo: MemoryRepository; + let articleRepo: MemoryRepository; + let localeRepo: MemoryRepository; + let trackRepo: MemoryRepository; + let leadRepo: MemoryRepository; + let leadAuditRepo: MemoryRepository; + let conciergeRepo: MemoryRepository; + let glossaryRepo: MemoryRepository; + let nfcDeviceRepo: MemoryRepository; + let nfcRuleRepo: MemoryRepository; + let deptRepo: MemoryRepository; + let userRepo: MemoryRepository; + let domainRepo: MemoryRepository; + let subRepo: MemoryRepository; + let invoiceRepo: MemoryRepository; + let handoverLogRepo: MemoryRepository; + + // 业务服务实例 (真实逻辑) + 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(); + bentoRepo = new MemoryRepository(); + casesRepo = new MemoryRepository(); + articleRepo = new MemoryRepository(); + localeRepo = new MemoryRepository(); + trackRepo = new MemoryRepository(); + leadRepo = new MemoryRepository(); + leadAuditRepo = new MemoryRepository(); + conciergeRepo = new MemoryRepository(); + glossaryRepo = new MemoryRepository(); + nfcDeviceRepo = new MemoryRepository(); + nfcRuleRepo = new MemoryRepository(); + deptRepo = new MemoryRepository(); + userRepo = new MemoryRepository(); + domainRepo = new MemoryRepository(); + subRepo = new MemoryRepository(); + invoiceRepo = new MemoryRepository(); + handoverLogRepo = new MemoryRepository(); + + // 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(); + + 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 = ` +
+

出海获客新引擎:2026 跨境数字名片实战洞察

+

在全球数字化出海与国际展会场景中,传统的纸质名片不仅浪费印刷成本,且存在高达 88% 的即时丢弃率。通过引入具备 NFC 极速碰触、多语言自适应以及 24 小时 AI 自动留资的智能微官网,出海销售代表能够在 30 秒内建立品牌信任,并让每一位海外客商的浏览轨迹实时映射在销售雷达上,实现端到端的精准线索转化。

+
+ `; + + 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('

'); + }); + + 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:AI 智能中枢 (P13 ~ P17) + // 人设配置 ➔ RAG 切片与沙盒召回 ➔ 术语库润色 ➔ 流式对话与留资卡片 + // ========================================================================= + describe('主干流 3: AI 智能中枢 (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); + // 验证滑动重叠覆盖 (overlap = 15) + 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'); + }); + }); + + // ========================================================================= + // 主干流 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 年付结算会话 + const session = await teamStripeService.createCheckoutSession({ + tenantId: sharedTenantId, + plan: 'enterprise', + billingCycle: 'yearly', + }); + 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'); + }); + }); +});