/** * Tests for Appointments.getAppointmentEvent snapshot fallback * when a removed employee's User account has been deleted. * * The fallback uses Employee.profileSnapshot fields instead of crashing. */ // Mock all external dependencies jest.mock("../../Users/Users", () => ({ __esModule: true, default: { users: { findOne: jest.fn(), }, getUserFullName: jest.fn(), }, })); jest.mock("../../Clients/Clients", () => ({ __esModule: true, default: { clients: { findOne: jest.fn(), }, getClientFullName: jest.fn(), }, })); jest.mock("../../Companies/Companies", () => ({ __esModule: true, default: { companies: { findOne: jest.fn(), }, getCompanyAddress: jest.fn(), }, })); jest.mock("../../Employees/Employee", () => ({ __esModule: true, default: { employees: { findOne: jest.fn(), }, checkPermission: jest.fn(), }, })); jest.mock("../../Services/Service", () => ({ __esModule: true, default: { services: { find: jest.fn(), findOne: jest.fn(), }, }, })); jest.mock("../../HeatMap/HeatMap", () => ({ __esModule: true, default: { getHeatMap: jest.fn(), addHeatMap: jest.fn(), }, })); jest.mock("../../Schedules/Schedules", () => ({ __esModule: true, default: { find: jest.fn(), }, })); jest.mock("../../SchedulesEnabled/SchedulesEnabled", () => ({ __esModule: true, default: { findOne: jest.fn(), }, })); jest.mock("../../SchedulesDisabled/SchedulesDisabled", () => ({ __esModule: true, default: { find: jest.fn(), }, })); jest.mock("../../SchedulesOverrides/SchedulesOverrides", () => ({ __esModule: true, default: { find: jest.fn(), }, })); jest.mock("../../Metrics/Metrics", () => ({ __esModule: true, default: { addAppointment: jest.fn(), canAddAppointment: jest.fn(), }, })); jest.mock("../../Notifications/Notifications", () => ({ __esModule: true, NotificationsManager: { sendSystemNotification: jest.fn(), sendPushNotification: jest.fn(), sendEmail: jest.fn(), }, })); jest.mock("../../SystemNotifications/SystemNotification", () => ({ __esModule: true, default: { create: jest.fn(), }, })); jest.mock("../../PlanSubscriptions/PlanSubscriptons", () => ({ __esModule: true, default: { findOne: jest.fn(), checkFeature: jest.fn(), }, })); jest.mock("../../Repeats/Repeats", () => ({ __esModule: true, default: { repeats: { find: jest.fn().mockResolvedValue([]), findOne: jest.fn(), }, existDisabledDate: jest.fn(), }, })); jest.mock("../../Discounts/Discounts", () => ({ __esModule: true, default: { getDiscountData: jest.fn().mockResolvedValue({ name: "", value: 0, type: "PERCENTAGE", code: "", }), }, })); jest.mock("../../../helpers/check", () => ({ validatePermissionsByCompany: jest.fn(), validateSessionUser: jest.fn(), })); jest.mock("../../Templates/Templates", () => ({ __esModule: true, default: { templates: { findOne: jest.fn(), }, sendEmail: jest.fn(), }, })); jest.mock("../../ClientAccounts/ClientAccount", () => ({ __esModule: true, default: { getAvailableAmount: jest.fn().mockResolvedValue(0), }, })); jest.mock("../../DiscountsEmail/DiscountsEmail", () => ({ __esModule: true, default: { findOne: jest.fn(), }, })); jest.mock("../../../index", () => ({ io: { to: jest.fn(() => ({ emit: jest.fn() })), }, })); import AppointmentsList from "../Appointments"; import UsersManager from "../../Users/Users"; import EmployeesList from "../../Employees/Employee"; import ClientsManager from "../../Clients/Clients"; import ServiceList from "../../Services/Service"; import CompaniesManager from "../../Companies/Companies"; import Templates from "../../Templates/Templates"; import PlanSubscriptionsList from "../../PlanSubscriptions/PlanSubscriptons"; import { NotificationsManager } from "../../Notifications/Notifications"; import { APPOINTMENT_NOTIFICATION_TYPE } from "../Appointments.Interface"; describe("getAppointmentEvent — historical read snapshot fallback", () => { let originalAdapter: any; beforeEach(() => { jest.clearAllMocks(); // Save and replace adapter originalAdapter = AppointmentsList.Appointments; const mockAppointmentFindOne = jest.fn(); (AppointmentsList as any).Appointments = { find: jest.fn(), findOne: mockAppointmentFindOne, AppointmentList: { findOne: mockAppointmentFindOne, find: jest.fn(), }, }; }); afterEach(() => { (AppointmentsList as any).Appointments = originalAdapter; }); it("falls back to Employee snapshot when User account is deleted", async () => { const mockAppointment = { _id: "appt-001", id: "appt-001", companyId: "company-001", clientId: "client-001", employeeId: "emp-removed", serviceId: "svc-001", start: new Date("2026-07-20T10:00:00Z"), startHour: 10, endHour: 11, dateDay: new Date("2026-07-20"), price: 100, length: 60, payment: false, comments: "", discountId: undefined, }; const mockClient = { _id: "client-001", id: "client-001", userId: "client-user-001", firstName: "Client", lastName: "Smith", }; const mockClientUser = { _id: "client-user-001", id: "client-user-001", firstName: "Client", lastName: "Smith", avatar: "client-avatar", }; // Employee exists (removed, with snapshot) const mockRemovedEmployee = { _id: "emp-removed", id: "emp-removed", companyId: "company-001", userId: "deleted-user-id", calendarColor: "#ff0000", removed: true, profileSnapshot: { firstName: "John", lastName: "Doe", email: "john@example.com", avatar: "old-avatar-url", }, }; // Mock adapter.findOne for the appointment const mockAppointmentFindOne = (AppointmentsList as any).Appointments.AppointmentList.findOne; mockAppointmentFindOne.mockResolvedValue(mockAppointment); // Mock lookups (ClientsManager.clients.findOne as jest.Mock).mockResolvedValue(mockClient); (ServiceList.services.findOne as jest.Mock).mockResolvedValue({ _id: "svc-001", name: "Haircut", description: "Professional haircut", }); // Call order for UsersManager.users.findOne (same mock as UserList): // 1. Line 1217: UserList.users.findOne (NOT awaited, but consumes a value) // 2. Line 1241: UsersManager.users.findOne for userClient // 3. Line 1253: UsersManager.users.findOne for userEmployee (UsersManager.users.findOne as jest.Mock) .mockResolvedValueOnce({ _id: "session-user" }) // line 1217: not awaited, just needs truthy .mockResolvedValueOnce(mockClientUser) // line 1241: client user .mockResolvedValueOnce(null); // line 1253: employee user DELETED → triggers fallback (EmployeesList.employees.findOne as jest.Mock).mockResolvedValue(mockRemovedEmployee); (EmployeesList as any).findById = jest.fn().mockResolvedValue({ id: "emp-removed", fullName: "John Doe", avatar: "old-avatar-url", }); const result = await AppointmentsList.getAppointmentEvent({ appointmentId: "appt-001", sessionUser: "session-user", }); // Verify snapshot fields are used for collaboratorName expect(result.collaboratorName).toBe("John Doe"); // Verify snapshot avatar is used expect(result.collaboratorAvatar).toBe("old-avatar-url"); // Verify collaboratorId comes from employee, not user expect(result.collaboratorId).toBe("emp-removed"); }); it("uses live User data when User account still exists", async () => { const mockAppointment = { _id: "appt-002", id: "appt-002", companyId: "company-001", clientId: "client-001", employeeId: "emp-active", serviceId: "svc-001", start: new Date("2026-07-20T10:00:00Z"), startHour: 10, endHour: 11, dateDay: new Date("2026-07-20"), price: 100, length: 60, payment: false, comments: "", discountId: undefined, }; const mockClient = { _id: "client-001", id: "client-001", userId: "client-user-001", firstName: "Client", lastName: "Smith", }; const mockClientUser = { _id: "client-user-001", id: "client-user-001", firstName: "Client", lastName: "Smith", avatar: "client-avatar", }; const mockActiveEmployee = { _id: "emp-active", id: "emp-active", companyId: "company-001", userId: "active-user-id", calendarColor: "#00ff00", removed: false, profileSnapshot: undefined, }; const mockActiveUser = { _id: "active-user-id", id: "active-user-id", firstName: "Jane", lastName: "Roe", avatar: "live-avatar", email: "jane@example.com", }; const mockAppointmentFindOne = (AppointmentsList as any).Appointments.AppointmentList.findOne; mockAppointmentFindOne.mockResolvedValue(mockAppointment); (ClientsManager.clients.findOne as jest.Mock).mockResolvedValue(mockClient); (ServiceList.services.findOne as jest.Mock).mockResolvedValue({ _id: "svc-001", name: "Haircut", description: "Professional haircut", }); // Call order for UsersManager.users.findOne (same mock as UserList): // 1. Line 1217: UserList.users.findOne (NOT awaited, but consumes a value) // 2. Line 1241: UsersManager.users.findOne for userClient // 3. Line 1253: UsersManager.users.findOne for userEmployee (UsersManager.users.findOne as jest.Mock) .mockResolvedValueOnce({ _id: "session-user" }) // line 1217: not awaited .mockResolvedValueOnce(mockClientUser) // line 1241: client user .mockResolvedValueOnce(mockActiveUser); // line 1253: live user exists (EmployeesList.employees.findOne as jest.Mock).mockResolvedValue(mockActiveEmployee); (EmployeesList as any).findById = jest.fn().mockResolvedValue({ id: "emp-active", fullName: "Jane Roe", avatar: "live-avatar", }); const result = await AppointmentsList.getAppointmentEvent({ appointmentId: "appt-002", sessionUser: "session-user", }); // Live user data is used, not snapshot expect(result.collaboratorName).toBe("Jane Roe"); }); }); describe("cancellation notification jobs", () => { let originalAdapter: any; let originalJobService: any; beforeEach(() => { jest.clearAllMocks(); originalAdapter = AppointmentsList.Appointments; originalJobService = (AppointmentsList as any).jobService; (AppointmentsList as any).Appointments = { findOne: jest.fn().mockResolvedValue({ _id: "appt-001", companyId: "company-001", clientId: "client-001", employeeId: "employee-001", serviceId: "service-001", start: new Date("2026-07-22T11:00:00.000Z"), price: 1500, }), }; (AppointmentsList as any).jobService = { createJob: jest.fn().mockResolvedValue([]), createMandatoryCreationJob: jest.fn().mockResolvedValue([]), createReminderJobs: jest.fn().mockResolvedValue([]), }; (CompaniesManager.companies.findOne as jest.Mock).mockResolvedValue({ _id: "company-001", name: "clases llavallol", ownerId: "owner-001", templateEmailCancellationId: "email-template-001", templateWapCancellationId: "wap-template-001", address: "Av. Siempre Viva 123", }); (CompaniesManager.getCompanyAddress as jest.Mock).mockReturnValue("Av. Siempre Viva 123"); (ClientsManager.clients.findOne as jest.Mock).mockResolvedValue({ _id: "client-001", userId: "client-user-001", firstName: "Ada", lastName: "Lovelace", email: "ada@example.com", }); (ClientsManager.getClientFullName as jest.Mock).mockReturnValue("Ada Lovelace"); (EmployeesList.employees.findOne as jest.Mock).mockResolvedValue({ _id: "employee-001", userId: "employee-user-001", }); (EmployeesList.checkPermission as jest.Mock | undefined)?.mockResolvedValue?.(true); (UsersManager.users.findOne as jest.Mock).mockResolvedValue({ _id: "employee-user-001", firstName: "Grace", lastName: "Hopper", }); (UsersManager.getUserFullName as jest.Mock).mockReturnValue("Grace Hopper"); (ServiceList.services.findOne as jest.Mock).mockResolvedValue({ _id: "service-001", name: "Contabilidad", }); (Templates.templates.findOne as jest.Mock).mockImplementation(({ id }) => { if (id === "email-template-001") { return Promise.resolve({ template: "EMAIL CANCEL <---CLIENTE---> <---SERVICIO---> <---ORGANIZACION---> <---FECHA---> <---HORARIO--->", }); } if (id === "wap-template-001") { return Promise.resolve({ template: "WAP CANCEL <---CLIENTE---> <---SERVICIO---> <---ORGANIZACION---> <---FECHA---> <---HORARIO--->", }); } return Promise.resolve(null); }); }); afterEach(() => { (AppointmentsList as any).Appointments = originalAdapter; (AppointmentsList as any).jobService = originalJobService; }); it("stores organization-rendered cancellation template content per channel", async () => { const emailContent = await (AppointmentsList as any).tryToSendNotification({ appointmentId: "appt-001", sessionUser: "owner-001", type: "cancellation", channel: "email", }); const wapContent = await (AppointmentsList as any).tryToSendNotification({ appointmentId: "appt-001", sessionUser: "owner-001", type: "cancellation", channel: "whatsapp", }); await (AppointmentsList as any).createCancellationNotificationJobs({ appointmentId: "appt-001", companyId: "company-001", clientId: "client-001", clientUserId: "client-user-001", clientEmail: "ada@example.com", clientPhoneNumber: "5491112345678", companyOwnerId: "owner-001", companyName: "clases llavallol", emailMessage: emailContent.message, wapMessage: wapContent.message, }); expect((AppointmentsList as any).jobService.createJob).toHaveBeenCalledWith( expect.objectContaining({ type: "cancellation", payload: expect.objectContaining({ email: "ada@example.com", phoneNumber: "5491112345678", userId: "client-user-001", companyOwnerId: "owner-001", emailSubject: "TurnosXpress :: Turno cancelado", emailMessage: "EMAIL CANCEL Ada Lovelace Contabilidad clases llavallol 22/07/2026 08:00hs.", wapMessage: "WAP CANCEL Ada Lovelace Contabilidad clases llavallol 22/07/2026 08:00hs.", systemSubject: "Turno cancelado en clases llavallol", systemMessage: "EMAIL CANCEL Ada Lovelace Contabilidad clases llavallol 22/07/2026 08:00hs.", }), }) ); }); it("sends update email notifications with a non-empty subject", async () => { (PlanSubscriptionsList.checkFeature as jest.Mock).mockResolvedValue(true); await (AppointmentsList as any).sendEmailNotification({ appointmentId: "appt-001", sessionUser: "owner-001", type: APPOINTMENT_NOTIFICATION_TYPE.UPDATE, }); expect(NotificationsManager.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ email: "ada@example.com", subject: "TurnosXpress :: Turno actualizado", }) ); }); }); describe("creation notification jobs", () => { let originalAdapter: any; let originalJobService: any; beforeEach(() => { jest.clearAllMocks(); originalAdapter = AppointmentsList.Appointments; originalJobService = (AppointmentsList as any).jobService; (AppointmentsList as any).Appointments = { findOne: jest.fn().mockResolvedValue({ _id: "appt-001", companyId: "company-001", clientId: "client-001", employeeId: "employee-001", serviceId: "service-001", start: new Date("2026-07-22T11:00:00.000Z"), price: 1500, }), }; (AppointmentsList as any).jobService = { createJob: jest.fn().mockResolvedValue([]), createMandatoryCreationJob: jest.fn().mockResolvedValue([]), createReminderJobs: jest.fn().mockResolvedValue([]), }; (CompaniesManager.companies.findOne as jest.Mock).mockResolvedValue({ _id: "company-001", name: "clases llavallol", ownerId: "owner-001", templateEmailAltaId: "email-alta-template-001", templateWapAltaId: "wap-alta-template-001", address: "Av. Siempre Viva 123", }); (CompaniesManager.getCompanyAddress as jest.Mock).mockReturnValue("Av. Siempre Viva 123"); (ClientsManager.clients.findOne as jest.Mock).mockResolvedValue({ _id: "client-001", userId: "client-user-001", firstName: "Ada", lastName: "Lovelace", email: "ada@example.com", }); (ClientsManager.getClientFullName as jest.Mock).mockReturnValue("Ada Lovelace"); (EmployeesList.employees.findOne as jest.Mock).mockResolvedValue({ _id: "employee-001", userId: "employee-user-001", }); (EmployeesList.checkPermission as jest.Mock | undefined)?.mockResolvedValue?.(true); (UsersManager.users.findOne as jest.Mock).mockResolvedValue({ _id: "employee-user-001", firstName: "Grace", lastName: "Hopper", }); (UsersManager.getUserFullName as jest.Mock).mockReturnValue("Grace Hopper"); (ServiceList.services.findOne as jest.Mock).mockResolvedValue({ _id: "service-001", name: "Historia", }); (Templates.templates.findOne as jest.Mock).mockImplementation(({ id }) => { if (id === "email-alta-template-001") { return Promise.resolve({ template: "EMAIL ALTA <---CLIENTE---> <---SERVICIO---> <---ORGANIZACION---> <---FECHA---> <---HORARIO--->", }); } if (id === "wap-alta-template-001") { return Promise.resolve({ template: "WAP ALTA <---CLIENTE---> <---SERVICIO---> <---ORGANIZACION---> <---FECHA---> <---HORARIO--->", }); } return Promise.resolve(null); }); }); afterEach(() => { (AppointmentsList as any).Appointments = originalAdapter; (AppointmentsList as any).jobService = originalJobService; }); it("stores organization-rendered Alta template content per channel", async () => { const emailContent = await (AppointmentsList as any).tryToSendNotification({ appointmentId: "appt-001", sessionUser: "owner-001", type: "creation", channel: "email", }); const wapContent = await (AppointmentsList as any).tryToSendNotification({ appointmentId: "appt-001", sessionUser: "owner-001", type: "creation", channel: "whatsapp", }); await (AppointmentsList as any).createCreationNotificationJobs({ appointmentId: "appt-001", companyId: "company-001", clientId: "client-001", clientUserId: "client-user-001", clientEmail: "ada@example.com", clientPhoneNumber: "5491112345678", companyOwnerId: "owner-001", companyName: "clases llavallol", appointmentStart: new Date("2026-07-22T11:00:00.000Z"), emailMessage: emailContent.message, wapMessage: wapContent.message, reminderEmailMessage: "EMAIL REMINDER Ada Lovelace", reminderWapMessage: "WAP REMINDER Ada Lovelace", }); expect((AppointmentsList as any).jobService.createMandatoryCreationJob).toHaveBeenCalledWith( expect.objectContaining({ type: "creation", payload: expect.objectContaining({ emailSubject: "TurnosXpress :: Alta de turno", emailMessage: "EMAIL ALTA Ada Lovelace Historia clases llavallol 22/07/2026 08:00hs.", wapMessage: "WAP ALTA Ada Lovelace Historia clases llavallol 22/07/2026 08:00hs.", systemSubject: "Turno reservado en clases llavallol", systemMessage: "EMAIL ALTA Ada Lovelace Historia clases llavallol 22/07/2026 08:00hs.", }), }) ); expect((AppointmentsList as any).jobService.createReminderJobs).toHaveBeenCalledWith( expect.objectContaining({ type: "reminder", reminderPayload: expect.objectContaining({ email: "ada@example.com", phoneNumber: "5491112345678", userId: "client-user-001", companyOwnerId: "owner-001", emailSubject: "TurnosXpress :: Recordatorio", emailMessage: "EMAIL REMINDER Ada Lovelace", wapMessage: "WAP REMINDER Ada Lovelace", }), }) ); }); it("routes customer notification jobs for creation, repeat reminder-only, and disabled notifications", () => { expect((AppointmentsList as any).shouldCreateCustomerNotificationJobs(true, undefined)).toBe(true); expect((AppointmentsList as any).shouldCreateCustomerNotificationJobs(false, "repeat-001")).toBe(true); expect((AppointmentsList as any).shouldCreateCustomerNotificationJobs(false, undefined)).toBe(false); }); it("creates reminder-only jobs without rendering Alta content for repeat-created appointments", async () => { const tryToSendNotificationSpy = jest .spyOn(AppointmentsList as any, "tryToSendNotification") .mockImplementation(({ type, channel }: any) => { if (type === APPOINTMENT_NOTIFICATION_TYPE.CREATION) { throw new Error("Creation content should not be rendered"); } return Promise.resolve({ message: channel === "email" ? "EMAIL REMINDER Ada Lovelace" : "WAP REMINDER Ada Lovelace", }); }); const getOptionalClientWapNumberSpy = jest .spyOn(AppointmentsList as any, "getOptionalClientWapNumber") .mockResolvedValue("5491112345678"); try { await (AppointmentsList as any).createAppointmentNotificationJobs({ appointmentId: "appt-001", companyId: "company-001", clientId: "client-001", clientUserId: "client-user-001", clientEmail: "ada@example.com", client: { _id: "client-001" }, companyOwnerId: "owner-001", companyName: "clases llavallol", appointmentStart: new Date("2026-07-22T11:00:00.000Z"), notification: false, }); expect(tryToSendNotificationSpy).toHaveBeenCalledTimes(2); expect(tryToSendNotificationSpy).toHaveBeenCalledWith( expect.objectContaining({ type: APPOINTMENT_NOTIFICATION_TYPE.REMINDER, channel: "email" }) ); expect(tryToSendNotificationSpy).toHaveBeenCalledWith( expect.objectContaining({ type: APPOINTMENT_NOTIFICATION_TYPE.REMINDER, channel: "whatsapp" }) ); expect((AppointmentsList as any).jobService.createMandatoryCreationJob).not.toHaveBeenCalled(); expect((AppointmentsList as any).jobService.createReminderJobs).toHaveBeenCalledWith( expect.objectContaining({ type: "reminder", appointmentStart: new Date("2026-07-22T11:00:00.000Z"), reminderPayload: expect.objectContaining({ email: "ada@example.com", phoneNumber: "5491112345678", userId: "client-user-001", companyOwnerId: "owner-001", emailSubject: "TurnosXpress :: Recordatorio", emailMessage: "EMAIL REMINDER Ada Lovelace", wapMessage: "WAP REMINDER Ada Lovelace", systemSubject: "Recordatorio de turno en clases llavallol", systemMessage: "EMAIL REMINDER Ada Lovelace", }), }) ); } finally { tryToSendNotificationSpy.mockRestore(); getOptionalClientWapNumberSpy.mockRestore(); } }); it("falls back to organization WAP Alta template for email when email Alta is absent", async () => { (CompaniesManager.companies.findOne as jest.Mock).mockResolvedValue({ _id: "company-001", name: "clases llavallol", ownerId: "owner-001", templateWapAltaId: "wap-alta-template-001", address: "Av. Siempre Viva 123", }); const emailContent = await (AppointmentsList as any).tryToSendNotification({ appointmentId: "appt-001", sessionUser: "owner-001", type: "creation", channel: "email", }); expect(emailContent.message).toBe("WAP ALTA Ada Lovelace Historia clases llavallol 22/07/2026 08:00hs."); }); it("stores organization-rendered update template content with contact snapshot fields", async () => { const emailContent = await (AppointmentsList as any).tryToSendNotification({ appointmentId: "appt-001", sessionUser: "owner-001", type: "update", channel: "email", }); const wapContent = await (AppointmentsList as any).tryToSendNotification({ appointmentId: "appt-001", sessionUser: "owner-001", type: "update", channel: "whatsapp", }); await (AppointmentsList as any).createUpdateNotificationJobs({ appointmentId: "appt-001", companyId: "company-001", clientId: "client-001", clientUserId: "client-user-001", clientEmail: "ada@example.com", clientPhoneNumber: "5491112345678", companyOwnerId: "owner-001", companyName: "clases llavallol", appointmentStart: new Date("2026-07-22T11:00:00.000Z"), emailMessage: emailContent.message, wapMessage: wapContent.message, reminderEmailMessage: "EMAIL REMINDER Ada Lovelace", reminderWapMessage: "WAP REMINDER Ada Lovelace", }); expect((AppointmentsList as any).jobService.createJob).toHaveBeenCalledWith( expect.objectContaining({ type: "update", appointmentStart: new Date("2026-07-22T11:00:00.000Z"), payload: expect.objectContaining({ email: "ada@example.com", phoneNumber: "5491112345678", userId: "client-user-001", companyOwnerId: "owner-001", emailSubject: "TurnosXpress :: Turno actualizado", emailMessage: "EMAIL ALTA Ada Lovelace Historia clases llavallol 22/07/2026 08:00hs.", wapMessage: "WAP ALTA Ada Lovelace Historia clases llavallol 22/07/2026 08:00hs.", systemSubject: "Turno actualizado en clases llavallol", systemMessage: "EMAIL ALTA Ada Lovelace Historia clases llavallol 22/07/2026 08:00hs.", }), reminderPayload: expect.objectContaining({ email: "ada@example.com", phoneNumber: "5491112345678", userId: "client-user-001", companyOwnerId: "owner-001", emailSubject: "TurnosXpress :: Recordatorio", emailMessage: "EMAIL REMINDER Ada Lovelace", wapMessage: "WAP REMINDER Ada Lovelace", }), }) ); }); });