import { describe, expect, it, vi } from 'vitest'; import { DocumentService, EXPORT_NOTICE } from './document.service.js'; import type { PrismaService } from '../prisma/prisma.service.js'; import { ErrorCodes } from '../common/error-codes.js'; import { encrypt } from '../common/id-card-cipher.js'; import { runWithRequestContext } from '../common/context/request-context.js'; import type { RequestContext } from '../common/context/request-context.js'; const ctx: RequestContext = { tenantCode: 'T001', userId: 'u1', username: 'admin', super: false, dataScope: 'self', orgCode: null, orgCodes: [], }; function makeService( overrides: { elderFindUnique?: ReturnType; agencyFindUnique?: ReturnType; planFindMany?: ReturnType; planItemFindMany?: ReturnType; recordFindMany?: ReturnType; recordItemFindMany?: ReturnType; recordAbnormalFindMany?: ReturnType; } = {}, ) { const elderFindUnique = overrides.elderFindUnique ?? vi.fn().mockResolvedValue(null); const agencyFindUnique = overrides.agencyFindUnique ?? vi.fn().mockResolvedValue({ code: 'T001', orgName: '测试机构' }); const planFindMany = overrides.planFindMany ?? vi.fn().mockResolvedValue([]); const planItemFindMany = overrides.planItemFindMany ?? vi.fn().mockResolvedValue([]); const recordFindMany = overrides.recordFindMany ?? vi.fn().mockResolvedValue([]); const recordItemFindMany = overrides.recordItemFindMany ?? vi.fn().mockResolvedValue([]); const recordAbnormalFindMany = overrides.recordAbnormalFindMany ?? vi.fn().mockResolvedValue([]); const prisma = { client: { elder: { findUnique: elderFindUnique }, agency: { findUnique: agencyFindUnique }, plan: { findMany: planFindMany }, planItem: { findMany: planItemFindMany }, record: { findMany: recordFindMany }, recordItem: { findMany: recordItemFindMany }, recordAbnormal: { findMany: recordAbnormalFindMany }, }, }; const service = new DocumentService(prisma as unknown as PrismaService); return { service, elderFindUnique, agencyFindUnique, planFindMany, planItemFindMany, recordFindMany, recordItemFindMany, recordAbnormalFindMany, }; } describe('DocumentService.export', () => { it('老人不存在 → ELDER_NOT_FOUND', async () => { const { service, recordFindMany } = makeService(); await expect( runWithRequestContext(ctx, () => service.export('missing')), ).rejects.toMatchObject({ response: { code: ErrorCodes.ELDER_NOT_FOUND } }); expect(recordFindMany).not.toHaveBeenCalled(); }); it('组装一人一档:掩码身份证 + 日期转 YYYY-MM-DD + 计划/记录/异常 + 水印', async () => { const elder = { id: 'e1', name: '张三', gender: '1', birthDate: new Date('1950-01-01T00:00:00Z'), idCardNoEnc: encrypt('530102195001010011'), phone: '13800000000', address: '昆明市', insurNo: 'I001', careLevel: '重度', treatStatus: 'active', benefitStart: new Date('2026-01-01T00:00:00Z'), benefitEnd: null, }; const plans = [ { id: 'p1', elderId: 'e1', planPeriodStart: new Date('2026-10-01T00:00:00Z'), planPeriodEnd: new Date('2026-10-31T00:00:00Z'), careLevel: '重度', careMode: 'home_org', planStatus: 'active', }, ]; const planItems = [ { id: 'pi1', planId: 'p1', svcId: 'svc1', svcNameSnapshot: '协助翻身', freqType: 'weekly', freqValue: 3, durationMin: 30, photoTier: 'must' }, ]; const records = [ { id: 'r1', elderId: 'e1', checkinTime: new Date('2026-10-15T02:00:00Z'), checkoutTime: new Date('2026-10-15T03:00:00Z'), durationMin: 60, recordStatus: 'submitted', }, ]; const recordItems = [ { id: 'ri1', recordId: 'r1', svcId: 'svc1', svcNameSnapshot: '协助翻身', execResult: 'done', undoReason: null, extraFlag: false }, ]; const abnormals = [ { id: 'a1', elderId: 'e1', abnormalType: '跌倒', abnormalDesc: '轻微', handleStatus: 'open' }, ]; const { service, elderFindUnique, agencyFindUnique, planFindMany, planItemFindMany, recordFindMany, recordItemFindMany, recordAbnormalFindMany, } = makeService({ elderFindUnique: vi.fn().mockResolvedValue(elder), planFindMany: vi.fn().mockResolvedValue(plans), planItemFindMany: vi.fn().mockResolvedValue(planItems), recordFindMany: vi.fn().mockResolvedValue(records), recordItemFindMany: vi.fn().mockResolvedValue(recordItems), recordAbnormalFindMany: vi.fn().mockResolvedValue(abnormals), }); const res = await runWithRequestContext(ctx, () => service.export('e1', '2026-10-01', '2026-10-31')); // 定位与范围 expect(elderFindUnique).toHaveBeenCalledWith({ where: { id: 'e1' } }); expect(agencyFindUnique).toHaveBeenCalledWith({ where: { code: 'T001' } }); expect(planFindMany).toHaveBeenCalledWith({ where: { elderId: 'e1' }, orderBy: { createdAt: 'desc' } }); expect(planItemFindMany).toHaveBeenCalledWith({ where: { planId: { in: ['p1'] } } }); expect(recordFindMany).toHaveBeenCalledWith({ where: { elderId: 'e1', checkinTime: { gte: new Date('2026-10-01T00:00:00+08:00'), lte: new Date('2026-10-31T23:59:59.999+08:00'), }, }, orderBy: { checkinTime: 'desc' }, }); expect(recordItemFindMany).toHaveBeenCalledWith({ where: { recordId: { in: ['r1'] } } }); expect(recordAbnormalFindMany).toHaveBeenCalledWith({ where: { elderId: 'e1' } }); // 水印:机构名 + 时间 + 操作人(username)+ R4 文案 expect(res.watermark).toMatchObject({ orgName: '测试机构', operator: 'admin', notice: EXPORT_NOTICE, }); expect(res.watermark.exportTime).toEqual(expect.any(String)); // 老人信息:身份证掩码 + 日期转 YYYY-MM-DD expect(res.elder).toEqual({ name: '张三', gender: '1', birthDate: '1950-01-01', idCardNo: '5301**********0011', phone: '13800000000', address: '昆明市', insurNo: 'I001', careLevel: '重度', treatStatus: 'active', benefitStart: '2026-01-01', benefitEnd: null, }); // 计划 + 计划项 expect(res.plans).toEqual([ { id: 'p1', planPeriodStart: '2026-10-01', planPeriodEnd: '2026-10-31', careLevel: '重度', careMode: 'home_org', planStatus: 'active', items: [{ svcId: 'svc1', svcName: '协助翻身', freqType: 'weekly', freqValue: 3, durationMin: 30, photoTier: 'must' }], }, ]); // 记录 + 记录项(checkin/checkout 转 ISO) expect(res.records).toEqual([ { id: 'r1', checkinTime: new Date('2026-10-15T02:00:00Z').toISOString(), checkoutTime: new Date('2026-10-15T03:00:00Z').toISOString(), durationMin: 60, recordStatus: 'submitted', items: [{ svcId: 'svc1', svcName: '协助翻身', execResult: 'done', undoReason: null, extraFlag: false }], }, ]); // 异常 expect(res.abnormals).toEqual([{ id: 'a1', type: '跌倒', desc: '轻微', handleStatus: 'open' }]); }); it('无日期范围时不加 checkinTime 条件', async () => { const elder = { id: 'e1', name: '张三', gender: '1', birthDate: new Date('1950-01-01T00:00:00Z'), idCardNoEnc: encrypt('530102195001010011'), phone: null, address: '昆明市', insurNo: 'I001', careLevel: '重度', treatStatus: 'active', benefitStart: null, benefitEnd: null }; const recordFindMany = vi.fn().mockResolvedValue([]); const { service } = makeService({ elderFindUnique: vi.fn().mockResolvedValue(elder), recordFindMany, }); await runWithRequestContext(ctx, () => service.export('e1')); expect(recordFindMany).toHaveBeenCalledWith({ where: { elderId: 'e1' }, orderBy: { checkinTime: 'desc' } }); }); });