import { NoPermissionMessage } from "../../helpers/NoPermissionMessage"; import EmployeesList from "../Employees/Employee"; import { EmployeeRoles } from "../Employees/Employees.Interface"; import UserList from "../Users/Users"; import { IUserDocument } from "../Users/Users.Adapter.Mongoose"; import { PaymentsAdapterMongoose } from "./Payments.Adapter.Mongoose"; import { CreatePaymentParams, DeletePaymentParams, DeletePaymentsByClientParams, DeletePaymentsByCompanyParams, FindPaymentsParams, GetAppointmentPaymentsParams, IPayment, IPaymentsManager, MarkAsPayedParams, PaginateNotPayedAppointmentsParams, PaginateNotPayedAppointmentsResult, NotPayedReportResult, PaginatePaymentsView, PaginatePaymentsViewParams, PaginatePaymentsViewResults, PaymentsReportResult, PAYMENT_METHODS_CLIENT_ACCOUNT, PAYMENT_PROCCESS_DATE, } from "./Payments.Interface"; import AppointmentList from "../Appointments/Appointments"; import { CLIENT_ACCOUNT_MOVEMENT_STATUS, CLIENT_ACCOUNT_MOVEMENT_TYPES, CLIENT_ACCOUNT_PAYMENT_METHODS, } from "../ClientAccountMovements/ClientAccountMovements.Interface"; import ClientsList from "../Clients/Clients"; import ServicesList from "../Services/Service"; import ClientAccount from "../ClientAccounts/ClientAccount"; import ClientAccountMovements from "../ClientAccountMovements/ClientAccountMovements"; import CashFlowBalance from "../CashFlowBalance/CashFlowBalance"; import dayjs from "dayjs"; import { FilterQuery } from "mongoose"; import { IAppointment } from "../Appointments/Appointments.Interface"; import PlanSubscriptionsList from "../PlanSubscriptions/PlanSubscriptons"; import CompaniesList from "../Companies/Companies"; import { PlanFeatures } from "../Plans/Plans.interface"; class PaymentsManager implements IPaymentsManager { public async getPaymentsReport( filters: FindPaymentsParams ): Promise { await this.validateSessionUser(((filters.sessionUser || "") || "")); await this.validatePermissions(((filters.sessionUser || "") || ""), String(filters.companyId)); const results = await this.payments.find(filters); // Sort descending by createdAt since find() in adapter doesn't sort by default results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); const contentData: PaginatePaymentsView[] = []; let totalAmount = 0; for (const pago of results) { const appointment = await AppointmentList.Appointments.findOne({ _id: pago.appointmentId, }); if (!appointment) continue; const client = await ClientsList.clients.findOne({ _id: appointment.clientId, }); if (!client) continue; const employee = await EmployeesList.employees.findOne({ _id: appointment.employeeId, }); if (!employee) continue; const employeeUser = await UserList.users.findOne({ _id: String(employee.userId), }); if (!employeeUser) continue; const service = await ServicesList.services.findOne({ _id: appointment.serviceId, }); const clientName = await ClientsList.getClientFullName(client); const employeeName = await UserList.getUserFullName(employeeUser); totalAmount += pago.amount; contentData.push({ id: pago.id || "", appointmentId: appointment.id, appointmentDate: appointment.start as Date, clientId: String(client._id), clientName: clientName || "", employeeId: employee?.id || "", employeeName: employeeName || "", serviceId: service?.id || "", serviceName: service?.name || "", price: pago.amount, method: pago.method || "", status: pago.status as any, description: pago.description || "", createdAt: pago.createdAt as Date, }); } return { items: contentData, totalAmount }; } payments: PaymentsAdapterMongoose; constructor() { this.payments = new PaymentsAdapterMongoose(); } public async deletePaymentsByCompany(data: DeletePaymentsByCompanyParams): Promise { await this.payments.paymentsList.deleteMany({ companyId: data.companyId, }); } public async deletePaymentsByClient(data: DeletePaymentsByClientParams): Promise { await this.payments.paymentsList.deleteMany({ clientId: data.clientId, }); } public async paginatedPaymentsViewTotal(data: FindPaymentsParams): Promise { const pagos = await this.payments.find(data); const pagosPrice = pagos.map((pago) => pago.amount); return pagosPrice.reduce((previous, current) => current + previous, 0); } public async paginatePaymentsView( data: PaginatePaymentsViewParams ): Promise { const userSession = await this.validateSessionUser((data.sessionUser || "")); await this.validatePermissions(String(userSession.id), String(data.companyId)); const results: Array = []; const pagos = await this.payments.paginate(data); for (const pago of pagos.data) { const appointment = await AppointmentList.Appointments.findOne({ _id: pago.appointmentId, }); if (!appointment) { continue; } const client = await ClientsList.clients.findOne({ _id: appointment.clientId, }); if (!client) { continue; } const employee = await EmployeesList.employees.findOne({ _id: appointment.employeeId, }); if (!employee) { continue; } const employeeUser = await UserList.users.findOne({ _id: String(employee.userId), }); if (!employeeUser) { continue; } const service = await ServicesList.services.findOne({ _id: appointment.serviceId, }); if (!service) { continue; } if (!pago._id) { continue; } results.push({ id: String(pago.id), appointmentId: String(pago.appointmentId), appointmentDate: appointment.dateDay, clientId: String(pago.clientId), clientName: ClientsList.getClientFullName(client), employeeId: String(pago.employeeId), employeeName: UserList.getUserFullName(employeeUser), serviceId: String(pago.serviceId), serviceName: service.name, price: Number(pago.amount), status: pago.status, method: pago.method || "", description: pago.description, createdAt: pago.createdAt, }); } return { data: results, page: data.page, pages: pagos.pages, }; } private async validateSessionUser(sessionUserId: string): Promise { const sessionUser = await UserList.users.findOne({ _id: sessionUserId, }); if (!sessionUser) { throw new Error( "No se ha encontrado el usuario o no tiene permisos para realizar esta acción" ); } return sessionUser; } private async validatePermissions(sessionUserId: string, companyId: string): Promise { // Verificar si el usuario de sesión es un empleado con permisos de administrador const hasAdminPermission = await EmployeesList.checkRoleById( companyId, sessionUserId, EmployeeRoles.ADMIN ); if (!hasAdminPermission) { throw new Error(NoPermissionMessage()); } const company = await CompaniesList.companies.findOne({ _id: companyId, }); if (!company) { throw new Error("No se ha encontrado la organización"); } const canAccess = await PlanSubscriptionsList.checkFeature({ userId: String(company.ownerId), feature: PlanFeatures.PAYMENTS, }); if (!canAccess) { throw new Error("La organizacion no tiene un plan que soporte el módulo de pagos"); } } private async getPayedAmount(appointmentId: string): Promise { const payments = await this.payments.find({ appointmentId: appointmentId, }); const payedAmount = payments.reduce((acc, payment) => acc + payment.amount, 0); return payedAmount; } public async createPayment(data: CreatePaymentParams): Promise { const userSession = await this.validateSessionUser((data.sessionUser || "")); const appointment = await AppointmentList.Appointments.findOne({ _id: data.appointmentId, }); if (!appointment) { throw new Error("No se encontró la reserva"); } if (appointment.payment) { throw new Error("La reserva ya fue pagada"); } await this.validatePermissions(String(userSession.id), String(appointment.companyId)); const toPayAmount = appointment.price; const payedAmount = await this.getPayedAmount(data.appointmentId); const remainingAmount = toPayAmount - payedAmount; if (remainingAmount <= 0) { throw new Error("La reserva ya está pagada"); } if (data.amount > remainingAmount) { throw new Error("El monto del pago no puede ser mayor al monto restante"); } if (data.amount <= 0) { throw new Error("El monto del pago no puede ser menor o igual a 0"); } let availableClientAccountAmount = 0; if (data.method == PAYMENT_METHODS_CLIENT_ACCOUNT.CLIENT_ACCOUNT) { availableClientAccountAmount = await ClientAccount.getAvailableAmount({ clientId: String(appointment.clientId), sessionUser: (data.sessionUser || ""), }); if (availableClientAccountAmount < data.amount) { throw new Error("El saldo de la cuenta del cliente no es suficiente"); } } let proccessDate = new Date(); if (data.proccessDate == PAYMENT_PROCCESS_DATE.NOW) { proccessDate = new Date(); } else if (data.proccessDate == PAYMENT_PROCCESS_DATE.APPOINTMENT) { proccessDate = new Date(appointment.start); } const payment = await this.payments.create({ ...data, clientId: String(appointment.clientId), companyId: String(appointment.companyId), employeeId: String(appointment.employeeId), serviceId: String(appointment.serviceId), ...{ createdAt: proccessDate }, }); const eventData = await AppointmentList.getAppointmentEvent({ appointmentId: data.appointmentId, sessionUser: (data.sessionUser || ""), }); const reservaDesc = eventData.title + " - " + eventData.serviceName + " (" + eventData.collaboratorName + ")"; //si el pago es con cuenta de cliente, se debe descontar el dinero de la cuenta del cliente if (data.method == PAYMENT_METHODS_CLIENT_ACCOUNT.CLIENT_ACCOUNT) { await ClientAccountMovements.add({ clientId: String(appointment.clientId), amount: data.amount, description: reservaDesc, sessionUser: (data.sessionUser || ""), type: CLIENT_ACCOUNT_MOVEMENT_TYPES.WITHDRAWAL, status: CLIENT_ACCOUNT_MOVEMENT_STATUS.APPROVED, paymentMethod: CLIENT_ACCOUNT_PAYMENT_METHODS.CASH, }); } if (remainingAmount === data.amount) { await AppointmentList.setPaymentStatus({ appointmentId: data.appointmentId, status: true, sessionUser: (data.sessionUser || ""), }); } //Creamos el movimiento de caja si es un pago que no viene de la cuenta corriente del cliente. if (data.method != PAYMENT_METHODS_CLIENT_ACCOUNT.CLIENT_ACCOUNT) { const paymentDate = dayjs(payment.createdAt); const paymentYear = paymentDate.year(); const paymentMonth = paymentDate.month(); await CashFlowBalance.createCashFlowMovement({ amount: data.amount, description: reservaDesc, year: paymentYear, month: paymentMonth, companyId: String(appointment.companyId), paymentId: String(payment._id), employeeId: String(appointment.employeeId), clientId: String(appointment.clientId), appointmentId: String(appointment._id), paymentMethod: data.method, reference: "", type: CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT, proccessDate: data.proccessDate, sessionUser: (data.sessionUser || ""), }); } return payment; } public async markAsPayed(data: MarkAsPayedParams): Promise { const userSession = await this.validateSessionUser((data.sessionUser || "")); const appointment = await AppointmentList.Appointments.findOne({ _id: data.appointmentId, }); if (!appointment) { throw new Error("No se encontró la reserva"); } await this.validatePermissions(String(userSession.id), String(appointment.companyId)); const toPayAmount = appointment.price; const payedAmount = await this.getPayedAmount(data.appointmentId); const remainingAmount = toPayAmount - payedAmount; if (remainingAmount == 0) { throw new Error("La reserva ya está pagada"); } await this.createPayment({ appointmentId: data.appointmentId, amount: remainingAmount, status: CLIENT_ACCOUNT_MOVEMENT_STATUS.APPROVED, description: "Pago total de la reserva", method: data.method, proccessDate: PAYMENT_PROCCESS_DATE.NOW, sessionUser: (data.sessionUser || ""), }); } public async deletePayment(data: DeletePaymentParams): Promise { await this.validateSessionUser((data.sessionUser || "")); // Primero buscamos el movimiento por su ID const payment = await this.payments.paymentsList.findOne({ _id: String(data.id), }); if (!payment) { throw new Error("No se encontró el pago"); } await this.validatePermissions((data.sessionUser || ""), payment.companyId); const appointment = await AppointmentList.Appointments.findOne({ _id: payment.appointmentId, }); if (!appointment) { throw new Error("No se encontró la reserva"); } const toPayAmount = appointment.price; const payedAmount = await this.getPayedAmount(payment.appointmentId); const remainingAmount = toPayAmount - (payedAmount - payment.amount); //si el pago es con cuenta de cliente, se debe agregar el dinero a la cuenta del cliente if (payment.method == PAYMENT_METHODS_CLIENT_ACCOUNT.CLIENT_ACCOUNT) { await ClientAccountMovements.add({ clientId: String(appointment.clientId), amount: payment.amount, description: "Reembolso de la reserva - " + appointment.id, sessionUser: (data.sessionUser || ""), type: CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT, status: CLIENT_ACCOUNT_MOVEMENT_STATUS.APPROVED, paymentMethod: CLIENT_ACCOUNT_PAYMENT_METHODS.CASH, }); } //Si el pago que se borra no viene de la cuenta del cliente, borramos el movimiento de la caja. if (payment.method != PAYMENT_METHODS_CLIENT_ACCOUNT.CLIENT_ACCOUNT) { await CashFlowBalance.deleteByPayment({ companyId: String(appointment.companyId), paymentId: String(payment._id), sessionUser: (data.sessionUser || ""), }); } await this.payments.delete(data.id); if (remainingAmount > 0) { await AppointmentList.setPaymentStatus({ appointmentId: String(appointment._id), status: false, sessionUser: (data.sessionUser || ""), }); } } public async getAppointmentPayments(data: GetAppointmentPaymentsParams): Promise { await this.validateSessionUser((data.sessionUser || "")); const appointment = await AppointmentList.Appointments.findOne({ _id: data.appointmentId, }); if (!appointment) { throw new Error("No se encontró la reserva"); } await this.validatePermissions((data.sessionUser || ""), String(appointment.companyId)); return await this.payments.find({ appointmentId: data.appointmentId, }); } private getNotPayedSearchCriteria(filters: FindPaymentsParams): FilterQuery { const searchCriteria: FilterQuery = {}; if (filters.startDate) { searchCriteria.start = { $gte: filters.startDate, $lte: filters.endDate, }; } searchCriteria.payment = false; if (filters.clientIds && filters.clientIds.length > 0) { searchCriteria.clientId = { $in: filters.clientIds }; } else if (filters.clientId) { searchCriteria.clientId = filters.clientId; } if (filters.method) { searchCriteria.method = filters.method; } if (filters.companyId) { searchCriteria.companyId = filters.companyId; } if (filters.employeeId) { searchCriteria.employeeId = filters.employeeId; } if (filters.serviceId) { searchCriteria.serviceId = filters.serviceId; } return searchCriteria; } public async paginateNotPayedAppointments( filters: PaginateNotPayedAppointmentsParams ): Promise { await this.validateSessionUser(((filters.sessionUser || "") || "")); await this.validatePermissions(((filters.sessionUser || "") || ""), String(filters.companyId)); const searchCriteria = this.getNotPayedSearchCriteria(filters); const count = await AppointmentList.Appointments.AppointmentList.countDocuments( searchCriteria ).exec(); const skip = (filters.page - 1) * filters.limit; const results = await AppointmentList.Appointments.AppointmentList.find(searchCriteria) .skip(skip) .limit(filters.limit) .sort({ start: -1 }) .exec(); const contentData: PaginatePaymentsView[] = []; for (const item of results) { const client = await ClientsList.clients.findOne({ _id: item.clientId, }); const employee = await EmployeesList.employees.findOne({ _id: item.employeeId, }); const service = await ServicesList.services.findOne({ _id: item.serviceId, }); if (!client) { continue; } if (!employee) { continue; } if (!service) { continue; } const employeeUser = await UserList.users.findOne({ _id: String(employee.userId), }); if (!employeeUser) { continue; } const clientName = await ClientsList.getClientFullName(client); const employeeName = await UserList.getUserFullName(employeeUser); contentData.push({ id: item.id, appointmentId: item.id, appointmentDate: item.start, clientId: item.clientId, clientName: clientName || "", employeeId: item.employeeId, employeeName: employeeName || "", serviceId: item.serviceId, serviceName: service.name, price: item.price, status: CLIENT_ACCOUNT_MOVEMENT_STATUS.PENDING, method: CLIENT_ACCOUNT_PAYMENT_METHODS.OTHER, description: "", createdAt: item.start, }); } let pages = Math.ceil(count / filters.limit); if (pages < 1) { pages = 1; } return { data: contentData, page: filters.page, pages: pages, }; } public async paginatedNotPayedAppiontmentsTotal(data: FindPaymentsParams): Promise { await this.validateSessionUser((data.sessionUser || "")); await this.validatePermissions((data.sessionUser || ""), String(data.companyId)); const searchCriteria = this.getNotPayedSearchCriteria(data); const pagos = await AppointmentList.Appointments.AppointmentList.find(searchCriteria); return pagos.reduce((previous, current) => current.price + previous, 0); } public async getNotPayedReport( filters: FindPaymentsParams ): Promise { await this.validateSessionUser(((filters.sessionUser || "") || "")); await this.validatePermissions(((filters.sessionUser || "") || ""), String(filters.companyId)); const searchCriteria = this.getNotPayedSearchCriteria(filters); const results = await AppointmentList.Appointments.AppointmentList.find(searchCriteria) .sort({ start: -1 }) .exec(); const contentData: PaginatePaymentsView[] = []; let totalAmount = 0; for (const item of results) { const client = await ClientsList.clients.findOne({ _id: item.clientId, }); const employee = await EmployeesList.employees.findOne({ _id: item.employeeId, }); const service = await ServicesList.services.findOne({ _id: item.serviceId, }); if (!client || !employee || !service) { continue; } const employeeUser = await UserList.users.findOne({ _id: String(employee.userId), }); if (!employeeUser) { continue; } const clientName = await ClientsList.getClientFullName(client); const employeeName = await UserList.getUserFullName(employeeUser); contentData.push({ id: item.id, appointmentId: item.id, appointmentDate: item.start, clientId: item.clientId, clientName: clientName || "", employeeId: item.employeeId, employeeName: employeeName || "", serviceId: item.serviceId, serviceName: service.name, price: item.price, status: CLIENT_ACCOUNT_MOVEMENT_STATUS.PENDING, method: CLIENT_ACCOUNT_PAYMENT_METHODS.OTHER, description: "", createdAt: item.start, }); totalAmount += item.price; } return { items: contentData, totalAmount: totalAmount, }; } public async payNotPayedAppointments(data: FindPaymentsParams): Promise { await this.validateSessionUser((data.sessionUser || "")); await this.validatePermissions((data.sessionUser || ""), String(data.companyId)); const { method, ...searchCriteria } = this.getNotPayedSearchCriteria(data); //Estraigo la propiedad method de searchCriteria const toPayAppointments = await AppointmentList.Appointments.AppointmentList.find( searchCriteria ); if (!method) { throw new Error("No se ha especidicado un método de pago"); } if (!data.proccessDate) { throw new Error("No se ha especidicado una fecha de pago"); } for (const item of toPayAppointments) { try { if (dayjs(item.dateDay).isAfter(dayjs())) { continue; // No se pueden pagar reservas futuras de forma masiva. } await this.createPayment({ appointmentId: item.id, method: method, status: CLIENT_ACCOUNT_MOVEMENT_STATUS.APPROVED, description: "Pago completo de la reserva", amount: item.price, proccessDate: data.proccessDate, sessionUser: (data.sessionUser || ""), }); } catch (e) { console.log(e); } } } } export default new PaymentsManager();