const { z } = require("zod"); const { buildBusinessValidationError } = require("../utils/businessValidationError"); const { AppError } = require("../utils/AppError"); const { buildAuditLogData } = require("../utils/audit"); const { sendData } = require("../utils/reply"); const { decryptStringOrThrow, maskBankAccount } = require("../services/crypto"); const NOTIFICATION_PREFERENCES_SCHEMA = z .object({ notificationPushEnabled: z.boolean(), }) .strict(); function parseNotificationPreferences(body) { const parsed = NOTIFICATION_PREFERENCES_SCHEMA.safeParse(body ?? {}); if (!parsed.success) { throw buildBusinessValidationError("profile.notification_preferences_invalid", parsed.error.flatten()); } return parsed.data; } function buildProfileCompleteness(employee) { const hasEmergencyContact = Boolean(employee?.emergency_contact_name && employee?.emergency_contact_phone); const hasBankAccount = Boolean( employee?.bank_account_encrypted && (employee?.bank_institution || employee?.bank_name), ); const hasAvatar = Boolean(employee?.avatar_path); const hasIdNumber = Boolean(employee?.id_number_encrypted); const completedCount = [hasEmergencyContact, hasBankAccount, hasAvatar, hasIdNumber].filter(Boolean).length; return { hasEmergencyContact, hasBankAccount, hasAvatar, hasIdNumber, score: completedCount * 25, }; } async function meRoutes(fastify) { fastify.get( "/me", { preHandler: fastify.authenticate, }, async (request, reply) => { const employee = await fastify.prisma.employee.findUnique({ where: { id: request.employee.id }, select: { id: true, employee_no: true, name: true, phone: true, email: true, department_id: true, direct_manager_id: true, status: true, emergency_contact_name: true, emergency_contact_phone: true, bank_account_encrypted: true, bank_name: true, bank_institution: { select: { id: true, code: true, name: true } }, avatar_path: true, id_number_encrypted: true, notification_push_enabled: true, roles: { select: { role_code: true } }, wecom_identity_binding: { select: { external_user_id: true, created_at: true } }, }, }); const roleCodes = (employee?.roles ?? []).map((r) => r.role_code); const profileCompleteness = buildProfileCompleteness(employee); let bankAccountMasked = null; try { const bankAccount = employee?.bank_account_encrypted ? decryptStringOrThrow(fastify.config, employee.bank_account_encrypted) : null; bankAccountMasked = bankAccount ? maskBankAccount(bankAccount) : null; } catch (error) { fastify.log.error({ err: error, requestId: request.id }, "FIELD_ENCRYPTION_KEY misconfigured"); throw new AppError({ code: "SYS_001", message: "服务器内部错误", statusCode: 500 }); } return sendData(reply, request, { id: employee?.id ?? request.employee.id, employeeNo: employee?.employee_no ?? "", name: employee?.name ?? "", phone: employee?.phone ?? "", email: employee?.email ?? "", departmentId: employee?.department_id ?? "", directManagerId: employee?.direct_manager_id ?? null, status: employee?.status ?? "active", notificationPushEnabled: employee?.notification_push_enabled ?? true, roleCodes, bankAccount: { bankName: employee?.bank_institution?.name ?? employee?.bank_name ?? null, bankInstitutionCode: employee?.bank_institution?.code ?? null, accountNumberMasked: bankAccountMasked, }, profileCompleteness, wecomBinding: employee?.wecom_identity_binding ? { externalUserId: employee.wecom_identity_binding.external_user_id, createdAt: employee.wecom_identity_binding.created_at?.toISOString?.() ?? null } : null, }); }, ); fastify.patch( "/me/notification-preferences", { preHandler: fastify.authenticate, }, async (request, reply) => { const body = parseNotificationPreferences(request.body); const employee = await fastify.prisma.employee.update({ where: { id: request.employee.id }, data: { notification_push_enabled: body.notificationPushEnabled, updated_by: request.employee.id, }, select: { id: true, notification_push_enabled: true, }, }); await fastify.prisma.auditLog.create({ data: buildAuditLogData(request, { actorId: request.employee.id, action: "EMPLOYEE_NOTIFICATION_PREFERENCE_UPDATED", targetType: "employee", targetId: employee.id, extra: { notificationPushEnabled: employee.notification_push_enabled, }, }), }); return sendData(reply, request, { notificationPushEnabled: employee.notification_push_enabled, }); }, ); } module.exports = meRoutes;