first commit

This commit is contained in:
2026-07-16 20:48:43 -03:00
commit 5696cee264
1111 changed files with 322270 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
DATABASE_CONNECTION = mongodb://horacio:frx8us2w@localhost:20215/turnosxpress
DATABASE_CONNECTION_LOCAL = mongodb://horacio:frx8us2w@localhost:20215/turnosxpress
DATABASE_CONNECTION_OMV = mongodb://horacio:frx8us2w@192.168.1.50:20215/turnosxpress
SECRET_WORD = k34lklJ4HKJ2j3kj@kl23
TOKEN_SESSION_TIME = 5d
UPLOAD_SERVICE_FILES_ROOT_DEV = ../../../uploads/services/
UPLOAD_SERVICE_FILES_ROOT_PRO = ../../../uploads/services/
UPLOAD_COMPANY_FILES_ROOT_DEV = ../../../uploads/companies/
UPLOAD_COMPANY_FILES_ROOT_PRO = ../../../uploads/companies/
UPLOAD_USERS_FILES_ROOT_DEV = ../../../uploads/users/
UPLOAD_USERS_FILES_ROOT_PRO = ../../../uploads/users/
GET_AVATAR_USERS_FILES_ROOT_DEV = ../../uploads/users/
GET_AVATAR_USERS_FILES_ROOT_PRO = ../../uploads/users/
GET_HEADER_COMPANIES_FILES_ROOT_DEV = ../../uploads/companies/
GET_HEADER_COMPANIES_FILES_ROOT_PRO = ../../uploads/companies/
GET_SERVICE_IMAGE_FILES_ROOT_DEV = ../../uploads/services/
GET_SERVICE_IMAGE_FILES_ROOT_PRO = ../../uploads/services/
DEFAULT_COMPANY_SERVICE_FRACTION = 15
GMAIL_PASSWORD = ktvm kbvm fach iohn
GMAIL_APP_NAME = particular
GMAIL_EMAIL = clases.hdr@gmail.com
PUBLIC_AVATAR_URL = http://localhost:3000/public/users/
PUBLIC_HEADER_URL = http://localhost:3000/public/companies/
PUBLIC_SERVICE_IMAGE_URL = http://localhost:3000/public/services/
DEFAULT_HEADER_COLOR = f5da43
DEFAULT_HEADER_FONT_COLOR = 3e3748
DEFAULT_HEADER_FONT_SHADOW_COLOR = ffffff
DEFAULT_SERVICE_COLOR = 55d400
DEFAULT_SERVICE_FONT_COLOR = ffffff
DEFAULT_SERVICE_FONT_SHADOW_COLOR = 000000
MP_SUBSCRIPTION_CREATE_URL = https://api.mercadopago.com/preapproval
MP_SUBSCRIPTION_FIND_URL = https://api.mercadopago.com/preapproval/
MP_ACCESS_TOKEN = APP_USR-8753424836448507-121412-731599daea1ffa0b278b5dcc78aa1ec8-1611134648
MP_BACK_URL = https://turnosxpress.com.ar/landing/dashboard
MP_CHECK_PAYMENT_MINUTES = 1
MP_WEBHOOK_URL = https://5747-186-132-168-62.ngrok-free.app/mercadopago/webhook
DEFAULT_BOT_ADMIN_PORT = 3005
DONWEB_MAIL_API_KEY = eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NDYxMTc3NzUsImV4cCI6NDkwMTc5MTM3NSwicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJraWQiOiI2ODEzYTQ4ZjQ1NGQwYzA1YjAwYTcxYTciLCJhaWQiOiI2ODEzOGIzNjQ2NGFlYzAyMGQwZWY0YjkiLCJ1c2VybmFtZSI6ImhvcmFjaW9kcnNAZ21haWwuY29tIn0.BEIMAFx9SzwRkWK0UEhmem0KpswDK86s4_cdiNeuHLn47G_7dvzfsAR4zqa2EeVeCPC3J62qOtnhMH6GmIKaiZhx0C_ngTJ1nFVzENlZR7pk-R-QdnG8Iq6ZvQrXvQfd3NPi8MNGJAnWEHo9JW2HfvMW534TdBcBcAYTgU51EnLSMnoJMYQ7gkvkPugdIEtD01_2OM_KOx7HFG0svLACgnz_m0TKOxQD1XvDqzbOlCukQgkrftuHEJK5JYa_WrBuvS_RwiaS3CuLMny076gMNtEAJMiHYe4D-9zfMTzJJc-uWu-DB8ASwSwzwPWsNhVbzpsoZGtopWdZ3RJizk1r0g
WAP_TIME_LIMIT = 5000
WAP_REMINDER = 🔔Hola <---CLIENTE---> <---BR---> 📌Te recuerdo que el <---FECHA---> te esperamos a las <---HORARIO---> ⏰ <---BR---> 🏫 En <---DIRECCION---> <---BR--->Con <---PROFESIONAL---> para <---SERVICIO---> <---BR--->Precio: <---PRECIO---> <---BR---> *<---ORGANIZACION--->*
WAP_ALTA = 🔔Hola <---CLIENTE---> <---BR---> 📌Has realizado una reserva para el día <---FECHA---> a las <---HORARIO---> ⏰ <---BR---> 🏫 En <---DIRECCION---> <---BR--->Con <---PROFESIONAL---> para <---SERVICIO---> <---BR--->Precio: <---PRECIO---> <---BR---> *<---ORGANIZACION--->*
GOOGLE_CLIENT_ID = 252287289364-bu8mquhttsaa282egh8su2sumvkjqe58.apps.googleusercontent.com
CORS_ALLOWED_ORIGIN = http://localhost:3021
APPOINTMENT_TIME = 24
CANCELLATION_TIME = 24
SYSTEM_KEY = 753a2sd1du64-c412-3c3a-9a4hdr9-ac419213957e
FILE_SERVER_URL = http://localhost:4000/
FILE_SERVER_URL_prod = https://files.turnosxpress.com.ar/
SYS_ADMIN_API_PUBLIC_KEY = keys/tx-sysadmin-public.key
+68
View File
@@ -0,0 +1,68 @@
DATABASE_CONNECTION = mongodb://horacio:frx8us2w@10.0.2.2:20215/turnosxpress
DATABASE_CONNECTION_LOCAL = mongodb://horacio:frx8us2w@localhost:20215/turnosxpress
DATABASE_CONNECTION_OMV = mongodb://horacio:frx8us2w@192.168.1.50:20215/turnosxpress
SECRET_WORD = k34lklJ4HKJ2j3kj@kl23
TOKEN_SESSION_TIME = 5d
UPLOAD_SERVICE_FILES_ROOT_DEV = ../../../uploads/services/
UPLOAD_SERVICE_FILES_ROOT_PRO = ../../../uploads/services/
UPLOAD_COMPANY_FILES_ROOT_DEV = ../../../uploads/companies/
UPLOAD_COMPANY_FILES_ROOT_PRO = ../../../uploads/companies/
UPLOAD_USERS_FILES_ROOT_DEV = ../../../uploads/users/
UPLOAD_USERS_FILES_ROOT_PRO = ../../../uploads/users/
GET_AVATAR_USERS_FILES_ROOT_DEV = ../../uploads/users/
GET_AVATAR_USERS_FILES_ROOT_PRO = ../../uploads/users/
GET_HEADER_COMPANIES_FILES_ROOT_DEV = ../../uploads/companies/
GET_HEADER_COMPANIES_FILES_ROOT_PRO = ../../uploads/companies/
GET_SERVICE_IMAGE_FILES_ROOT_DEV = ../../uploads/services/
GET_SERVICE_IMAGE_FILES_ROOT_PRO = ../../uploads/services/
DEFAULT_COMPANY_SERVICE_FRACTION = 15
GMAIL_PASSWORD = ktvm kbvm fach iohn
GMAIL_APP_NAME = particular
GMAIL_EMAIL = clases.hdr@gmail.com
PUBLIC_AVATAR_URL = http://10.0.2.2:3000/public/users/
PUBLIC_HEADER_URL = http://10.0.2.2:3000/public/companies/
PUBLIC_SERVICE_IMAGE_URL = http://10.0.2.2:3000/public/services/
DEFAULT_HEADER_COLOR = f5da43
DEFAULT_HEADER_FONT_COLOR = 3e3748
DEFAULT_HEADER_FONT_SHADOW_COLOR = ffffff
DEFAULT_SERVICE_COLOR = 55d400
DEFAULT_SERVICE_FONT_COLOR = ffffff
DEFAULT_SERVICE_FONT_SHADOW_COLOR = 000000
MP_SUBSCRIPTION_CREATE_URL = https://api.mercadopago.com/preapproval
MP_SUBSCRIPTION_FIND_URL = https://api.mercadopago.com/preapproval/
MP_ACCESS_TOKEN = APP_USR-8753424836448507-121412-731599daea1ffa0b278b5dcc78aa1ec8-1611134648
MP_BACK_URL = https://turnosxpress.com.ar/landing/dashboard
MP_CHECK_PAYMENT_MINUTES = 1
DEFAULT_BOT_ADMIN_PORT = 3005
DONWEB_MAIL_API_KEY = eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NDYxMTc3NzUsImV4cCI6NDkwMTc5MTM3NSwicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJraWQiOiI2ODEzYTQ4ZjQ1NGQwYzA1YjAwYTcxYTciLCJhaWQiOiI2ODEzOGIzNjQ2NGFlYzAyMGQwZWY0YjkiLCJ1c2VybmFtZSI6ImhvcmFjaW9kcnNAZ21haWwuY29tIn0.BEIMAFx9SzwRkWK0UEhmem0KpswDK86s4_cdiNeuHLn47G_7dvzfsAR4zqa2EeVeCPC3J62qOtnhMH6GmIKaiZhx0C_ngTJ1nFVzENlZR7pk-R-QdnG8Iq6ZvQrXvQfd3NPi8MNGJAnWEHo9JW2HfvMW534TdBcBcAYTgU51EnLSMnoJMYQ7gkvkPugdIEtD01_2OM_KOx7HFG0svLACgnz_m0TKOxQD1XvDqzbOlCukQgkrftuHEJK5JYa_WrBuvS_RwiaS3CuLMny076gMNtEAJMiHYe4D-9zfMTzJJc-uWu-DB8ASwSwzwPWsNhVbzpsoZGtopWdZ3RJizk1r0g
WAP_TIME_LIMIT = 5000
WAP_REMINDER = 🔔Hola <---CLIENTE---> <---BR---> 📌Te recuerdo que el <---FECHA---> te esperamos a las <---HORARIO---> ⏰ <---BR---> 🏫 En <---DIRECCION---> <---BR--->Con <---PROFESIONAL---> para <---SERVICIO---> <---BR--->Precio: <---PRECIO---> <---BR---> *<---ORGANIZACION--->*
WAP_ALTA = 🔔Hola <---CLIENTE---> <---BR---> 📌Has realizado una reserva para el día <---FECHA---> a las <---HORARIO---> ⏰ <---BR---> 🏫 En <---DIRECCION---> <---BR--->Con <---PROFESIONAL---> para <---SERVICIO---> <---BR--->Precio: <---PRECIO---> <---BR---> *<---ORGANIZACION--->*
GOOGLE_CLIENT_ID = 252287289364-bu8mquhttsaa282egh8su2sumvkjqe58.apps.googleusercontent.com
CORS_ALLOWED_ORIGIN = http://10.0.2.2:3021
APPOINTMENT_TIME = 24
CANCELLATION_TIME = 24
SYSTEM_KEY = 753a2sd1du64-c412-3c3a-9a4hdr9-ac419213957e
FILE_SERVER_URL = http://10.0.2.2:4000/
SYS_ADMIN_API_PUBLIC_KEY = keys/tx-sysadmin-public.key
+68
View File
@@ -0,0 +1,68 @@
DATABASE_CONNECTION = mongodb://horacio:frx8us2w@localhost:20215/turnosxpress
DATABASE_CONNECTION_LOCAL = mongodb://horacio:frx8us2w@localhost:20215/turnosxpress
DATABASE_CONNECTION_OMV = mongodb://horacio:frx8us2w@192.168.1.50:20215/turnosxpress
SECRET_WORD = k34lklJ4HKJ2j3kj@kl23
TOKEN_SESSION_TIME = 5d
UPLOAD_SERVICE_FILES_ROOT_DEV = ../../../uploads/services/
UPLOAD_SERVICE_FILES_ROOT_PRO = ../../../uploads/services/
UPLOAD_COMPANY_FILES_ROOT_DEV = ../../../uploads/companies/
UPLOAD_COMPANY_FILES_ROOT_PRO = ../../../uploads/companies/
UPLOAD_USERS_FILES_ROOT_DEV = ../../../uploads/users/
UPLOAD_USERS_FILES_ROOT_PRO = ../../../uploads/users/
GET_AVATAR_USERS_FILES_ROOT_DEV = ../../uploads/users/
GET_AVATAR_USERS_FILES_ROOT_PRO = ../../uploads/users/
GET_HEADER_COMPANIES_FILES_ROOT_DEV = ../../uploads/companies/
GET_HEADER_COMPANIES_FILES_ROOT_PRO = ../../uploads/companies/
GET_SERVICE_IMAGE_FILES_ROOT_DEV = ../../uploads/services/
GET_SERVICE_IMAGE_FILES_ROOT_PRO = ../../uploads/services/
DEFAULT_COMPANY_SERVICE_FRACTION = 15
GMAIL_PASSWORD = ktvm kbvm fach iohn
GMAIL_APP_NAME = particular
GMAIL_EMAIL = clases.hdr@gmail.com
PUBLIC_AVATAR_URL = http://localhost:3000/public/users/
PUBLIC_HEADER_URL = http://localhost:3000/public/companies/
PUBLIC_SERVICE_IMAGE_URL = http://localhost:3000/public/services/
DEFAULT_HEADER_COLOR = f5da43
DEFAULT_HEADER_FONT_COLOR = 3e3748
DEFAULT_HEADER_FONT_SHADOW_COLOR = ffffff
DEFAULT_SERVICE_COLOR = 55d400
DEFAULT_SERVICE_FONT_COLOR = ffffff
DEFAULT_SERVICE_FONT_SHADOW_COLOR = 000000
MP_SUBSCRIPTION_CREATE_URL = https://api.mercadopago.com/preapproval
MP_SUBSCRIPTION_FIND_URL = https://api.mercadopago.com/preapproval/
MP_ACCESS_TOKEN = APP_USR-8753424836448507-121412-731599daea1ffa0b278b5dcc78aa1ec8-1611134648
MP_BACK_URL = https://turnosxpress.com.ar/landing/dashboard
MP_CHECK_PAYMENT_MINUTES = 1
DEFAULT_BOT_ADMIN_PORT = 3005
DONWEB_MAIL_API_KEY = eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NDYxMTc3NzUsImV4cCI6NDkwMTc5MTM3NSwicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJraWQiOiI2ODEzYTQ4ZjQ1NGQwYzA1YjAwYTcxYTciLCJhaWQiOiI2ODEzOGIzNjQ2NGFlYzAyMGQwZWY0YjkiLCJ1c2VybmFtZSI6ImhvcmFjaW9kcnNAZ21haWwuY29tIn0.BEIMAFx9SzwRkWK0UEhmem0KpswDK86s4_cdiNeuHLn47G_7dvzfsAR4zqa2EeVeCPC3J62qOtnhMH6GmIKaiZhx0C_ngTJ1nFVzENlZR7pk-R-QdnG8Iq6ZvQrXvQfd3NPi8MNGJAnWEHo9JW2HfvMW534TdBcBcAYTgU51EnLSMnoJMYQ7gkvkPugdIEtD01_2OM_KOx7HFG0svLACgnz_m0TKOxQD1XvDqzbOlCukQgkrftuHEJK5JYa_WrBuvS_RwiaS3CuLMny076gMNtEAJMiHYe4D-9zfMTzJJc-uWu-DB8ASwSwzwPWsNhVbzpsoZGtopWdZ3RJizk1r0g
WAP_TIME_LIMIT = 5000
WAP_REMINDER = 🔔Hola <---CLIENTE---> <---BR---> 📌Te recuerdo que el <---FECHA---> te esperamos a las <---HORARIO---> ⏰ <---BR---> 🏫 En <---DIRECCION---> <---BR--->Con <---PROFESIONAL---> para <---SERVICIO---> <---BR--->Precio: <---PRECIO---> <---BR---> *<---ORGANIZACION--->*
WAP_ALTA = 🔔Hola <---CLIENTE---> <---BR---> 📌Has realizado una reserva para el día <---FECHA---> a las <---HORARIO---> ⏰ <---BR---> 🏫 En <---DIRECCION---> <---BR--->Con <---PROFESIONAL---> para <---SERVICIO---> <---BR--->Precio: <---PRECIO---> <---BR---> *<---ORGANIZACION--->*
GOOGLE_CLIENT_ID = 252287289364-bu8mquhttsaa282egh8su2sumvkjqe58.apps.googleusercontent.com
CORS_ALLOWED_ORIGIN = http://localhost:3021
APPOINTMENT_TIME = 24
CANCELLATION_TIME = 24
SYSTEM_KEY = 753a2sd1du64-c412-3c3a-9a4hdr9-ac419213957e
FILE_SERVER_URL = http://localhost:4000/
SYS_ADMIN_API_PUBLIC_KEY = keys/tx-sysadmin-public.key
+65
View File
@@ -0,0 +1,65 @@
DATABASE_CONNECTION = mongodb://horacio:frx8us2w@192.168.1.50:20215/turnosxpress
SECRET_WORD = k34lklJ4HKJ2j3kj@kl23
TOKEN_SESSION_TIME = 5d
UPLOAD_SERVICE_FILES_ROOT_DEV = ../../../uploads/services/
UPLOAD_SERVICE_FILES_ROOT_PRO = ../../../uploads/services/
UPLOAD_COMPANY_FILES_ROOT_DEV = ../../../uploads/companies/
UPLOAD_COMPANY_FILES_ROOT_PRO = ../../../uploads/companies/
UPLOAD_USERS_FILES_ROOT_DEV = ../../../uploads/users/
UPLOAD_USERS_FILES_ROOT_PRO = ../../../uploads/users/
GET_AVATAR_USERS_FILES_ROOT_DEV = ../../uploads/users/
GET_AVATAR_USERS_FILES_ROOT_PRO = ../../uploads/users/
GET_HEADER_COMPANIES_FILES_ROOT_DEV = ../../uploads/companies/
GET_HEADER_COMPANIES_FILES_ROOT_PRO = ../../uploads/companies/
GET_SERVICE_IMAGE_FILES_ROOT_DEV = ../../uploads/services/
GET_SERVICE_IMAGE_FILES_ROOT_PRO = ../../uploads/services/
DEFAULT_COMPANY_SERVICE_FRACTION = 15
GMAIL_PASSWORD = ktvm kbvm fach iohn
GMAIL_APP_NAME = particular
GMAIL_EMAIL = clases.hdr@gmail.com
PUBLIC_AVATAR_URL = http://192.168.1.50:3000/public/users/
PUBLIC_HEADER_URL = http://192.168.1.50:3000/public/companies/
PUBLIC_SERVICE_IMAGE_URL = http://192.168.1.50:3000/public/services/
DEFAULT_HEADER_COLOR = f5da43
DEFAULT_HEADER_FONT_COLOR = 3e3748
DEFAULT_HEADER_FONT_SHADOW_COLOR = ffffff
DEFAULT_SERVICE_COLOR = 55d400
DEFAULT_SERVICE_FONT_COLOR = ffffff
DEFAULT_SERVICE_FONT_SHADOW_COLOR = 000000
MP_SUBSCRIPTION_CREATE_URL = https://api.mercadopago.com/preapproval
MP_SUBSCRIPTION_FIND_URL = https://api.mercadopago.com/preapproval/
MP_ACCESS_TOKEN = APP_USR-8753424836448507-121412-731599daea1ffa0b278b5dcc78aa1ec8-1611134648
MP_BACK_URL = https://turnosxpress.com.ar/landing/dashboard
MP_CHECK_PAYMENT_MINUTES = 1
DEFAULT_BOT_ADMIN_PORT = 3005
DONWEB_MAIL_API_KEY = eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NDYxMTc3NzUsImV4cCI6NDkwMTc5MTM3NSwicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJraWQiOiI2ODEzYTQ4ZjQ1NGQwYzA1YjAwYTcxYTciLCJhaWQiOiI2ODEzOGIzNjQ2NGFlYzAyMGQwZWY0YjkiLCJ1c2VybmFtZSI6ImhvcmFjaW9kcnNAZ21haWwuY29tIn0.BEIMAFx9SzwRkWK0UEhmem0KpswDK86s4_cdiNeuHLn47G_7dvzfsAR4zqa2EeVeCPC3J62qOtnhMH6GmIKaiZhx0C_ngTJ1nFVzENlZR7pk-R-QdnG8Iq6ZvQrXvQfd3NPi8MNGJAnWEHo9JW2HfvMW534TdBcBcAYTgU51EnLSMnoJMYQ7gkvkPugdIEtD01_2OM_KOx7HFG0svLACgnz_m0TKOxQD1XvDqzbOlCukQgkrftuHEJK5JYa_WrBuvS_RwiaS3CuLMny076gMNtEAJMiHYe4D-9zfMTzJJc-uWu-DB8ASwSwzwPWsNhVbzpsoZGtopWdZ3RJizk1r0g
WAP_TIME_LIMIT = 5000
WAP_REMINDER = 🔔Hola <---CLIENTE---> <---BR---> 📌Te recuerdo que el <---FECHA---> te esperamos a las <---HORARIO---> ⏰ <---BR---> 🏫 En <---DIRECCION---> <---BR---> *<---ORGANIZACION--->* <---BR---> *Powered by turnosxpress* <---BR---> *_Este mensaje es automático_*
GOOGLE_CLIENT_ID = 252287289364-bu8mquhttsaa282egh8su2sumvkjqe58.apps.googleusercontent.com
CORS_ALLOWED_ORIGIN = http://192.168.1.50:3021
APPOINTMENT_TIME = 24
CANCELLATION_TIME = 24
SYSTEM_KEY = 753a2sd1du64-c412-3c3a-9a4hdr9-ac419213957e
FILE_SERVER_URL = http://192.168.1.50:4000/
SYS_ADMIN_API_PUBLIC_KEY = keys/tx-sysadmin-public.key
+68
View File
@@ -0,0 +1,68 @@
DATABASE_CONNECTION = mongodb://txuser:hdR2102Frx8us2WkuN21quAn12@mongodb:27017/turnosxpress
SECRET_WORD = k34lklJ4HKJ2j3kj@kl23
TOKEN_SESSION_TIME = 5d
UPLOAD_SERVICE_FILES_ROOT_DEV = ../../../uploads/services/
UPLOAD_SERVICE_FILES_ROOT_PRO = ../../../uploads/services/
UPLOAD_COMPANY_FILES_ROOT_DEV = ../../../uploads/companies/
UPLOAD_COMPANY_FILES_ROOT_PRO = ../../../uploads/companies/
UPLOAD_USERS_FILES_ROOT_DEV = ../../../uploads/users/
UPLOAD_USERS_FILES_ROOT_PRO = ../../../uploads/users/
GET_AVATAR_USERS_FILES_ROOT_DEV = ../../uploads/users/
GET_AVATAR_USERS_FILES_ROOT_PRO = ../../uploads/users/
GET_HEADER_COMPANIES_FILES_ROOT_DEV = ../../uploads/companies/
GET_HEADER_COMPANIES_FILES_ROOT_PRO = ../../uploads/companies/
GET_SERVICE_IMAGE_FILES_ROOT_DEV = ../../uploads/services/
GET_SERVICE_IMAGE_FILES_ROOT_PRO = ../../uploads/services/
DEFAULT_COMPANY_SERVICE_FRACTION = 15
GMAIL_PASSWORD = ktvm kbvm fach iohn
GMAIL_APP_NAME = particular
GMAIL_EMAIL = clases.hdr@gmail.com
PUBLIC_AVATAR_URL = https://api.turnosxpress.com.ar/public/users/
PUBLIC_HEADER_URL = https://api.turnosxpress.com.ar/public/companies/
PUBLIC_SERVICE_IMAGE_URL = https://api.turnosxpress.com.ar/public/services/
DEFAULT_HEADER_COLOR = f5da43
DEFAULT_HEADER_FONT_COLOR = 3e3748
DEFAULT_HEADER_FONT_SHADOW_COLOR = ffffff
DEFAULT_SERVICE_COLOR = 55d400
DEFAULT_SERVICE_FONT_COLOR = ffffff
DEFAULT_SERVICE_FONT_SHADOW_COLOR = 000000
MP_SUBSCRIPTION_CREATE_URL = https://api.mercadopago.com/preapproval
MP_SUBSCRIPTION_FIND_URL = https://api.mercadopago.com/preapproval/
MP_ACCESS_TOKEN = APP_USR-259699105381562-121310-ec66a442dda5d501541fc2a6026eb6d0-178056017
MP_BACK_URL = https://turnosxpress.com.ar/landing/dashboard
MP_CHECK_PAYMENT_MINUTES = 5
MP_WEBHOOK_URL = https://api.mercadopago.com/mercadopago/webhook
DEFAULT_BOT_ADMIN_PORT = 3005
DONWEB_MAIL_API_KEY = eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NDYxMTc3NzUsImV4cCI6NDkwMTc5MTM3NSwicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJraWQiOiI2ODEzYTQ4ZjQ1NGQwYzA1YjAwYTcxYTciLCJhaWQiOiI2ODEzOGIzNjQ2NGFlYzAyMGQwZWY0YjkiLCJ1c2VybmFtZSI6ImhvcmFjaW9kcnNAZ21haWwuY29tIn0.BEIMAFx9SzwRkWK0UEhmem0KpswDK86s4_cdiNeuHLn47G_7dvzfsAR4zqa2EeVeCPC3J62qOtnhMH6GmIKaiZhx0C_ngTJ1nFVzENlZR7pk-R-QdnG8Iq6ZvQrXvQfd3NPi8MNGJAnWEHo9JW2HfvMW534TdBcBcAYTgU51EnLSMnoJMYQ7gkvkPugdIEtD01_2OM_KOx7HFG0svLACgnz_m0TKOxQD1XvDqzbOlCukQgkrftuHEJK5JYa_WrBuvS_RwiaS3CuLMny076gMNtEAJMiHYe4D-9zfMTzJJc-uWu-DB8ASwSwzwPWsNhVbzpsoZGtopWdZ3RJizk1r0g
WAP_TIME_LIMIT = 5000
WAP_REMINDER = 🔔Hola <---CLIENTE---> <---BR---> 📌Te recuerdo que el <---FECHA---> te esperamos a las <---HORARIO---> ⏰ <---BR---> 🏫 En <---DIRECCION---> <---BR--->Con <---PROFESIONAL---> para <---SERVICIO---> <---BR--->Precio: <---PRECIO---> <---BR---> *<---ORGANIZACION--->*
WAP_ALTA = 🔔Hola <---CLIENTE---> <---BR---> 📌Has realizado una reserva para el día <---FECHA---> a las <---HORARIO---> ⏰ <---BR---> 🏫 En <---DIRECCION---> <---BR--->Con <---PROFESIONAL---> para <---SERVICIO---> <---BR--->Precio: <---PRECIO---> <---BR---> *<---ORGANIZACION--->*
GOOGLE_CLIENT_ID = 252287289364-bu8mquhttsaa282egh8su2sumvkjqe58.apps.googleusercontent.com
CORS_ALLOWED_ORIGIN = https://turnosxpress.com.ar
APPOINTMENT_TIME = 24
CANCELLATION_TIME = 24
SYSTEM_KEY = ad7c956a-76bf-45hdr60-8a50-2ca7b2180997
FILE_SERVER_URL = https://files.turnosxpress.com.ar/
SYS_ADMIN_API_PUBLIC_KEY = keys/tx-sysadmin-public.key
+67
View File
@@ -0,0 +1,67 @@
DATABASE_CONNECTION = mongodb://horacio:frx8us2w@mongodb:27017/turnosxpress
SECRET_WORD = k34lklJ4HKJ2j3kj@kl23
TOKEN_SESSION_TIME = 5d
UPLOAD_SERVICE_FILES_ROOT_DEV = ../../../uploads/services/
UPLOAD_SERVICE_FILES_ROOT_PRO = ../../../uploads/services/
UPLOAD_COMPANY_FILES_ROOT_DEV = ../../../uploads/companies/
UPLOAD_COMPANY_FILES_ROOT_PRO = ../../../uploads/companies/
UPLOAD_USERS_FILES_ROOT_DEV = ../../../uploads/users/
UPLOAD_USERS_FILES_ROOT_PRO = ../../../uploads/users/
GET_AVATAR_USERS_FILES_ROOT_DEV = ../../uploads/users/
GET_AVATAR_USERS_FILES_ROOT_PRO = ../../uploads/users/
GET_HEADER_COMPANIES_FILES_ROOT_DEV = ../../uploads/companies/
GET_HEADER_COMPANIES_FILES_ROOT_PRO = ../../uploads/companies/
GET_SERVICE_IMAGE_FILES_ROOT_DEV = ../../uploads/services/
GET_SERVICE_IMAGE_FILES_ROOT_PRO = ../../uploads/services/
DEFAULT_COMPANY_SERVICE_FRACTION = 15
GMAIL_PASSWORD = ktvm kbvm fach iohn
GMAIL_APP_NAME = particular
GMAIL_EMAIL = clases.hdr@gmail.com
PUBLIC_AVATAR_URL = http://api.tx.local/public/users/
PUBLIC_HEADER_URL = http://api.tx.local/public/companies/
PUBLIC_SERVICE_IMAGE_URL = http://api.tx.local/public/services/
DEFAULT_HEADER_COLOR = f5da43
DEFAULT_HEADER_FONT_COLOR = 3e3748
DEFAULT_HEADER_FONT_SHADOW_COLOR = ffffff
DEFAULT_SERVICE_COLOR = 55d400
DEFAULT_SERVICE_FONT_COLOR = ffffff
DEFAULT_SERVICE_FONT_SHADOW_COLOR = 000000
MP_SUBSCRIPTION_CREATE_URL = https://api.mercadopago.com/preapproval
MP_SUBSCRIPTION_FIND_URL = https://api.mercadopago.com/preapproval/
MP_ACCESS_TOKEN = APP_USR-8753424836448507-121412-731599daea1ffa0b278b5dcc78aa1ec8-1611134648
MP_BACK_URL = http://tx.local/landing/dashboard
MP_CHECK_PAYMENT_MINUTES = 5
DEFAULT_BOT_ADMIN_PORT = 3005
DONWEB_MAIL_API_KEY = eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NDYxMTc3NzUsImV4cCI6NDkwMTc5MTM3NSwicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJraWQiOiI2ODEzYTQ4ZjQ1NGQwY>
WAP_TIME_LIMIT = 5000
WAP_REMINDER = ^=^t^tHola <---CLIENTE---> <---BR---> ^=^s^lTe recuerdo que el <---FECHA---> te esperamos a las <---HORARIO---> ^o <---BR---> ^=^o En <---DIRECCION---> <---BR->
WAP_ALTA = ^=^t^tHola <---CLIENTE---> <---BR---> ^=^s^lHas realizado una reserva para el d a <---FECHA---> a las <---HORARIO---> ^o <---BR---> ^=^o En <---DIRECCION---> <-->
GOOGLE_CLIENT_ID = 252287289364-bu8mquhttsaa282egh8su2sumvkjqe58.apps.googleusercontent.com
CORS_ALLOWED_ORIGIN = http://tx.local
APPOINTMENT_TIME = 24
CANCELLATION_TIME = 24
SYSTEM_KEY = ad7c956a-76bf-45hdr60-8a50-2ca7b2180997
FILE_SERVER_URL = http://files.tx.local/
SYS_ADMIN_API_PUBLIC_KEY = keys/tx-sysadmin-public.key
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
};
+3
View File
@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAGRUvdrR899s3X1s08h+uA0pMsxtLZnBJPy2C/D5kfxU=
-----END PUBLIC KEY-----
+8903
View File
File diff suppressed because it is too large Load Diff
+52
View File
@@ -0,0 +1,52 @@
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc",
"dev": "ts-node-dev src/index.ts",
"routes": "tsoa spec-and-routes",
"import": "ts-node-dev src/ImportData/import.ts",
"start": "node build/src/index.js",
"test": "jest"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@types/bcryptjs": "^2.4.2",
"@types/cors": "^2.8.13",
"@types/express": "4.17.17",
"@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.2",
"@types/sanitize-html": "^2.16.0",
"@types/socket.io": "^3.0.1",
"@types/swagger-ui-express": "^4.1.3",
"concurrently": "^8.2.0",
"csv-parse": "^5.4.0",
"jest": "^30.4.2",
"ts-jest": "^29.4.11",
"ts-node-dev": "^2.0.0",
"typescript": "5.0.4"
},
"dependencies": {
"@types/nodemailer": "^6.4.16",
"axios": "^1.7.7",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dayjs": "^1.11.13",
"dotenv": "^16.0.3",
"express": "^4.21.1",
"google-auth-library": "^10.1.0",
"joi": "^17.9.2",
"jsonwebtoken": "^9.0.1",
"mongoose": "^8.8.3",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.15",
"sanitize-html": "^2.17.0",
"sharp": "^0.33.5",
"socket.io": "^4.8.1",
"swagger-ui-express": "^5.0.0",
"tsoa": "^5.1.1"
}
}
+4
View File
@@ -0,0 +1,4 @@
export type ApiVoidResult = {
success: boolean;
message: string;
};
@@ -0,0 +1,85 @@
import {
FindAppointmentsParams,
IAppointmentsAdapter,
IAppointment,
CreateAppointmentParams,
PaginateAppointmentsParams,
PaginateAppointmentsResults,
} from "./Appointments.Interface";
import { Document, Model, Schema, model } from "mongoose";
export interface IAppointmentDocument extends Omit<IAppointment, "id">, Document {}
export class AppointmentsAdapterMongoose implements IAppointmentsAdapter {
schema: Schema;
AppointmentList: Model<IAppointmentDocument>;
constructor() {
this.schema = new Schema({
companyId: { type: Schema.Types.ObjectId, required: true, ref: "Companie" },
serviceId: { type: Schema.Types.ObjectId, required: true, ref: "Service" },
employeeId: { type: Schema.Types.ObjectId, required: true, ref: "Employee" },
clientId: { type: Schema.Types.ObjectId, required: true, ref: "Client" },
userId: { type: Schema.Types.ObjectId, required: false, ref: "User" },
discountId: { type: Schema.Types.ObjectId, required: false, ref: "Discount" },
start: { type: Date, required: true },
startHour: { type: Number, required: true, default: 0 },
endHour: { type: Number, required: true },
dateDay: { type: Date, required: true },
price: { type: Number, required: true, default: 0 },
length: { type: Number, required: true, default: 0 },
present: { type: Boolean, required: true, default: true },
payment: { type: Boolean, required: true, default: false },
comments: { type: String, required: false, default: "" },
creationDate: { type: Date, required: true, default: Date.now },
});
this.AppointmentList = model<IAppointmentDocument>("Appointment", this.schema);
}
public async create(data: CreateAppointmentParams): Promise<IAppointment> {
return await this.AppointmentList.create(data);
}
public async delete(id: string): Promise<void> {
await this.AppointmentList.deleteOne({ _id: id });
}
public async findByDateRange(
startDate: Date,
endDate: Date,
companyId: string
): Promise<IAppointment[]> {
return this.AppointmentList.find({
dateDay: { $gte: startDate, $lte: endDate },
companyId: companyId,
}).exec();
}
public async find(filters: FindAppointmentsParams): Promise<IAppointment[]> {
return this.AppointmentList.find(filters).exec();
}
public async findOne(filters: FindAppointmentsParams): Promise<IAppointmentDocument | null> {
return this.AppointmentList.findOne(filters).exec();
}
public async paginate(
filters: PaginateAppointmentsParams
): Promise<PaginateAppointmentsResults> {
const { page, limit, ...findFilters } = filters; //Extract page and limit from filters
const count = await this.AppointmentList.countDocuments(findFilters).exec();
const skip = (page - 1) * limit;
const results = await this.AppointmentList.find(findFilters)
.skip(skip)
.limit(filters.limit)
.exec();
return {
data: results,
page: filters.page,
pages: Math.ceil(count / filters.limit),
};
}
}
@@ -0,0 +1,323 @@
import { IHeatMap } from "../../Models/HeatMap/HeatMap.Interface";
import { IClientDocument } from "../Clients/Clients.Adapter.Mongoose";
import { ICompanyDocument } from "../Companies/Companies.Adapter.Mongoose";
import { DiscountType } from "../Discounts/Discounts.Interface";
import { TextObjectFilterResult } from "../TextObjectFilter.model";
export type FindAppointmentsParams = {
_id?: string;
companyId?: string;
serviceId?: string;
employeeId?: string;
clientId?: string;
start?: Date;
dateDay?: Date;
};
export type FindAppointmentsByUserParams = {
sessionUser: string;
};
export type FindAppointmentsByUserPaginatedParams = FindAppointmentsByUserParams & {
page: number;
limit: number;
};
export type FindAppointmentsByCollaboratorParams = {
sessionUser: string;
};
export type FindAppointmentsByCollaboratorPaginatedParams = FindAppointmentsByCollaboratorParams & {
page: number;
limit: number;
};
export enum APPOINTMENT_NOTIFICATION_TYPE {
"CREATION" = "creation",
"REMINDER" = "reminder",
}
export type SendAppointmentNotificationParams = {
appointmentId: string;
sessionUser: string;
type: APPOINTMENT_NOTIFICATION_TYPE;
};
export type MoveAppointmentParams = {
id: string;
companyId: string;
newStart: Date;
newEnd: Date;
sessionUser?: string;
};
export type ChangeServiceParams = {
id: string;
newServiceId: string;
newEmployeeId: string;
sessionUser: string;
};
export type SendWapSystemNotificationParams = {
appointmentId: string;
systemToken: string;
};
export type HeatMapConfig = {
companyId: string;
serviceId: string;
employeeId: string;
fraction: number;
length: number;
heatMapDate: Date;
start: Date;
serviceLimit: number;
serviceLength: number;
};
export type PaginateAppointmentsParams = FindAppointmentsParams & {
page: number;
limit: number;
};
export type PaginateAppointmentsResults = {
data: IAppointment[];
page: number;
pages: number;
};
export type CreateAppointmentParams = {
companyId: string;
serviceId: string;
employeeId: string;
userId?: string; // for user reservations
clientId?: string; // for collaborator reservations
repeatId?: string; // Para ver si tiene un descuento asignado...
start: string | Date;
startHour?: number;
endHour?: number;
dateDay?: Date;
length?: number;
price?: number;
comments?: string;
validation?: boolean;
notification?: boolean;
sessionUser: string;
};
export type UpdateAppointmentParams = {
id: string;
start: string | Date;
length: number;
price: number;
comments: string;
validation?: boolean;
notification?: boolean;
sessionUser: string;
};
export type DeleteAppointmentParams = {
id: string;
validation?: boolean;
sessionUser: string;
};
export type FindAppointmentSchedulesParams = {
companyId: string;
serviceId: string;
employeeId: string;
dateDay: string | Date;
};
export type DeleteAppointmentsByCompanyParams = {
companyId: string;
};
export type ChangeEmployeeToOwnerParams = {
appointmentId: string;
companyId: string;
};
export type CountAppointmentsByMonthParams = {
companyId: string;
month: number;
year: number;
};
export type SetPaymentStatusParams = {
appointmentId: string;
status: boolean;
sessionUser: string;
};
export type ApplyAppointmentDiscountParams = {
companyId: string;
appointmentId: string;
discountId: string;
sessionUser: string;
};
export type DeleteAppointmentDiscountParams = {
appointmentId: string;
companyId: string;
sessionUser: string;
};
export type GetAvailableDatesParams = {
companyId: string;
serviceId: string;
employeeId: string;
length: number; //Cantidad de dias para realizar el chequeo a partir de la fecha de consulta
sessionUser: string; //El el id de un usuario cualquiera. no importa si pertenece o no a la organizacion. Es informacion publica.
};
export type GetAvailableDatesResult = {
availableDates: Date[]; //Fechas que tienen lugar disponible.
inRangeDates: Date[]; //Fechas que estan dentro de un periodo habilitado.
};
export interface AppointmentNotificationIntent {
message: string;
checkClient: IClientDocument;
companyCheck: ICompanyDocument;
}
export interface IAppointment {
id?: string;
companyId: string;
serviceId: string;
employeeId: string;
clientId: string;
userId: string;
discountId: string;
discountType: DiscountType;
discountValue: number;
discountName: string;
discountCode: string;
start: Date;
startHour: number;
endHour: number;
dateDay: Date;
length: number;
price: number;
present: boolean;
payment: boolean;
comments: string;
creationDate: Date;
}
export interface AppointmentEvent {
appointmentId: string;
startTime: number;
endTime: number;
title: string;
clientId: string;
serviceId: string;
employeeId: string;
avatar: string;
color: string;
repeatId: string;
payment: boolean;
discountId: string;
discountType: DiscountType;
discountValue: number;
discountName: string;
discountCode: string;
}
export interface AppointmentAdminByDateView {
events: AppointmentEvent[];
from: number;
to: number;
}
export interface AppointmentEventByClient {
id: string;
companyId: string;
serviceId: string;
serviceName: string;
serviceDescription: string;
collaboratorId: string;
collaboratorName: string;
collaboratorAvatar: string;
appointmentDate: string;
startTime: number;
endTime: number;
title: string;
clientId: string;
clientName: string;
avatar: string;
color: string;
comments: string;
price: number;
length: number;
payment: boolean;
discountId: string;
discountType: DiscountType;
discountValue: number;
discountName: string;
discountCode: string;
clientCtaCteBalance: number;
}
export interface AppointmentAdminByClientView {
events: AppointmentEventByClient[];
}
export interface PaginatedAppointmentEventByClientResult {
data: AppointmentEventByClient[];
page: number;
pages: number;
}
export interface GetAppointmentEventParams {
appointmentId: string;
sessionUser: string;
}
export interface IAppointmentsAdapter {
create(data: CreateAppointmentParams): Promise<IAppointment>;
delete(id: string): Promise<void>;
find(filters: FindAppointmentsParams): Promise<IAppointment[]>;
findOne(filters: FindAppointmentsParams): Promise<IAppointment | null>;
paginate(filters: PaginateAppointmentsParams): Promise<PaginateAppointmentsResults>;
}
export interface IAppointmentsManager {
Appointments: IAppointmentsAdapter;
createAppointment(data: CreateAppointmentParams): Promise<IAppointment>;
updateAppointment(data: UpdateAppointmentParams): Promise<void>;
moveAppointmentForce(data: MoveAppointmentParams): Promise<void>;
changeServiceForce(data: ChangeServiceParams): Promise<void>;
deleteAppointment(data: DeleteAppointmentParams): Promise<void>;
buildHeatMapEmpty(config: HeatMapConfig): IHeatMap;
getHeatMap(config: HeatMapConfig): Promise<IHeatMap>;
updateHeatMap(config: HeatMapConfig, data: CreateAppointmentParams): Promise<void>;
checkAvailability(config: HeatMapConfig, data: CreateAppointmentParams): Promise<boolean>;
findAppointmentsAdminByDate(data: FindAppointmentsParams): Promise<AppointmentAdminByDateView>;
findAppointmentsAdminByClient(data: FindAppointmentsParams): Promise<AppointmentAdminByClientView>;
findAppointmentsByUser(data: FindAppointmentsByUserParams): Promise<AppointmentAdminByClientView>;
findAppointmentsByCollaborator(data: FindAppointmentsByCollaboratorParams): Promise<AppointmentAdminByClientView>;
findAppointmentsByUserPaginated(data: FindAppointmentsByUserPaginatedParams): Promise<PaginatedAppointmentEventByClientResult>;
findAppointmentsByCollaboratorPaginated(data: FindAppointmentsByCollaboratorPaginatedParams): Promise<PaginatedAppointmentEventByClientResult>;
getAppointmentEvent(data: GetAppointmentEventParams): Promise<AppointmentEventByClient>;
getSchedules(data: FindAppointmentSchedulesParams): Promise<TextObjectFilterResult[]>;
getAvailableDates(data: GetAvailableDatesParams): Promise<GetAvailableDatesResult>;
sendWapNotification(data: SendAppointmentNotificationParams): Promise<void>;
sendEmailNotification(data: SendAppointmentNotificationParams): Promise<void>;
sendWapSystemNotification(data: SendWapSystemNotificationParams): Promise<void>;
deleteAppointmentsByCompany(data: DeleteAppointmentsByCompanyParams): Promise<void>;
changeEmployeeToOwner(data: ChangeEmployeeToOwnerParams): Promise<void>;
countAppointmentsByMonth(data: CountAppointmentsByMonthParams): Promise<number>;
setPaymentStatus(data: SetPaymentStatusParams): Promise<void>;
applyDiscount(data: ApplyAppointmentDiscountParams): Promise<void>;
deleteDiscount(data: DeleteAppointmentDiscountParams): Promise<void>;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,176 @@
import { Document, Model, Schema, model, FilterQuery } from "mongoose";
import {
CreateCashFlowParams,
FindCashFlowParams,
ICashFlow,
ICashFlowAdapter,
PaginateCashFlowParams,
PaginateCashFlowResults,
} from "./CashFlow.Interface";
import { PAYMENT_PROCCESS_DATE } from "../Payments/Payments.Interface";
import AppointmentList from "../Appointments/Appointments";
export interface ICashFlowDocument extends Omit<ICashFlow, "id">, Document {}
export class CashFlowAdapterMongoose implements ICashFlowAdapter {
schema: Schema;
movementList: Model<ICashFlowDocument>;
constructor() {
this.schema = new Schema({
companyId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Company",
},
clientId: {
type: Schema.Types.ObjectId,
required: false,
ref: "Client",
},
clientAccountMovementId: {
type: Schema.Types.ObjectId,
required: false,
ref: "ClientAccountMovement",
},
paymentId: {
type: Schema.Types.ObjectId,
required: false,
ref: "Payment",
},
appointmentId: {
type: Schema.Types.ObjectId,
required: false,
ref: "Appointment",
},
employeeId: {
type: Schema.Types.ObjectId,
required: false,
ref: "Employee",
},
amount: { type: Number, required: true, default: 0 },
description: { type: String, required: false, default: "" },
year: { type: Number, required: true, default: 0 },
month: { type: Number, required: true, default: 0 },
type: { type: String, required: true, default: 0 },
reference: { type: String, required: false, default: "" },
paymentMethod: { type: String, required: false },
createdAt: { type: Date, required: true, default: Date.now },
});
this.movementList = model<ICashFlowDocument>("CashFlow", this.schema);
}
public async create(data: CreateCashFlowParams): Promise<ICashFlow> {
let creationDate = new Date();
if (data.proccessDate == PAYMENT_PROCCESS_DATE.NOW) {
creationDate = new Date();
} else if (data.proccessDate == PAYMENT_PROCCESS_DATE.APPOINTMENT) {
const appointment = await AppointmentList.Appointments.AppointmentList.findOne({
_id: data.appointmentId,
});
if (!appointment) {
throw new Error("No se encontró la reserva");
}
creationDate = new Date(appointment.start);
}
return await this.movementList.create({ ...data, createdAt: creationDate });
}
public async delete(id: string): Promise<void> {
await this.movementList.deleteOne({ _id: id });
}
private buildSearchCriteria(
filters: Omit<FindCashFlowParams, "sessionUser">
): FilterQuery<ICashFlow> {
const searchCriteria: FilterQuery<ICashFlow> = {};
if (filters.startDate) {
searchCriteria.createdAt = {
$gte: filters.startDate,
$lte: filters.endDate,
};
}
if (filters.year) {
searchCriteria.year = filters.year;
}
if (filters.month) {
searchCriteria.month = filters.month;
}
if (filters.type) {
searchCriteria.type = filters.type;
}
if (filters.reference) {
searchCriteria.reference = filters.reference;
}
if (filters.clientId) {
searchCriteria.clientId = filters.clientId;
}
if (filters.clientAccountMovementId) {
searchCriteria.clientAccountMovementId = filters.clientAccountMovementId;
}
if (filters.companyId) {
searchCriteria.companyId = filters.companyId;
}
if (filters.paymentId) {
searchCriteria.paymentId = filters.paymentId;
}
if (filters.appointmentId) {
searchCriteria.appointmentId = filters.appointmentId;
}
if (filters.employeeId) {
searchCriteria.employeeId = filters.employeeId;
}
if (filters.paymentMethod) {
searchCriteria.paymentMethod = filters.paymentMethod;
}
return searchCriteria;
}
public async paginate(filters: PaginateCashFlowParams): Promise<PaginateCashFlowResults> {
const searchCriteria = this.buildSearchCriteria(filters);
const count = await this.movementList.countDocuments(searchCriteria).exec();
const skip = (filters.page - 1) * filters.limit;
const results = await this.movementList
.find(searchCriteria)
.skip(skip)
.limit(filters.limit)
.sort({ createdAt: -1 })
.exec();
return {
data: results,
balance: 0,
page: filters.page,
pages: Math.ceil(count / filters.limit),
};
}
public async find(filters: Omit<FindCashFlowParams, "sessionUser">): Promise<ICashFlow[]> {
const searchCriteria = this.buildSearchCriteria(filters);
return this.movementList.find(searchCriteria).exec();
}
public async findOne(
filters: Omit<FindCashFlowParams, "sessionUser">
): Promise<ICashFlowDocument | null> {
return this.movementList.findOne(filters).exec();
}
}
@@ -0,0 +1,111 @@
import {
CLIENT_ACCOUNT_MOVEMENT_TYPES,
CLIENT_ACCOUNT_PAYMENT_METHODS,
} from "../ClientAccountMovements/ClientAccountMovements.Interface";
import {
PAYMENT_METHODS_CLIENT_ACCOUNT,
PAYMENT_PROCCESS_DATE,
} from "../Payments/Payments.Interface";
import { ICashFlowDocument } from "./CashFlow.Adapter.Mongoose";
export type FindCashFlowParams = {
id?: string;
companyId: string;
paymentId?: string;
clientId?: string;
clientAccountMovementId?: string;
appointmentId?: string;
employeeId?: string;
type?: CLIENT_ACCOUNT_MOVEMENT_TYPES;
paymentMethod?: CLIENT_ACCOUNT_PAYMENT_METHODS | PAYMENT_METHODS_CLIENT_ACCOUNT;
startDate?: Date;
endDate?: Date;
year?: number;
month?: number;
reference?: string;
sessionUser: string;
};
export type PaginateCashFlowParams = FindCashFlowParams & {
page: number;
limit: number;
};
export type PaginateCashFlowResults = {
data: ICashFlow[];
balance: number;
page: number;
pages: number;
};
export type CreateCashFlowParams = {
companyId: string;
paymentId?: string;
clientId?: string;
clientAccountMovenemtId?: string;
appointmentId?: string;
employeeId?: string;
amount: number;
description: string;
reference: string;
year: number;
month: number;
type: CLIENT_ACCOUNT_MOVEMENT_TYPES;
paymentMethod: CLIENT_ACCOUNT_PAYMENT_METHODS | PAYMENT_METHODS_CLIENT_ACCOUNT;
proccessDate: PAYMENT_PROCCESS_DATE;
sessionUser: string;
};
export type DeleteMovementByPaymentParams = {
companyId: string;
paymentId: string;
sessionUser: string;
};
export type DeleteMovementParams = {
id: string;
companyId: string;
sessionUser: string;
};
export type DeleteByClientAccountMovementParams = {
companyId: string;
clientAccountMovementId: string;
sessionUser: string;
};
export interface ICashFlow {
id?: string;
companyId: string;
paymentId?: string;
clientId?: string;
clientAccountMovementId?: string;
appointmentId?: string;
employeeId?: string;
amount: number;
description: string;
reference: string;
year: number;
month: number;
type: CLIENT_ACCOUNT_MOVEMENT_TYPES;
paymentMethod: CLIENT_ACCOUNT_PAYMENT_METHODS | PAYMENT_METHODS_CLIENT_ACCOUNT;
createdAt: Date;
}
export interface ICashFlowAdapter {
create(data: CreateCashFlowParams): Promise<ICashFlow>;
delete(id: string): Promise<void>;
find(filters: FindCashFlowParams): Promise<ICashFlow[]>;
findOne(filters: FindCashFlowParams): Promise<ICashFlowDocument | null>;
paginate(filters: PaginateCashFlowParams): Promise<PaginateCashFlowResults>;
}
export interface ICashFlowManager {
movements: ICashFlowAdapter;
add(data: CreateCashFlowParams): Promise<ICashFlow>;
paginate(data: PaginateCashFlowParams): Promise<PaginateCashFlowResults>;
getCashFlowBalance(data: FindCashFlowParams): Promise<number>;
deleteByPayment(data: DeleteMovementByPaymentParams): Promise<void>;
deleteByClientAccountMovement(data: DeleteByClientAccountMovementParams): Promise<void>;
delete(data: DeleteMovementParams): Promise<void>;
}
+134
View File
@@ -0,0 +1,134 @@
import UsersManager from "../Users/Users";
import { IUserDocument } from "../Users/Users.Adapter.Mongoose";
import CompaniesList from "../Companies/Companies";
import { EmployeeRoles } from "../Employees/Employees.Interface";
import EmployeeManager from "../Employees/Employee";
import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
import {
CreateCashFlowParams,
DeleteByClientAccountMovementParams,
DeleteMovementByPaymentParams,
DeleteMovementParams,
FindCashFlowParams,
ICashFlow,
ICashFlowManager,
PaginateCashFlowParams,
PaginateCashFlowResults,
} from "./CashFlow.Interface";
import { CashFlowAdapterMongoose } from "./CashFlow.Adapter.Mongoose";
import { CLIENT_ACCOUNT_MOVEMENT_TYPES } from "../ClientAccountMovements/ClientAccountMovements.Interface";
import PlanSubscriptionsList from "../PlanSubscriptions/PlanSubscriptons";
import { PlanFeatures } from "../Plans/Plans.interface";
class CashFlowManager implements ICashFlowManager {
movements: CashFlowAdapterMongoose;
constructor() {
this.movements = new CashFlowAdapterMongoose();
}
public async getCashFlowBalance(data: FindCashFlowParams): Promise<number> {
const userSession = await this.validateSessionUser(data.sessionUser);
await this.validatePermissions(String(userSession.id), data.companyId);
const movements = await this.movements.find(data);
return movements.reduce((acc, movement) => acc + movement.amount, 0);
}
public async delete(data: DeleteMovementParams): Promise<void> {
const userSession = await this.validateSessionUser(data.sessionUser);
await this.validatePermissions(String(userSession.id), data.companyId);
await this.movements.delete(data.id);
}
public async deleteByPayment(data: DeleteMovementByPaymentParams): Promise<void> {
const userSession = await this.validateSessionUser(data.sessionUser);
await this.validatePermissions(String(userSession.id), data.companyId);
await this.movements.movementList.deleteOne({ paymentId: data.paymentId });
}
public async deleteByClientAccountMovement(
data: DeleteByClientAccountMovementParams
): Promise<void> {
const userSession = await this.validateSessionUser(data.sessionUser);
await this.validatePermissions(String(userSession.id), data.companyId);
await this.movements.movementList.deleteOne({
clientAccountMovementId: data.clientAccountMovementId,
});
}
private async validateSessionUser(sessionUserId: string): Promise<IUserDocument> {
const sessionUser = await UsersManager.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<void> {
const company = await CompaniesList.companies.findOne({
_id: companyId,
});
if (!company) {
throw new Error("No se ha encontrado la organización");
}
// Verificar si el usuario de sesión es un empleado con permisos de administrador
const hasAdminPermission = await EmployeeManager.checkRoleById(
companyId,
sessionUserId,
EmployeeRoles.ADMIN
);
if (!hasAdminPermission) {
throw new Error(NoPermissionMessage());
}
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");
}
}
public async add(data: CreateCashFlowParams): Promise<ICashFlow> {
const userSession = await this.validateSessionUser(data.sessionUser);
await this.validatePermissions(String(userSession.id), data.companyId);
if (data.type === CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT) {
data.amount = Math.abs(data.amount);
} else {
data.amount = -Math.abs(data.amount);
}
const movement = await this.movements.create({
...data,
...{ createdAt: new Date() },
});
return movement;
}
public async paginate(data: PaginateCashFlowParams): Promise<PaginateCashFlowResults> {
const balance = await this.getCashFlowBalance(data);
const result = await this.movements.paginate(data);
return {
...result,
balance,
};
}
}
export default new CashFlowManager();
@@ -0,0 +1,71 @@
import { Document, Model, Schema, model, FilterQuery } from "mongoose";
import {
CreateCashFlowBalanceParams,
FindCashFlowBalanceParams,
ICashFlowBalance,
ICashFlowBalanceAdapter,
} from "./CashFlowBalance.Interface";
export interface ICashFlowBalanceDocument extends Omit<ICashFlowBalance, "id">, Document {}
export class CashFlowBalanceAdapterMongoose implements ICashFlowBalanceAdapter {
schema: Schema;
balanceList: Model<ICashFlowBalanceDocument>;
constructor() {
this.schema = new Schema({
companyId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Company",
},
amount: { type: Number, required: true, default: 0 },
year: { type: Number, required: true, default: 0 },
month: { type: Number, required: true, default: 0 },
updatedAt: { type: Date, required: true, default: Date.now },
});
this.balanceList = model<ICashFlowBalanceDocument>("CashFlowBalance", this.schema);
}
public async create(data: CreateCashFlowBalanceParams): Promise<ICashFlowBalance> {
return await this.balanceList.create(data);
}
public async delete(id: string): Promise<void> {
await this.balanceList.deleteOne({ _id: id });
}
private buildSearchCriteria(
filters: Omit<FindCashFlowBalanceParams, "sessionUser">
): FilterQuery<ICashFlowBalance> {
const searchCriteria: FilterQuery<ICashFlowBalance> = {};
if (filters.companyId) {
searchCriteria.companyId = filters.companyId;
}
if (filters.month) {
searchCriteria.month = filters.month;
}
if (filters.year) {
searchCriteria.year = filters.year;
}
return searchCriteria;
}
public async find(
filters: Omit<FindCashFlowBalanceParams, "sessionUser">
): Promise<ICashFlowBalance[]> {
const searchCriteria = this.buildSearchCriteria(filters);
return this.balanceList.find(searchCriteria).exec();
}
public async findOne(
filters: Omit<FindCashFlowBalanceParams, "sessionUser">
): Promise<ICashFlowBalanceDocument | null> {
return this.balanceList.findOne(filters).exec();
}
}
@@ -0,0 +1,67 @@
import {
CreateCashFlowParams,
DeleteByClientAccountMovementParams,
DeleteMovementByPaymentParams,
DeleteMovementParams,
} from "../CashFlow/CashFlow.Interface";
import { ICashFlowBalanceDocument } from "./CashFlowBalance.Adapter.Mongoose";
export type FindCashFlowBalanceParams = {
companyId: string;
year: number;
month: number;
sessionUser: string;
};
export type CreateCashFlowBalanceParams = {
companyId: string;
amount: number;
year: number;
month: number;
updatedAt: Date;
};
export type CalculateCashFlowBalanceParams = {
companyId: string;
year: number;
month: number;
sessionUser: string;
};
export interface ICashFlowBalance {
id?: string;
companyId: string;
amount: number;
year: number;
month: number;
updatedAt: Date;
}
export interface CashFlowHistoryItem {
amount: number;
month: number;
year: number;
}
export interface CashFlowHistoryView {
companyId: string;
data: CashFlowHistoryItem[];
total: number;
}
export interface ICashFlowBalanceAdapter {
create(data: CreateCashFlowBalanceParams): Promise<ICashFlowBalance>;
delete(id: string): Promise<void>;
find(filters: FindCashFlowBalanceParams): Promise<ICashFlowBalance[]>;
findOne(filters: FindCashFlowBalanceParams): Promise<ICashFlowBalanceDocument | null>;
}
export interface ICashFlowBalanceManager {
balance: ICashFlowBalanceAdapter;
createCashFlowMovement(data: CreateCashFlowParams): Promise<void>;
calculateBalance(data: CalculateCashFlowBalanceParams): Promise<ICashFlowBalance>;
delete(data: DeleteMovementParams): Promise<void>;
deleteByPayment(data: DeleteMovementByPaymentParams): Promise<void>;
deleteByClientAccountMovement(data: DeleteByClientAccountMovementParams): Promise<void>;
history(data: FindCashFlowBalanceParams): Promise<CashFlowHistoryView>;
}
@@ -0,0 +1,261 @@
import UsersManager from "../Users/Users";
import { IUserDocument } from "../Users/Users.Adapter.Mongoose";
import CompaniesList from "../Companies/Companies";
import { EmployeeRoles } from "../Employees/Employees.Interface";
import EmployeeManager from "../Employees/Employee";
import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
import {
CalculateCashFlowBalanceParams,
CashFlowHistoryView,
FindCashFlowBalanceParams,
ICashFlowBalance,
ICashFlowBalanceManager,
} from "./CashFlowBalance.Interface";
import { CashFlowBalanceAdapterMongoose } from "./CashFlowBalance.Adapter.Mongoose";
import dayjs from "dayjs";
import CashFlow from "../CashFlow/CashFlow";
import {
CreateCashFlowParams,
DeleteByClientAccountMovementParams,
DeleteMovementByPaymentParams,
DeleteMovementParams,
} from "../CashFlow/CashFlow.Interface";
import Payments from "../Payments/Payments";
import ClientAccountMovements from "../ClientAccountMovements/ClientAccountMovements";
import { CLIENT_ACCOUNT_MOVEMENT_TYPES } from "../ClientAccountMovements/ClientAccountMovements.Interface";
import PlanSubscriptionsList from "../PlanSubscriptions/PlanSubscriptons";
import { PlanFeatures } from "../Plans/Plans.interface";
class CashFlowBalanceManager implements ICashFlowBalanceManager {
balance: CashFlowBalanceAdapterMongoose;
constructor() {
this.balance = new CashFlowBalanceAdapterMongoose();
}
private async validateSessionUser(sessionUserId: string): Promise<IUserDocument> {
const sessionUser = await UsersManager.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<void> {
const company = await CompaniesList.companies.findOne({
_id: companyId,
});
if (!company) {
throw new Error("No se ha encontrado la organización");
}
// Verificar si el usuario de sesión es un empleado con permisos de administrador
const hasAdminPermission = await EmployeeManager.checkRoleById(
companyId,
sessionUserId,
EmployeeRoles.ADMIN
);
if (!hasAdminPermission) {
throw new Error(NoPermissionMessage());
}
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");
}
}
public async createCashFlowMovement(data: CreateCashFlowParams): Promise<void> {
const userSession = await this.validateSessionUser(data.sessionUser);
await this.validatePermissions(String(userSession.id), data.companyId);
if (data.type === CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT) {
data.amount = Math.abs(data.amount);
} else {
data.amount = -Math.abs(data.amount);
}
const movement = await CashFlow.movements.create(data);
const movDate = dayjs(movement.createdAt);
const movDateYear = movDate.year();
const movDateMonth = movDate.month();
console.log("calculando balance:", {
year: movDateYear,
month: movDateMonth,
companyId: data.companyId,
sessionUser: data.sessionUser,
});
await this.calculateBalance({
year: movDateYear,
month: movDateMonth,
companyId: data.companyId,
sessionUser: data.sessionUser,
});
}
private async getBalanceByYearMonth(
companyId: string,
year: number,
month: number
): Promise<number> {
const movements = await CashFlow.movements.find({
companyId: companyId,
month: month,
year: year,
});
return movements.reduce((acc, movement) => acc + movement.amount, 0);
}
public async calculateBalance(data: CalculateCashFlowBalanceParams): Promise<ICashFlowBalance> {
const userSession = await this.validateSessionUser(data.sessionUser);
await this.validatePermissions(String(userSession.id), data.companyId);
const balance = await this.balance.findOne({
companyId: data.companyId,
year: data.year,
month: data.month,
});
if (!balance) {
const amount = await this.getBalanceByYearMonth(data.companyId, data.year, data.month);
return await this.balance.balanceList.create({
amount,
companyId: data.companyId,
year: data.year,
month: data.month,
updatedAt: new Date(),
});
}
const amount = await this.getBalanceByYearMonth(data.companyId, data.year, data.month);
balance.amount = amount;
balance.updatedAt = new Date();
await balance.save();
return balance as ICashFlowBalance;
}
public async delete(data: DeleteMovementParams): Promise<void> {
const movement = await CashFlow.movements.movementList.findOne({
_id: data.id,
});
if (!movement) {
throw new Error("No se encontró el movimiento");
}
const companyId = movement.companyId;
const movementDate = dayjs(movement.createdAt);
const movementDateYear = movementDate.year();
const movementDateMonth = movementDate.month();
await CashFlow.movements.delete(data.id);
await this.calculateBalance({
companyId: companyId,
year: movementDateYear,
month: movementDateMonth,
sessionUser: data.sessionUser,
});
}
public async deleteByPayment(data: DeleteMovementByPaymentParams): Promise<void> {
const payment = await Payments.payments.paymentsList.findOne({
_id: data.paymentId,
});
if (!payment) {
throw new Error("No se encontró el pago");
}
const companyId = payment.companyId;
const paymentDate = dayjs(payment.createdAt);
const paymentYear = paymentDate.year();
const paymentMonth = paymentDate.month();
await CashFlow.deleteByPayment({
companyId: companyId,
paymentId: data.paymentId,
sessionUser: data.sessionUser,
});
await this.calculateBalance({
companyId: companyId,
year: paymentYear,
month: paymentMonth,
sessionUser: data.sessionUser,
});
}
public async deleteByClientAccountMovement(
data: DeleteByClientAccountMovementParams
): Promise<void> {
const clientAccountMovement = await ClientAccountMovements.movements.movementList.findOne({
_id: data.clientAccountMovementId,
});
if (!clientAccountMovement) {
throw new Error("No se encontró el movimiento");
}
const companyId = clientAccountMovement.clientId;
const movementDate = dayjs(clientAccountMovement.createdAt);
const movementDateYear = movementDate.year();
const movementDateMonth = movementDate.month();
await ClientAccountMovements.movements.delete(data.clientAccountMovementId);
await this.calculateBalance({
companyId: companyId,
year: movementDateYear,
month: movementDateMonth,
sessionUser: data.sessionUser,
});
}
public async history(data: FindCashFlowBalanceParams): Promise<CashFlowHistoryView> {
const returnData: CashFlowHistoryView = {
companyId: data.companyId,
data: [],
total: 0,
};
const userSession = await this.validateSessionUser(data.sessionUser);
await this.validatePermissions(String(userSession.id), data.companyId);
const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
const year = data.year;
const companyId = data.companyId;
let totalBalance = 0;
for (const month of months) {
//obtenemos los balances previamente calculados para cada mes/anio de una organizacion.
const balance = await this.balance.findOne({
year: year,
month: month,
companyId: companyId,
});
returnData.data.push({
amount: balance ? balance.amount : 0,
month: month,
year: year,
});
totalBalance += balance ? balance.amount : 0;
}
return { ...returnData, ...{ total: totalBalance } };
}
}
export default new CashFlowBalanceManager();
@@ -0,0 +1,126 @@
import { Document, Model, Schema, model, FilterQuery, QuerySelector } from "mongoose";
import {
CreateClientAccountMovementParams,
FindClientAccountMovementsParams,
IClientAccountMovement,
IClientAccountMovementsAdapter,
PaginateClientAccountMovementsParams,
PaginateClientAccountMovementsResults,
} from "./ClientAccountMovements.Interface";
export interface IClientAccountMovementDocument
extends Omit<IClientAccountMovement, "id">,
Document {}
export class ClientAccountMovementsAdapterMongoose implements IClientAccountMovementsAdapter {
schema: Schema;
movementList: Model<IClientAccountMovementDocument>;
constructor() {
this.schema = new Schema({
clientId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Client",
},
amount: { type: Number, required: true, default: 0 },
description: { type: String, required: false, default: "" },
type: { type: String, required: true },
reference: { type: String, required: false, default: "" },
status: { type: String, required: true },
paymentMethod: { type: String, required: false },
createdAt: { type: Date, required: true, default: Date.now },
updatedAt: { type: Date, required: true, default: Date.now },
});
this.movementList = model<IClientAccountMovementDocument>(
"ClientAccountMovement",
this.schema
);
}
public async create(data: CreateClientAccountMovementParams): Promise<IClientAccountMovement> {
return await this.movementList.create(data);
}
public async delete(id: string): Promise<void> {
await this.movementList.deleteOne({ _id: id });
}
private buildSearchCriteria(
filters: Omit<FindClientAccountMovementsParams, "sessionUser">
): FilterQuery<IClientAccountMovement> {
const searchCriteria: FilterQuery<IClientAccountMovement> = {};
const createdAtFilter: QuerySelector<Date> = {};
if (filters.startDate) {
createdAtFilter.$gte = filters.startDate;
}
if (filters.endDate) {
createdAtFilter.$lte = filters.endDate;
}
if (Object.keys(createdAtFilter).length > 0) {
searchCriteria.createdAt = createdAtFilter;
}
if (filters.type) {
searchCriteria.type = filters.type;
}
if (filters.status) {
searchCriteria.status = filters.status;
}
if (filters.method) {
searchCriteria.paymentMethod = filters.method;
}
if (filters.reference) {
searchCriteria.reference = filters.reference;
}
if (filters.clientId) {
searchCriteria.clientId = filters.clientId;
}
return searchCriteria;
}
public async paginate(
filters: PaginateClientAccountMovementsParams
): Promise<PaginateClientAccountMovementsResults> {
const searchCriteria = this.buildSearchCriteria(filters);
const count = await this.movementList.countDocuments(searchCriteria).exec();
const skip = (filters.page - 1) * filters.limit;
const results = await this.movementList
.find(searchCriteria)
.skip(skip)
.limit(filters.limit)
.sort({ createdAt: -1 })
.exec();
return {
data: results,
page: filters.page,
pages: Math.ceil(count / filters.limit),
};
}
public async find(
filters: Omit<FindClientAccountMovementsParams, "sessionUser">
): Promise<IClientAccountMovement[]> {
const searchCriteria = this.buildSearchCriteria(filters);
return this.movementList.find(searchCriteria).exec();
}
public async findOne(
filters: Omit<FindClientAccountMovementsParams, "sessionUser">
): Promise<IClientAccountMovementDocument | null> {
return this.movementList.findOne(filters).exec();
}
}
@@ -0,0 +1,101 @@
import { IClientAccountMovementDocument } from "./ClientAccountMovements.Adapter.Mongoose";
export type FindClientAccountMovementsParams = {
id?: string;
clientId: string;
type?: CLIENT_ACCOUNT_MOVEMENT_TYPES;
status?: CLIENT_ACCOUNT_MOVEMENT_STATUS;
reference?: string;
startDate?: Date;
endDate?: Date;
method?: CLIENT_ACCOUNT_PAYMENT_METHODS;
sessionUser: string;
};
export type PaginateClientAccountMovementsParams = FindClientAccountMovementsParams & {
page: number;
limit: number;
};
export type PaginateClientAccountMovementsResults = {
data: IClientAccountMovement[];
page: number;
pages: number;
};
export type CreateClientAccountMovementParams = {
clientId: string;
amount: number;
description?: string;
type: CLIENT_ACCOUNT_MOVEMENT_TYPES;
reference?: string;
status: CLIENT_ACCOUNT_MOVEMENT_STATUS;
paymentMethod: CLIENT_ACCOUNT_PAYMENT_METHODS;
notification?: boolean;
sessionUser: string;
};
export type DeleteClientAccountMovementParams = {
movementId: string;
sessionUser: string;
};
export type CalculateClientAccountBalanceParams = {
clientId: string;
sessionUser: string;
};
export enum CLIENT_ACCOUNT_MOVEMENT_TYPES {
DEPOSIT = "deposit",
WITHDRAWAL = "withdrawal",
}
export enum CLIENT_ACCOUNT_MOVEMENT_STATUS {
PENDING = "pending",
APPROVED = "approved",
REJECTED = "rejected",
}
export enum CLIENT_ACCOUNT_PAYMENT_METHODS {
CASH = "cash",
BANK_TRANSFER = "bank_transfer",
CREDIT_CARD = "credit_card",
DEBIT_CARD = "debit_card",
OTHER = "other",
}
export interface IClientAccountMovement {
id?: string;
clientId: string;
amount: number;
description: string;
type: CLIENT_ACCOUNT_MOVEMENT_TYPES;
reference: string;
status: CLIENT_ACCOUNT_MOVEMENT_STATUS;
paymentMethod: CLIENT_ACCOUNT_PAYMENT_METHODS;
createdAt: Date;
updatedAt: Date;
}
export interface IClientAccountMovementsAdapter {
create(data: CreateClientAccountMovementParams): Promise<IClientAccountMovement>;
delete(id: string): Promise<void>;
find(filters: FindClientAccountMovementsParams): Promise<IClientAccountMovement[]>;
findOne(
filters: FindClientAccountMovementsParams
): Promise<IClientAccountMovementDocument | null>;
paginate(
filters: PaginateClientAccountMovementsParams
): Promise<PaginateClientAccountMovementsResults>;
}
export interface IClientAccountsManager {
movements: IClientAccountMovementsAdapter;
add(data: CreateClientAccountMovementParams): Promise<IClientAccountMovement>;
calculateBalance(data: CalculateClientAccountBalanceParams): Promise<number>;
getBalance(data: FindClientAccountMovementsParams): Promise<number>;
paginate(
data: PaginateClientAccountMovementsParams
): Promise<PaginateClientAccountMovementsResults>;
deleteMovement(data: DeleteClientAccountMovementParams): Promise<void>;
}
@@ -0,0 +1,267 @@
import { ClientAccountMovementsAdapterMongoose } from "./ClientAccountMovements.Adapter.Mongoose";
import {
CreateClientAccountMovementParams,
IClientAccountMovement,
IClientAccountsManager,
FindClientAccountMovementsParams,
DeleteClientAccountMovementParams,
PaginateClientAccountMovementsParams,
PaginateClientAccountMovementsResults,
CalculateClientAccountBalanceParams,
CLIENT_ACCOUNT_MOVEMENT_TYPES,
} from "./ClientAccountMovements.Interface";
import UsersManager from "../Users/Users";
import { IUserDocument } from "../Users/Users.Adapter.Mongoose";
import ClientsList from "../Clients/Clients";
import CompaniesList from "../Companies/Companies";
import { EmployeeRoles } from "../Employees/Employees.Interface";
import EmployeeManager from "../Employees/Employee";
import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
import ClientAccount from "../ClientAccounts/ClientAccount";
import { NotificationsManager } from "../Notifications/Notifications";
import { formatCurrency } from "../../helpers/formatCurrency";
import CashFlowBalance from "../CashFlowBalance/CashFlowBalance";
import dayjs from "dayjs";
import { IClient } from "../Clients/Clients.Interface";
import { PAYMENT_PROCCESS_DATE } from "../Payments/Payments.Interface";
import PlanSubscriptionsList from "../PlanSubscriptions/PlanSubscriptons";
import { PlanFeatures } from "../Plans/Plans.interface";
import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface";
type ValidatePermisionsReturnData = {
companyId: string;
client: IClient;
};
class ClientAccountMovementsManager implements IClientAccountsManager {
movements: ClientAccountMovementsAdapterMongoose;
constructor() {
this.movements = new ClientAccountMovementsAdapterMongoose();
}
private async validateSessionUser(sessionUserId: string): Promise<IUserDocument> {
const sessionUser = await UsersManager.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,
clientId: string
): Promise<ValidatePermisionsReturnData> {
const client = await ClientsList.clients.findOne({ _id: clientId });
if (!client) {
throw new Error("No se ha encontrado el cliente");
}
const clientCompany = await CompaniesList.companies.findOne({
_id: client.companyId,
});
if (!clientCompany) {
throw new Error("No se ha encontrado la empresa del cliente");
}
// Verificar si el usuario de sesión es un empleado con permisos de administrador
const hasAdminPermission = await EmployeeManager.checkRoleById(
clientCompany.id,
sessionUserId,
EmployeeRoles.ADMIN
);
if (!hasAdminPermission) {
throw new Error(NoPermissionMessage());
}
const canAccess = await PlanSubscriptionsList.checkFeature({
userId: String(clientCompany.ownerId),
feature: PlanFeatures.PAYMENTS,
});
if (!canAccess) {
throw new Error("La organizacion no tiene un plan que soporte el módulo de pagos");
}
return {
companyId: clientCompany.id,
client,
};
}
public async calculateBalance(data: CalculateClientAccountBalanceParams): Promise<number> {
const userSession = await this.validateSessionUser(data.sessionUser);
await this.validatePermissions(String(userSession.id), data.clientId);
const movements = await this.movements.find({
clientId: data.clientId,
});
const balance = movements.reduce((acc, movement) => acc + movement.amount, 0);
const account = await ClientAccount.clientAccounts.findOne({
clientId: data.clientId,
});
if (!account) {
throw new Error("No se encontró la cuenta del cliente");
}
account.amount = balance;
await account.save();
return balance;
}
public async getBalance(data: FindClientAccountMovementsParams): Promise<number> {
const userSession = await this.validateSessionUser(data.sessionUser);
await this.validatePermissions(String(userSession.id), data.clientId);
const movements = await this.movements.find(data);
const balance = movements.reduce((acc, movement) => acc + movement.amount, 0);
return balance;
}
private async sendNotification(data: CreateClientAccountMovementParams, balance: number) {
const client = await ClientsList.clients.findOne({
_id: data.clientId,
});
if (!client) {
throw new Error("No se encontró el cliente");
}
const clientUser = await UsersManager.users.findOne({
_id: String(client.userId),
});
if (!clientUser) {
return;
}
const clientName = ClientsList.getClientFullName(client);
const subject =
data.type === CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT
? "Cuenta Corriente - Credito"
: "Cuenta Corriente - Debito";
const message = `Hola ${clientName}. Se ha registrado un nuevo ${
data.type === CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT ? "credito" : "debito"
} de ${formatCurrency(
data.amount
)} en tu cuenta corriente. El saldo actual es: ${formatCurrency(balance)}`;
//Envio la notificacion al cliente.
await NotificationsManager.sendSystemNotification({
userId: String(client.userId),
subject: subject,
message: message,
type: NotificationType.BILLING,
code: String((client as any)._id)
});
}
public async add(data: CreateClientAccountMovementParams): Promise<IClientAccountMovement> {
const userSession = await this.validateSessionUser(data.sessionUser);
const { companyId, client } = await this.validatePermissions(
String(userSession.id),
data.clientId
);
if (data.type === CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT) {
data.amount = Math.abs(data.amount);
} else {
data.amount = -Math.abs(data.amount);
}
const movement = await this.movements.create({
...data,
...{ createdAt: new Date() },
});
const balance = await this.calculateBalance({
clientId: data.clientId,
sessionUser: data.sessionUser,
});
if (data.notification) {
this.sendNotification(data, balance);
}
if (data.type === CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT) {
const clientName = ClientsList.getClientFullName(client);
const paymentDate = dayjs(new Date());
const paymentYear = paymentDate.year();
const paymentMonth = paymentDate.month();
await CashFlowBalance.createCashFlowMovement({
amount: data.amount,
description: "Ingreso en la cuenta del cliente " + clientName,
year: paymentYear,
month: paymentMonth,
companyId: String(companyId),
clientAccountMovenemtId: String(movement.id),
clientId: String(data.clientId),
paymentMethod: data.paymentMethod,
reference: "",
type: CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT,
proccessDate: PAYMENT_PROCCESS_DATE.NOW,
sessionUser: data.sessionUser,
});
}
return movement;
}
public async paginate(
data: PaginateClientAccountMovementsParams
): Promise<PaginateClientAccountMovementsResults> {
return await this.movements.paginate(data);
}
public async deleteMovement(data: DeleteClientAccountMovementParams): Promise<void> {
await this.validateSessionUser(data.sessionUser);
// Primero buscamos el movimiento por su ID
const movement = await this.movements.findOne({
id: data.movementId,
sessionUser: data.sessionUser,
} as FindClientAccountMovementsParams);
if (!movement) {
throw new Error("No se encontró el movimiento");
}
const { companyId } = await this.validatePermissions(data.sessionUser, movement.clientId);
const clientId = movement.clientId;
await this.movements.delete(data.movementId);
await this.calculateBalance({
clientId: clientId,
sessionUser: data.sessionUser,
});
//Si se borra un ingreso de dinero en la cuenta de un cliente, borramos el movimiento en la caja.
if (movement.type === CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT) {
await CashFlowBalance.deleteByClientAccountMovement({
companyId: String(companyId),
clientAccountMovementId: data.movementId,
sessionUser: data.sessionUser,
});
}
}
}
export default new ClientAccountMovementsManager();
@@ -0,0 +1,220 @@
import { ClientAccountsAdapterMongoose } from "./ClientAccounts.Adapter.Mongoose";
import {
CreateClientAccountParams,
IClientAccount,
IClientAccountsManager,
DeleteClientAccountParams,
FindClientAccountsParams,
ClientAccountView,
DeleteClientsAccountsByCompanyParams,
ClientAccountReport,
GetClientAccountReportParams,
GetPrevoiusBalanceParams,
ClientAccountReportItem,
} from "./ClientAccounts.Interface";
import ClientsList from "../Clients/Clients";
import ClientAccountMovements from "../ClientAccountMovements/ClientAccountMovements";
import { PlanFeatures } from "../Plans/Plans.interface";
import { validatePermissionsByClient, validateSessionUser } from "../../helpers/check";
class ClientAccountManager implements IClientAccountsManager {
clientAccounts: ClientAccountsAdapterMongoose;
constructor() {
this.clientAccounts = new ClientAccountsAdapterMongoose();
}
public async deleteClientsAccountsByCompany(
data: DeleteClientsAccountsByCompanyParams
): Promise<void> {
const clients = await ClientsList.clients.find({
companyId: data.companyId,
});
for (const client of clients) {
await ClientAccountMovements.movements.movementList.deleteMany({
clientId: String(client.id),
});
await this.clientAccounts.clientAccountList.deleteMany({
clientId: String(client.id),
});
}
}
public async createClientAccount(data: CreateClientAccountParams): Promise<IClientAccount> {
await validateSessionUser({ sessionUser: data.sessionUser });
await validatePermissionsByClient({
sessionUser: data.sessionUser,
feature: PlanFeatures.PAYMENTS,
clientId: data.clientId,
});
const existingAccount = await this.clientAccounts.findOne({
clientId: data.clientId,
});
if (existingAccount) {
throw new Error("El cliente ya tiene una cuenta registrada");
}
return await this.clientAccounts.create(data);
}
public async findById(data: FindClientAccountsParams): Promise<IClientAccount> {
await validateSessionUser({ sessionUser: data.sessionUser });
await validatePermissionsByClient({
sessionUser: String(data.sessionUser),
feature: PlanFeatures.PAYMENTS,
clientId: data.clientId,
});
let account = (await this.clientAccounts.findOne({
clientId: data.clientId,
})) as IClientAccount;
if (!account) {
account = await this.createClientAccount({
clientId: data.clientId,
amount: 0,
sessionUser: data.sessionUser,
});
}
return account;
}
public async deleteClientAccount(data: DeleteClientAccountParams): Promise<void> {
const { checkCashOnAccount = true } = data;
await validateSessionUser({ sessionUser: data.sessionUser });
const account = await this.clientAccounts.findOne({
clientId: data.clientId,
});
if (!account) {
return;
}
await validatePermissionsByClient({
sessionUser: String(data.sessionUser),
feature: PlanFeatures.PAYMENTS,
clientId: data.clientId,
});
if (account.amount && account.amount > 0 && checkCashOnAccount) {
throw new Error(
"No se puede eliminar la cuenta del cliente ya que tiene saldo positivo."
);
}
//eliminar todos los movimientos de la cuenta
await ClientAccountMovements.movements.movementList.deleteMany({
clientId: account.clientId,
});
await this.clientAccounts.delete(account.id);
}
public async clientAccountView(data: FindClientAccountsParams): Promise<ClientAccountView> {
await validateSessionUser({ sessionUser: data.sessionUser });
await validatePermissionsByClient({
sessionUser: String(data.sessionUser),
feature: PlanFeatures.PAYMENTS,
clientId: data.clientId,
});
const account = await this.findById({
clientId: data.clientId,
sessionUser: data.sessionUser,
});
if (!account || !account.id) {
throw new Error("No se encontró la cuenta del cliente");
}
const movements = await ClientAccountMovements.paginate({
clientId: data.clientId,
page: 1,
limit: 10,
sessionUser: data.sessionUser,
});
return {
id: account.id,
clientId: account.clientId,
amount: account.amount,
lastMovements: movements.data,
};
}
public async getAvailableAmount(data: FindClientAccountsParams): Promise<number> {
const account = await this.findById(data);
return account.amount;
}
public async getPrevoiusBalance(data: GetPrevoiusBalanceParams): Promise<number> {
const balance = await ClientAccountMovements.getBalance({
clientId: data.clientId,
endDate: data.date,
sessionUser: data.sessionUser,
});
return balance;
}
public async getAccountReport(
data: GetClientAccountReportParams
): Promise<ClientAccountReport> {
await validateSessionUser({ sessionUser: data.sessionUser });
const { company, client } = await validatePermissionsByClient({
sessionUser: String(data.sessionUser),
feature: PlanFeatures.PAYMENTS,
clientId: data.clientId,
});
const previousBalance = await this.getPrevoiusBalance({
clientId: data.clientId,
date: data.dateFrom,
sessionUser: data.sessionUser,
});
const movements = await ClientAccountMovements.movements.find({
clientId: data.clientId,
startDate: data.dateFrom,
endDate: data.dateTo,
}); //falta implementar filtros de fecha
const dataReport: ClientAccountReportItem[] = movements.map((movement) => {
return {
date: movement.createdAt,
description: movement.description,
credit: movement.amount > 0 ? movement.amount : 0,
debit: movement.amount < 0 ? Math.abs(movement.amount) : 0,
balance: 0, //se calcula luego
};
});
//calcular saldos parciales
let runningBalance = previousBalance;
for (let i = 0; i < dataReport.length; i++) {
runningBalance += dataReport[i].credit;
runningBalance -= dataReport[i].debit;
dataReport[i].balance = runningBalance;
}
//retornamos el reporte
return {
organizationId: String(company.id),
organizacionName: String(company.name),
clientId: String(data.clientId),
clientName: ClientsList.getClientFullName(client),
balance: previousBalance,
data: dataReport,
dateFrom: data.dateFrom,
dateTo: data.dateTo,
} as ClientAccountReport;
}
}
export default new ClientAccountManager();
@@ -0,0 +1,55 @@
import {
FindClientAccountsParams,
IClientAccountsAdapter,
IClientAccount,
CreateClientAccountParams,
} from "./ClientAccounts.Interface";
import { Document, Model, Schema, model } from "mongoose";
export interface IClientAccountDocument
extends Omit<IClientAccount, "id">,
Document {}
export class ClientAccountsAdapterMongoose implements IClientAccountsAdapter {
schema: Schema;
clientAccountList: Model<IClientAccountDocument>;
constructor() {
this.schema = new Schema({
clientId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Client",
},
amount: { type: Number, required: true, default: 0 },
creationDate: { type: Date, required: true, default: Date.now },
});
this.clientAccountList = model<IClientAccountDocument>(
"ClientAccount",
this.schema
);
}
public async create(
data: CreateClientAccountParams
): Promise<IClientAccount> {
return await this.clientAccountList.create(data);
}
public async delete(id: string): Promise<void> {
await this.clientAccountList.deleteOne({ _id: id });
}
public async find(
filters: FindClientAccountsParams
): Promise<IClientAccount[]> {
return this.clientAccountList.find(filters).exec();
}
public async findOne(
filters: Omit<FindClientAccountsParams, "sessionUser">
): Promise<IClientAccountDocument | null> {
return this.clientAccountList.findOne(filters).exec();
}
}
@@ -0,0 +1,87 @@
import { IClientAccountMovement } from "../ClientAccountMovements/ClientAccountMovements.Interface";
import { IClientAccountDocument } from "./ClientAccounts.Adapter.Mongoose";
export type FindClientAccountsParams = {
clientId: string;
sessionUser: string;
};
export type CreateClientAccountParams = {
clientId: string;
amount: number;
sessionUser: string;
};
export type DeleteClientAccountParams = {
clientId: string;
checkCashOnAccount?: boolean;
sessionUser: string;
};
export type DeleteClientsAccountsByCompanyParams = {
companyId: string;
};
export type ClientAccountReportItem = {
date: Date;
description: string;
credit: number;
debit: number;
balance: number;
};
export type ClientAccountReport = {
organizacionName: string;
organizationId: string;
clientId: string;
clientName: string;
balance: number;
dateFrom: Date;
dateTo: Date;
data: ClientAccountReportItem[];
};
export type GetPrevoiusBalanceParams = {
clientId: string;
date: Date;
sessionUser: string;
};
export type GetClientAccountReportParams = {
clientId: string;
dateFrom: Date;
dateTo: Date;
sessionUser: string;
};
export interface ClientAccountView {
id: string;
clientId: string;
amount: number;
lastMovements: IClientAccountMovement[];
}
export interface IClientAccount {
id?: string;
clientId: string;
amount: number;
}
export interface IClientAccountsAdapter {
create(data: CreateClientAccountParams): Promise<IClientAccount>;
delete(id: string): Promise<void>;
find(filters: FindClientAccountsParams): Promise<IClientAccount[]>;
findOne(filters: FindClientAccountsParams): Promise<IClientAccountDocument | null>;
}
export interface IClientAccountsManager {
clientAccounts: IClientAccountsAdapter;
createClientAccount(data: CreateClientAccountParams): Promise<IClientAccount>;
findById(data: FindClientAccountsParams): Promise<IClientAccount>;
deleteClientAccount(data: DeleteClientAccountParams): Promise<void>;
deleteClientsAccountsByCompany(data: DeleteClientsAccountsByCompanyParams): Promise<void>;
clientAccountView(data: FindClientAccountsParams): Promise<ClientAccountView>;
getAvailableAmount(data: FindClientAccountsParams): Promise<number>;
getPrevoiusBalance(data: GetPrevoiusBalanceParams): Promise<number>;
getAccountReport(data: GetClientAccountReportParams): Promise<ClientAccountReport>;
}
@@ -0,0 +1,99 @@
import {
FindClientsParams,
IClientsAdapter,
IClient,
CreateCustomClientParams,
CreateClientByUserParams,
PaginateClientsParams,
PaginateClientsResults,
} from "./Clients.Interface";
import { Document, FilterQuery, Model, Schema, model } from "mongoose";
export interface IClientDocument extends Omit<IClient, "id">, Document {}
export class ClientsAdapterMongoose implements IClientsAdapter {
schema: Schema;
clientList: Model<IClientDocument>;
constructor() {
this.schema = new Schema({
userId: { type: Schema.Types.ObjectId, required: false, ref: "User" },
companyId: { type: Schema.Types.ObjectId, required: true, ref: "Companie" },
fromClientId: { type: String, required: false, default: undefined },
status: { type: Boolean, required: true, default: true },
firstName: { type: String, required: false, default: undefined },
lastName: { type: String, required: false, default: undefined },
phoneNumber: { type: String, required: false, default: undefined },
phoneCountryCode: { type: String, required: false, default: undefined },
phoneAreaCode: { type: String, required: false, default: undefined },
email: { type: String, required: true, default: undefined },
creationDate: { type: Date, required: true, default: Date.now },
banned: { type: Boolean, required: false, default: false },
});
this.clientList = model<IClientDocument>("Client", this.schema);
}
public async create(
data: CreateClientByUserParams | CreateCustomClientParams
): Promise<IClientDocument> {
return await this.clientList.create(data);
}
public async delete(id: string): Promise<void> {
await this.clientList.deleteOne({ _id: id });
}
public async find(filters: FindClientsParams): Promise<IClient[]> {
return this.clientList.find(filters).exec();
}
public async findOne(filters: FindClientsParams): Promise<IClientDocument | null> {
return this.clientList.findOne(filters).exec();
}
public async paginate(filters: PaginateClientsParams): Promise<PaginateClientsResults> {
const { page, limit, ...findFilters } = filters; //Extract page and limit from filters
const newFilter: FilterQuery<IClientDocument> = {};
if (findFilters.firstName) {
newFilter.firstName = { $regex: findFilters.firstName, $options: "i" };
}
if (findFilters.lastName) {
newFilter.lastName = { $regex: findFilters.lastName, $options: "i" };
}
if (findFilters.email) {
newFilter.email = { $regex: findFilters.email, $options: "i" };
}
if (findFilters.companyId) {
newFilter.companyId = findFilters.companyId;
}
if (findFilters.userId) {
newFilter.userId = findFilters.userId;
}
if (findFilters.status) {
newFilter.status = findFilters.status;
}
const count = await this.clientList.countDocuments(newFilter).exec();
const skip = (page - 1) * limit;
const results = await this.clientList
.find(newFilter)
.skip(skip)
.limit(filters.limit)
.exec();
return {
data: results,
page: filters.page,
pages: Math.ceil(count / filters.limit),
};
}
}
@@ -0,0 +1,142 @@
import {
TextObjectFilterParams,
TextObjectFilterResult,
} from "../../Models/TextObjectFilter.model";
import { IClientDocument } from "./Clients.Adapter.Mongoose";
export type FindClientsParams = {
_id?: string;
companyId?: string;
email?: string;
firstName?: string;
lastName?: string;
userId?: string;
status?: boolean;
};
export type FindClientByIdParams = {
id: string;
companyId: string;
sessionUser: string;
};
export type PaginateClientsParams = FindClientsParams & {
page: number;
limit: number;
};
export type PaginateClientsResults = {
data: IClient[];
page: number;
pages: number;
};
export type PaginateOrganizationClientsParams = FindClientsParams & {
page: number;
limit: number;
};
export type OrganizationClientsResults = {
data: OrganizationClientView[];
page: number;
pages: number;
};
export type CreateClientByUserParams = {
companyId: string;
userId: string;
sessionUser: string;
};
export type CreateCustomClientParams = {
companyId: string;
sessionUser: string;
firstName?: string;
lastName?: string;
phoneNumber?: string;
phoneCountryCode?: string;
phoneAreaCode?: string;
email?: string;
banned?: boolean;
};
export type UpdateClientParams = CreateCustomClientParams & {
id: string;
};
export type DelteClientsByCompanyParams = {
companyId: string;
};
export type DeleteClientParams = {
clientId: string;
checkCashOnAccount?: boolean;
sessionUser: string;
};
export interface IClient {
id?: string;
companyId: string;
userId: string;
fromClientId: string;
status: boolean;
firstName: string;
lastName: string;
phoneNumber: string;
phoneCountryCode: string;
phoneAreaCode: string;
email: string;
banned?: boolean;
}
export interface OrganizationClientView {
id: string;
firstName: string;
lastName: string;
fullName: string;
phoneNumber: string;
phoneCountryCode: string;
phoneAreaCode: string;
email: string;
avatar: string;
banned?: boolean;
}
export type ClientMergeParams = {
fromClientId: string;
toClientId: string;
sessionUser: string;
};
export interface IClientsAdapter {
create(
data: CreateClientByUserParams | CreateCustomClientParams
): Promise<IClient>;
delete(id: string): Promise<void>;
find(filters: FindClientsParams): Promise<IClient[]>;
findOne(filters: FindClientsParams): Promise<IClientDocument | null>;
paginate(filters: PaginateClientsParams): Promise<PaginateClientsResults>;
}
export interface IClientsManager {
clients: IClientsAdapter;
createClientByUser(
data: CreateClientByUserParams
): Promise<IClientDocument>;
createCustomClient(
data: CreateCustomClientParams
): Promise<IClientDocument>;
updateClient(data: UpdateClientParams): Promise<void>;
findOrganizationClients(
filters: PaginateOrganizationClientsParams
): Promise<OrganizationClientsResults>;
textObjectFilter(
data: TextObjectFilterParams
): Promise<TextObjectFilterResult[]>;
findClientById(data: FindClientByIdParams): Promise<OrganizationClientView>;
getClientWapNumber(data: IClient): Promise<string>;
getClientFullName(data: IClient): string;
clientMerge(data: ClientMergeParams): Promise<void>;
deleteClientsByCompany(data: DelteClientsByCompanyParams): Promise<void>;
deleteClient(data: DeleteClientParams): Promise<void>;
}
+649
View File
@@ -0,0 +1,649 @@
import { ClientsAdapterMongoose, IClientDocument } from "./Clients.Adapter.Mongoose";
import {
ClientMergeParams,
CreateClientByUserParams,
CreateCustomClientParams,
DeleteClientParams,
DelteClientsByCompanyParams,
FindClientByIdParams,
IClient,
IClientsManager,
OrganizationClientsResults,
OrganizationClientView,
PaginateOrganizationClientsParams,
UpdateClientParams,
} from "./Clients.Interface";
import UsersManager from "../Users/Users";
import CompaniesManager from "../Companies/Companies";
import { EmployeeRoles } from "../Employees/Employees.Interface";
import EmployeesList from "../Employees/Employee";
import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
import { isNull } from "../../helpers/IsNull";
import { joinStrings } from "../../helpers/String";
import { IUser } from "../Users/Users.Interface";
import getAvatar from "../../helpers/getAvatar";
import {
TextObjectFilterParams,
TextObjectFilterResult,
} from "../../Models/TextObjectFilter.model";
import MetricsList from "../../Models/Metrics/Metrics";
import { NotificationsManager } from "../Notifications/Notifications";
import AppointmentList from "../Appointments/Appointments";
import RepeatsList from "../Repeats/Repeats";
import ClientAccount from "../ClientAccounts/ClientAccount";
import Payments from "../Payments/Payments";
import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface";
class ClientManager implements IClientsManager {
clients: ClientsAdapterMongoose;
constructor() {
this.clients = new ClientsAdapterMongoose();
}
public async deleteClient(data: DeleteClientParams): Promise<void> {
const { checkCashOnAccount = true } = data;
const userChek = await UsersManager.users.findOne({
_id: data.sessionUser,
});
if (!userChek) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const clientChek = await this.clients.findOne({ _id: data.clientId });
if (!clientChek) {
throw new Error("El cliente no existe");
}
//Obtener el saldo de la cuenta del cliente.
if (checkCashOnAccount) {
const clientAccountAmount = await ClientAccount.getAvailableAmount({
clientId: data.clientId,
sessionUser: data.sessionUser,
});
if (clientAccountAmount > 0) {
throw new Error(
"No se puede eliminar el cliente ya que tiene saldo positivo en la cuenta."
);
}
}
const companyCheck = await CompaniesManager.companies.findOne({
_id: clientChek.companyId,
});
if (!companyCheck) {
throw new Error("La compañia no existe");
}
if (
!(await EmployeesList.checkPermission(
companyCheck,
data.sessionUser,
EmployeeRoles.ADMIN
))
) {
throw new Error(NoPermissionMessage());
}
//obtener los turnos del cliente
const appointments = await AppointmentList.Appointments.find({
clientId: data.clientId,
});
//eliminar los turnos
for (const item of appointments) {
if (!item.id) {
continue;
}
await AppointmentList.Appointments.delete(item.id);
}
//Eliminar las repeticiones.
await RepeatsList.deleteRepeatsByClient({ clientId: data.clientId });
//Eliminar la cuenta del cliente.
await ClientAccount.deleteClientAccount({
clientId: data.clientId,
checkCashOnAccount: checkCashOnAccount,
sessionUser: data.sessionUser,
});
//Eliminar los pagos del cliente.
await Payments.deletePaymentsByClient({
clientId: data.clientId,
});
await this.clients.clientList.deleteOne({ _id: data.clientId });
}
public async deleteClientsByCompany(data: DelteClientsByCompanyParams): Promise<void> {
await this.clients.clientList.deleteMany({ companyId: data.companyId });
}
public async clientMerge(data: ClientMergeParams): Promise<void> {
const sessionUser = await UsersManager.users.findOne({
_id: data.sessionUser,
});
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const clientFrom = await this.clients.findOne({
_id: data.fromClientId,
});
if (!clientFrom) {
throw new Error("El cliente origen no existe");
}
const clientTo = await this.clients.findOne({ _id: data.toClientId });
if (!clientTo) {
throw new Error("El cliente destino no existe");
}
if (String(clientFrom.companyId) != String(clientTo.companyId)) {
throw new Error("Los clientes deben pertenecer a la misma organización");
}
const companyId = clientFrom.companyId;
const companyCheck = await CompaniesManager.companies.findOne({
_id: companyId,
});
if (!companyCheck) {
throw new Error("Ha ocurrido un error al determinar la organización");
}
if (
!(await EmployeesList.checkPermission(
companyCheck,
data.sessionUser,
EmployeeRoles.ADMIN
))
) {
throw new Error(NoPermissionMessage());
}
if (clientFrom.userId) {
throw new Error(
"No se puede realizar la fusión con un cliente que posea un usuario asociado"
);
}
//Vamos a actualizar todas las colecciones que apuntes al cliente from y asignamos el cliente to.
//Appointments
const clientFromAppointments = await AppointmentList.Appointments.find({
clientId: data.fromClientId,
});
for (const item of clientFromAppointments) {
const appointment = await AppointmentList.Appointments.findOne({
_id: item.id,
});
if (appointment) {
appointment.clientId = data.toClientId;
appointment.userId = clientTo.userId;
await appointment.save();
}
}
//Copiar datos del cliente que esten vacios en el destino y tengan informacion en el origen.
if (!clientTo.firstName && clientFrom.firstName) {
clientTo.firstName = clientFrom.firstName;
}
if (!clientTo.lastName && clientFrom.lastName) {
clientTo.lastName = clientFrom.lastName;
}
if (!clientTo.phoneCountryCode && clientFrom.phoneCountryCode) {
clientTo.phoneCountryCode = clientFrom.phoneCountryCode;
}
if (!clientTo.phoneAreaCode && clientFrom.phoneAreaCode) {
clientTo.phoneAreaCode = clientFrom.phoneAreaCode;
}
if (!clientTo.phoneNumber && clientFrom.phoneNumber) {
clientTo.phoneNumber = clientFrom.phoneNumber;
}
await clientTo.save();
//Eliminar el clientFrom
await this.clients.delete(data.fromClientId);
}
public async createClientByUser(data: CreateClientByUserParams): Promise<IClientDocument> {
const sessionUser = await UsersManager.users.findOne({
_id: data.sessionUser,
});
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const companyCheck = await CompaniesManager.companies.findOne({
_id: data.companyId,
});
if (!companyCheck) {
throw new Error("La compañia no existe");
}
const userCheck = await UsersManager.users.findOne({
_id: data.userId,
});
if (!userCheck) {
throw new Error("El usuario no existe");
}
const clientCheck = await this.clients.findOne({
companyId: data.companyId,
userId: data.userId,
});
if (clientCheck) {
throw new Error("El cliente ya pertenece a la compañia");
}
const canAdd = await MetricsList.canAddClient(companyCheck.ownerId);
if (!canAdd) {
throw new Error(
"La organización ha alcanzado el limite de clientes permitidos de acuerdo a su plan."
);
}
const newClient = await this.clients.create({
...data,
...{
email: userCheck.email,
firstName: userCheck.firstName,
lastName: userCheck.lastName,
phoneAreaCode: userCheck.phoneAreaCode,
phoneCountryCode: userCheck.phoneCountryCode,
phoneNumber: userCheck.phoneNumber,
},
});
await MetricsList.addClient({
userId: String(companyCheck.ownerId),
quantity: 1,
});
await NotificationsManager.sendSystemNotification({
userId: String(userCheck._id),
subject: "Ha sido dado de alta como cliente",
message: `Su usuario ha sido dado de alta como cliente en la organización ${companyCheck.name}`,
type: NotificationType.BILLING
});
return newClient;
}
public async createCustomClient(data: CreateCustomClientParams): Promise<IClientDocument> {
const sessionUser = await UsersManager.users.findOne({
_id: data.sessionUser,
});
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const companyCheck = await CompaniesManager.companies.findOne({
_id: data.companyId,
});
if (!companyCheck) {
throw new Error("La compañia no existe");
}
if (
!(await EmployeesList.checkPermission(
companyCheck,
data.sessionUser,
EmployeeRoles.ADMIN
))
) {
throw new Error(NoPermissionMessage());
}
const checkClient = await this.clients.findOne({
email: data.email,
companyId: data.companyId,
});
if (checkClient) {
throw new Error("El cliente ya pertenece a la organización");
}
const canAdd = await MetricsList.canAddClient(companyCheck.ownerId);
if (!canAdd) {
throw new Error(
"La organización ha alcanzado el limite de clientes permitidos de acuerdo a su plan."
);
}
const newClient = await this.clients.create(data);
await MetricsList.addClient({
userId: companyCheck.ownerId,
quantity: 1,
});
return newClient;
}
public async updateClient(data: UpdateClientParams): Promise<void> {
const sessionUser = await UsersManager.users.findOne({
_id: data.sessionUser,
});
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const companyCheck = await CompaniesManager.companies.findOne({
_id: data.companyId,
});
if (!companyCheck) {
throw new Error("La compañia no existe");
}
if (
!(await EmployeesList.checkPermission(
companyCheck,
data.sessionUser,
EmployeeRoles.ADMIN
))
) {
throw new Error(NoPermissionMessage());
}
const clientToUpdate = await this.clients.findOne({ _id: data.id });
if (!clientToUpdate) {
throw new Error("El cliente no existe");
}
if (data.email) {
clientToUpdate.email = data.email;
}
if (data.banned !== undefined) {
clientToUpdate.banned = data.banned;
}
if (data.firstName) {
clientToUpdate.firstName = data.firstName;
}
if (data.lastName) {
clientToUpdate.lastName = data.lastName;
}
if (data.phoneAreaCode) {
clientToUpdate.phoneAreaCode = data.phoneAreaCode;
}
if (data.phoneNumber) {
clientToUpdate.phoneNumber = data.phoneNumber;
}
if (data.phoneCountryCode) {
clientToUpdate.phoneCountryCode = data.phoneCountryCode;
}
await clientToUpdate.save();
}
public async findOrganizationClients(
filters: PaginateOrganizationClientsParams
): Promise<OrganizationClientsResults> {
const clients = await this.clients.paginate(filters);
const formattedClients: OrganizationClientView[] = [];
for (const client of clients.data) {
let userInfo: IUser | null = null;
if (client.userId) {
userInfo = await UsersManager.users.findOne({
_id: client.userId,
});
}
const userFirstName = userInfo ? userInfo.firstName : client.firstName;
const userLastName = userInfo ? userInfo.lastName : client.lastName;
const userFullName = userInfo
? joinStrings([userInfo.firstName, userInfo.lastName], " ")
: joinStrings([client.firstName, client.lastName], " ");
const userPhoneNumber = userInfo ? userInfo.phoneNumber : client.phoneNumber;
const userPhoneCountryCode = userInfo
? userInfo.phoneCountryCode
: client.phoneCountryCode;
const userPhoneAreaCode = userInfo ? userInfo.phoneAreaCode : client.phoneAreaCode;
const userEmail = userInfo ? userInfo.email : client.email;
const userAvatar = userInfo
? getAvatar(client.userId, userInfo.avatar, userFullName)
: userFullName;
const clientFirstName = client.firstName;
const clientLastName = client.lastName;
const clientFullName = joinStrings([client.firstName, client.lastName], " ");
const clientPhoneNumber = client.phoneNumber;
const clientPhoneCountryCode = client.phoneCountryCode;
const clientPhoneAreaCode = client.phoneAreaCode;
formattedClients.push({
id: isNull<string>(client.id, ""),
firstName: isNull<string>(clientFirstName, isNull<string>(userFirstName, "")),
lastName: isNull<string>(clientLastName, isNull<string>(userLastName, "")),
fullName: isNull<string>(clientFullName, isNull<string>(userFullName, "")),
phoneNumber: isNull<string>(clientPhoneNumber, isNull<string>(userPhoneNumber, "")),
phoneCountryCode: isNull<string>(
clientPhoneCountryCode,
isNull<string>(userPhoneCountryCode, "")
),
phoneAreaCode: isNull<string>(
clientPhoneAreaCode,
isNull<string>(userPhoneAreaCode, "")
),
email: userEmail,
avatar: userAvatar,
banned: client.banned || false,
});
}
return {
data: formattedClients,
page: clients.page,
pages: clients.pages,
};
}
public async textObjectFilter(data: TextObjectFilterParams): Promise<TextObjectFilterResult[]> {
const clients = await this.clients.find(data);
const formattedResult: TextObjectFilterResult[] = [];
for (const client of clients) {
let userInfo: IUser | null = null;
if (client.userId) {
userInfo = await UsersManager.users.findOne({
_id: client.userId,
});
}
const userFullName = userInfo
? joinStrings([userInfo.firstName, userInfo.lastName], " ")
: joinStrings([client.firstName, client.lastName], " ");
const userAvatar = userInfo
? getAvatar(client.userId, userInfo.avatar, userFullName)
: userFullName;
formattedResult.push({
id: isNull<string>(client.id, ""),
name: joinStrings([client.firstName, client.lastName], " "),
description: client.email,
image: userAvatar,
});
}
return formattedResult;
}
public async findClientById(data: FindClientByIdParams): Promise<OrganizationClientView> {
const sessionUser = await UsersManager.users.findOne({
_id: data.sessionUser,
});
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const companyCheck = await CompaniesManager.companies.findOne({
_id: data.companyId,
});
if (!companyCheck) {
throw new Error("La compañia no existe");
}
if (
!(await EmployeesList.checkPermission(
companyCheck,
data.sessionUser,
EmployeeRoles.EMPLOYEE
))
) {
throw new Error(NoPermissionMessage());
}
const client = await this.clients.findOne({ _id: data.id });
if (!client) {
throw new Error("El cliente no existe");
}
let userInfo: IUser | null = null;
if (client.userId) {
userInfo = await UsersManager.users.findOne({
_id: String(client.userId),
});
}
const userFirstName = userInfo ? userInfo.firstName : client.firstName;
const userLastName = userInfo ? userInfo.lastName : client.lastName;
const userFullName = userInfo
? joinStrings([userInfo.firstName, userInfo.lastName], " ")
: joinStrings([client.firstName, client.lastName], " ");
const userPhoneNumber = userInfo ? userInfo.phoneNumber : client.phoneNumber;
const userPhoneCountryCode = userInfo ? userInfo.phoneCountryCode : client.phoneCountryCode;
const userPhoneAreaCode = userInfo ? userInfo.phoneAreaCode : client.phoneAreaCode;
const userEmail = userInfo ? userInfo.email : client.email;
const userAvatar = userInfo
? getAvatar(client.userId, userInfo.avatar, userFullName)
: userFullName;
const clientFirstName = client.firstName;
const clientLastName = client.lastName;
const clientFullName = joinStrings([client.firstName, client.lastName], " ");
const clientPhoneNumber = client.phoneNumber;
const clientPhoneCountryCode = client.phoneCountryCode;
const clientPhoneAreaCode = client.phoneAreaCode;
return {
id: isNull<string>(client.id, ""),
firstName: isNull<string>(clientFirstName, isNull<string>(userFirstName, "")),
lastName: isNull<string>(clientLastName, isNull<string>(userLastName, "")),
fullName: isNull<string>(clientFullName, isNull<string>(userFullName, "")),
phoneNumber: isNull<string>(clientPhoneNumber, isNull<string>(userPhoneNumber, "")),
phoneCountryCode: isNull<string>(
clientPhoneCountryCode,
isNull<string>(userPhoneCountryCode, "")
),
phoneAreaCode: isNull<string>(
clientPhoneAreaCode,
isNull<string>(userPhoneAreaCode, "")
),
email: userEmail,
avatar: userAvatar,
banned: client.banned || false,
};
}
public async getClientWapNumber(data: IClient): Promise<string> {
const userClient = await UsersManager.users.findOne({
_id: data.userId,
});
const countryCode = data.phoneCountryCode ? data.phoneCountryCode.trim() : "549";
const areaCode = data.phoneAreaCode
? data.phoneAreaCode.trim()
: isNull<string>(userClient?.phoneAreaCode, "");
const phoneNumber = data.phoneNumber
? data.phoneNumber.trim()
: isNull<string>(userClient?.phoneNumber, "");
if (!countryCode) {
throw new Error("No se ha especificado el código de país.");
}
if (!areaCode) {
throw new Error("No se ha especificado el código de área.");
}
if (!phoneNumber) {
throw new Error("No se ha especificado el número de teléfono.");
}
//El 9 especifica que se trata de un numero de celular.
//return `${countryCode}9${areaCode}${phoneNumber}@c.us`;
//Quite el "9" porque ya viene con el codigo de pais.
//Tambien quite el @c.us ya que daba error.
return `${countryCode}${areaCode}${phoneNumber}`;
}
public getClientFullName(data: IClient): string {
if (!data.firstName && !data.lastName) {
throw new Error("No se ha especificado el nombre del cliente.");
}
const firstName = data.firstName ? data.firstName.trim() : "";
const lastName = data.lastName ? data.lastName.trim() : "";
return joinStrings([firstName, lastName], " ");
}
}
const ClientsList = new ClientManager();
export default ClientsList;
@@ -0,0 +1,350 @@
import ServiceList from "../Services/Service";
import {
FindCompaniesParams,
UpdateCompanyParams,
ICompaniesAdapter,
ICompany,
PaginateCompaniesParams,
PaginateCompaniesResults,
SysAdminPaginateCompaniesParams,
} from "./Companies.Interface";
import mongoose, { Document, Model, Schema, model } from "mongoose";
export interface ICompanyDocument extends Omit<ICompany, "id">, Document {}
export class CompaniesAdapterMongoose implements ICompaniesAdapter {
schema: Schema;
companyList: Model<ICompanyDocument>;
constructor() {
this.schema = new Schema({
name: { type: String, required: true, unique: true },
slug: { type: String, required: true, unique: true },
description: { type: String, required: true, default: "" },
categoryId: { type: Number, required: false, default: 50000 },
street: { type: String, required: false },
streetNumber: { type: String, required: false },
builingFloor: { type: String, required: false },
buildingApartament: { type: String, required: false },
block: { type: String, required: false },
city: { type: String, required: false },
state: { type: String, required: false },
country: { type: String, required: false },
zipCode: { type: String, required: false },
latitude: { type: Number, required: false, default: 0 },
longitude: { type: Number, required: false, default: 0 },
phoneCountryCode: { type: String, required: false },
phoneAreaCode: { type: String, required: false },
phoneNumber: { type: String, required: false },
heatMapFraction: { type: Number, required: false },
headerFile: { type: String, required: false },
headerColor: { type: String, required: false },
headerFontColor: { type: String, required: false },
headerFontShadowColor: { type: String, required: false },
iconFile: { type: String, required: false },
logoFile: { type: String, required: false },
wapServerId: { type: Schema.Types.ObjectId, required: false, ref: "WapServer" },
ownerId: { type: Schema.Types.ObjectId, required: true, ref: "User" },
creationDate: { type: Date, required: true, default: Date.now },
published: { type: String, required: false },
automaticNotifications: { type: Boolean, required: false },
appointmentTime: { type: Number, required: false },
cancellationTime: { type: Number, required: false },
appointmentAlert: { type: String, required: false },
templateWapNotifId: { type: Schema.Types.ObjectId, required: false, ref: "Template" },
templateWapAltaId: { type: Schema.Types.ObjectId, required: false, ref: "Template" },
templateEmailNotifId: { type: Schema.Types.ObjectId, required: false, ref: "Template" },
templateEmailAltaId: { type: Schema.Types.ObjectId, required: false, ref: "Template" },
onboardingStep: { type: Number, required: false, default: 0 },
onboardingCompleted: { type: Boolean, required: false, default: false },
fixedPostIds: { type: Array, required: false },
banned: { type: Boolean, required: false, default: false },
});
this.companyList = model<ICompanyDocument>("Company", this.schema);
}
public async create(company: ICompany): Promise<ICompany> {
const newCompany = await this.companyList.create({
...company,
onboardingStep: company.onboardingStep || 0
});
return newCompany;
}
public async update(data: UpdateCompanyParams): Promise<void> {
const updateCompany = await this.companyList.findOne({ _id: data.id });
if (!updateCompany) {
throw new Error("Company not found");
}
if (data.name) {
updateCompany.name = data.name;
}
if (data.description) {
updateCompany.description = data.description;
}
if (data.categoryId) {
updateCompany.categoryId = data.categoryId;
}
if (data.headerFile) {
updateCompany.headerFile = data.headerFile;
}
if (data.iconFile) {
updateCompany.iconFile = data.iconFile;
}
if (data.logoFile) {
updateCompany.logoFile = data.logoFile;
}
if (data.headerColor) {
updateCompany.headerColor = data.headerColor;
}
if (data.headerFontColor) {
updateCompany.headerFontColor = data.headerFontColor;
}
if (data.headerFontShadowColor) {
updateCompany.headerFontShadowColor = data.headerFontShadowColor;
}
if (data.street) {
updateCompany.street = data.street;
}
if (data.streetNumber) {
updateCompany.streetNumber = data.streetNumber;
}
if (data.builingFloor) {
updateCompany.builingFloor = data.builingFloor;
}
if (data.buildingApartament) {
updateCompany.buildingApartament = data.buildingApartament;
}
if (data.block) {
updateCompany.block = data.block;
}
if (data.city) {
updateCompany.city = data.city;
}
if (data.state) {
updateCompany.state = data.state;
}
if (data.country) {
updateCompany.country = data.country;
}
if (data.zipCode) {
updateCompany.zipCode = data.zipCode;
}
if (data.latitude) {
updateCompany.latitude = data.latitude;
}
if (data.longitude) {
updateCompany.longitude = data.longitude;
}
if (data.phoneCountryCode) {
updateCompany.phoneCountryCode = data.phoneCountryCode;
}
if (data.phoneAreaCode) {
updateCompany.phoneAreaCode = data.phoneAreaCode;
}
if (data.phoneNumber) {
updateCompany.phoneNumber = data.phoneNumber;
}
if (data.heatMapFraction) {
updateCompany.heatMapFraction = data.heatMapFraction;
}
if (data.appointmentTime) {
updateCompany.appointmentTime = data.appointmentTime;
}
if (data.cancellationTime) {
updateCompany.cancellationTime = data.cancellationTime;
}
if (data.wapServerId) {
updateCompany.wapServerId = data.wapServerId;
}
if (data.appointmentAlert) {
updateCompany.appointmentAlert = data.appointmentAlert;
}
if (data.templateWapNotifId) {
updateCompany.templateWapNotifId = data.templateWapNotifId;
}
if (data.templateWapAltaId) {
updateCompany.templateWapAltaId = data.templateWapAltaId;
}
if (data.templateEmailNotifId) {
updateCompany.templateEmailNotifId = data.templateEmailNotifId;
}
if (data.templateEmailAltaId) {
updateCompany.templateEmailAltaId = data.templateEmailAltaId;
}
if (data.onboardingStep !== undefined) {
updateCompany.onboardingStep = data.onboardingStep;
}
if (data.onboardingCompleted !== undefined) {
updateCompany.onboardingCompleted = data.onboardingCompleted;
}
if (data.banned !== undefined) {
updateCompany.banned = data.banned;
}
updateCompany.save();
//actualizo los campos correspondientes a la localizacion de los servicios de la organizacion.
try {
const servicesUpdate = await ServiceList.services.find({ companyId: updateCompany.id });
for (const service of servicesUpdate) {
if (!service.id) {
continue;
}
await ServiceList.updateService({
id: service.id,
companyId: updateCompany.id,
latitude: updateCompany.latitude,
longitude: updateCompany.longitude,
block: updateCompany.block,
city: updateCompany.city,
state: updateCompany.state,
country: updateCompany.country,
zipCode: updateCompany.zipCode,
sessionUser: data.sessionUser,
});
}
} catch (error) {
console.log(
"Ha ocurrido un error al actualizar la localizacion de los servicios de la organizacion."
);
}
}
public async delete(id: string): Promise<void> {
await this.companyList.deleteOne({ _id: id }).exec();
}
public async find(filters: FindCompaniesParams): Promise<ICompany[]> {
return await this.companyList.find(filters).exec();
}
public async findOne(filters: FindCompaniesParams): Promise<ICompanyDocument | null> {
return await this.companyList.findOne(filters).exec();
}
public async paginate(filters: PaginateCompaniesParams): Promise<PaginateCompaniesResults> {
const { page, limit, ...findFilters } = filters; //Extract page and limit from filters
const count = await this.companyList.countDocuments(findFilters).exec();
const skip = (page - 1) * limit;
const results = await this.companyList
.find(findFilters)
.skip(skip)
.limit(filters.limit)
.exec();
return {
data: results,
page: filters.page,
pages: Math.ceil(count / filters.limit),
};
}
public async sysAdminPaginate(filters: SysAdminPaginateCompaniesParams): Promise<PaginateCompaniesResults> {
const { page, limit, sortBy, sortOrder, name, banned } = filters as any;
const query: any = {};
if (name) {
query.name = { $regex: name, $options: "i" };
}
if (banned !== undefined) {
query.banned = banned;
}
const count = await this.companyList.countDocuments(query).exec();
const skip = (page - 1) * limit;
const sortObj: any = {};
if (sortBy) {
sortObj[sortBy] = sortOrder === "desc" ? -1 : 1;
}
const results = await this.companyList
.find(query)
.sort(sortObj)
.skip(skip)
.limit(limit)
.exec();
return {
data: results,
page: page,
pages: Math.ceil(count / limit),
};
}
public async paginateFilterAll(
data: PaginateCompaniesParams
): Promise<PaginateCompaniesResults> {
const skip = (data.page - 1) * data.limit;
const searchCriteria: mongoose.FilterQuery<ICompany> = {};
if (data.name) {
const words = data.name.split(" ").map((word) => new RegExp(word, "i"));
searchCriteria.$and = words.map((word) => ({
$or: [{ name: word }, { description: word }],
}));
}
// Realiza la consulta con paginación
const results = await this.companyList
.find(searchCriteria)
.sort({ price: 1 }) // Ordena por precio ascendente
.skip(skip)
.limit(data.limit)
.exec();
// Cuenta el total de documentos que coinciden con los criterios de búsqueda
const count = await this.companyList.countDocuments(searchCriteria).exec();
return {
data: results,
page: data.page,
pages: Math.ceil(count / data.limit),
};
}
}
@@ -0,0 +1,298 @@
import { TextObjectFilterResult } from "../TextObjectFilter.model";
import { ICompanyDocument } from "./Companies.Adapter.Mongoose";
export enum COMPANY_PUBLISHED_STATUS {
PUBLISHED = "published",
PRIVATE = "private",
}
export type CreateCompanyParams = {
name: string;
description: string;
categoryId: number;
ownerId: string;
onboardingStep?: number;
};
export type UpdateCompanyParams = {
id: string;
name?: string;
description?: string;
categoryId?: number;
headerFile?: string;
iconFile?: string;
logoFile?: string;
headerColor?: string;
headerFontColor?: string;
headerFontShadowColor?: string;
street?: string;
streetNumber?: string;
builingFloor?: string;
buildingApartament?: string;
block?: string;
city?: string;
state?: string;
country?: string;
zipCode?: string;
latitude?: number;
longitude?: number;
phoneCountryCode?: string;
phoneAreaCode?: string;
phoneNumber?: string;
heatMapFraction?: number;
appointmentTime?: number;
cancellationTime?: number;
wapServerId?: string;
appointmentAlert?: string;
sessionUser: string;
templateWapNotifId?: string;
templateWapAltaId?: string;
templateEmailNotifId?: string;
templateEmailAltaId?: string;
onboardingStep?: number;
onboardingCompleted?: boolean;
banned?: boolean;
};
export type SetCompanyFileParams = {
companyId: string;
sessionUser: string;
file: Express.Multer.File;
};
export type DeleteCompanyFileParams = {
companyId: string;
sessionUser: string;
};
export type DeleteCompanyParams = {
companyId: string;
sessionUser: string;
};
export type FindCompaniesParams = {
_id?: string;
name?: string;
slug?: string;
categoryId?: number;
street?: string;
streetNumber?: string;
builingFloor?: string;
buildingApartament?: string;
block?: string;
city?: string;
state?: string;
country?: string;
zipCode?: string;
latitude?: number;
longitude?: number;
phoneCountryCode?: string;
phoneAreaCode?: string;
phoneNumber?: string;
heatMapFraction?: number;
appointmentTime?: number;
cancellationTime?: number;
ownerId?: string;
wapServerId?: string;
};
export type SetOrganizationPublishedStatusParams = {
companyId: string;
published: COMPANY_PUBLISHED_STATUS;
sessionUser: string;
};
export type SetNotificationAutoParams = {
companyId: string;
status?: boolean;
sessionUser: string;
};
export interface ICompany {
id?: string;
name: string;
slug: string;
description: string;
categoryId: number;
ownerId: string;
headerFile?: string;
iconFile?: string;
logoFile?: string;
headerColor?: string;
headerFontColor?: string;
headerFontShadowColor?: string;
street?: string;
streetNumber?: string;
builingFloor?: string;
buildingApartament?: string;
block?: string;
city?: string;
state?: string;
country?: string;
zipCode?: string;
latitude?: number;
longitude?: number;
phoneCountryCode?: string;
phoneAreaCode?: string;
phoneNumber?: string;
heatMapFraction?: number;
appointmentTime?: number;
cancellationTime?: number;
wapServerId?: string;
appointmentAlert?: string;
published?: COMPANY_PUBLISHED_STATUS;
automaticNotifications?: boolean;
templateWapNotifId?: string;
templateWapAltaId?: string;
templateEmailNotifId?: string;
templateEmailAltaId?: string;
onboardingStep?: number;
onboardingCompleted?: boolean;
fixedPostIds?: Array<string>;
banned?: boolean;
}
export interface MyOranizationsView {
id: string;
name: string;
slug: string;
description: string;
categoryId: number;
ownerId: string;
employeeId: string;
isOwner: boolean;
isAdmin: boolean;
hostOk: boolean;
guestOk: boolean;
fullOk: boolean;
headerFile?: string;
headerColor?: string;
headerFontColor?: string;
headerFontShadowColor?: string;
iconFile?: string;
logoFile?: string;
phoneAreaCode?: string;
phoneNumber?: string;
heatMapFraction?: number;
appointmentTime?: number;
cancellationTime?: number;
block?: string;
street?: string;
streetNumber?: string;
builingFloor?: string;
buildingApartament?: string;
city?: string;
state?: string;
country?: string;
zipCode?: string;
latitude?: number;
longitude?: number;
published?: COMPANY_PUBLISHED_STATUS;
automaticNotifications?: boolean;
appointmentAlert?: string;
templateWapNotifId?: string;
templateWapAltaId?: string;
templateEmailNotifId?: string;
templateEmailAltaId?: string;
onboardingStep?: number;
onboardingCompleted?: boolean;
banned?: boolean;
}
export interface ClientOrganizationView {
id: string;
name: string;
description: string;
slug: string;
iconFile: string;
logoFile: string;
}
export interface MyOranizationsViewParams {
sessionUser: string;
}
export interface ClientOranizationsViewParams {
clientId: string;
sessionUser: string;
}
export interface OranizationsViewByIdParams {
id: string;
sessionUser: string;
}
export type PaginateCompaniesParams = FindCompaniesParams & {
page: number;
limit: number;
};
export type PaginateCompaniesResults = {
data: ICompany[];
page: number;
pages: number;
};
export type SysAdminPaginateCompaniesParams = PaginateCompaniesParams & {
banned?: boolean;
};
export type SysAdminUpdateCompanyParams = {
id: string;
name?: string;
description?: string;
categoryId?: number;
};
export type SysAdminSetCompanyBannedParams = {
id: string;
banned: boolean;
};
export type FixCompanyPostParams = {
companyId: string;
postId: string;
sessionUser: string;
};
export type MoveCompanyFixedPostParams = {
companyId: string;
postId: string;
moveLength: number;
sessionUser: string;
};
export interface ICompaniesAdapter {
create(company: ICompany): Promise<ICompany>;
delete(id: string): Promise<void>;
find(filters: FindCompaniesParams): Promise<ICompany[]>;
findOne(filters: FindCompaniesParams): Promise<ICompanyDocument | null>;
paginate(filters: PaginateCompaniesParams): Promise<PaginateCompaniesResults>;
sysAdminPaginate(data: SysAdminPaginateCompaniesParams): Promise<PaginateCompaniesResults>;
paginateFilterAll(data: PaginateCompaniesParams): Promise<PaginateCompaniesResults>;
update(data: UpdateCompanyParams): Promise<void>;
}
export interface ICompaniesManager {
companies: ICompaniesAdapter;
createCompany(data: CreateCompanyParams): Promise<ICompany>;
isValidName(data: FindCompaniesParams): Promise<boolean>;
updateCompany(data: UpdateCompanyParams): Promise<void>;
setCompanyHeader(data: SetCompanyFileParams): Promise<void>;
setCompanyIcon(data: SetCompanyFileParams): Promise<void>;
deleteCompanyHeader(data: DeleteCompanyFileParams): Promise<void>;
deleteCompanyIcon(data: DeleteCompanyFileParams): Promise<void>;
getByUserId(data: MyOranizationsViewParams): Promise<MyOranizationsView[]>;
getByClientId(data: ClientOranizationsViewParams): Promise<ClientOrganizationView[]>;
getById(data: OranizationsViewByIdParams): Promise<MyOranizationsView>;
filterAll(data: PaginateCompaniesParams): Promise<TextObjectFilterResult[]>;
getCompanyAddress(data: ICompany): string;
deleteCompany(data: DeleteCompanyParams): Promise<void>;
setPublishedStatus(data: SetOrganizationPublishedStatusParams): Promise<void>;
setNotificationAuto(data: SetNotificationAutoParams): Promise<void>;
toggleFixedPost(data: FixCompanyPostParams): Promise<void>;
moveFixedPost(data: MoveCompanyFixedPostParams): Promise<void>;
sysAdminPaginate(data: SysAdminPaginateCompaniesParams): Promise<PaginateCompaniesResults>;
sysAdminUpdate(data: SysAdminUpdateCompanyParams): Promise<void>;
sysAdminSetBanned(data: SysAdminSetCompanyBannedParams): Promise<void>;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,101 @@
import {
IDiscount,
IDiscountDocument,
IDiscountsAdapter,
DiscountType,
CreateDiscountParams,
DeleteDiscountParams,
FindDiscountsParams,
PaginateDiscountsParams,
PaginateDiscountsResults,
} from "./Discounts.Interface";
import { Schema, model, Model, FilterQuery } from "mongoose";
export class DiscountsAdapterMongoose implements IDiscountsAdapter {
schema: Schema;
discountsList: Model<IDiscountDocument>;
constructor() {
this.schema = new Schema({
companyId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Company",
},
name: { type: String, required: true },
code: { type: String, required: true },
type: { type: String, enum: Object.values(DiscountType), required: true },
value: { type: Number, required: true },
createdAt: { type: Date, default: Date.now, required: true },
});
this.discountsList = model<IDiscountDocument>("Discount", this.schema);
}
public async create(data: Omit<CreateDiscountParams, "sessionUser">): Promise<IDiscount> {
return await this.discountsList.create(data);
}
public async delete(data: Omit<DeleteDiscountParams, "sessionUser">): Promise<void> {
await this.discountsList.deleteOne({ _id: data.id });
}
public async find(filters: Omit<FindDiscountsParams, "sessionUser">): Promise<IDiscount[]> {
const searchCriteria = this.buildSearchCriteria(filters);
return await this.discountsList.find(searchCriteria).exec();
}
public async findOne(
filters: Omit<FindDiscountsParams, "sessionUser">
): Promise<IDiscountDocument | null> {
const searchCriteria = this.buildSearchCriteria(filters);
return await this.discountsList.findOne(searchCriteria).exec();
}
private buildSearchCriteria(
filters: Omit<FindDiscountsParams, "sessionUser">
): FilterQuery<IDiscount> {
const searchCriteria: FilterQuery<IDiscount> = {};
if (filters.id) searchCriteria._id = filters.id;
if (filters.companyId) searchCriteria.companyId = filters.companyId;
if (filters.code) searchCriteria.code = { $regex: filters.code, $options: "i" };
if (filters.type) searchCriteria.type = filters.type;
if (filters.startDate && filters.endDate) {
searchCriteria.createdAt = {
$gte: filters.startDate,
$lte: filters.endDate,
};
}
if (filters.startValue !== undefined && filters.endValue !== undefined) {
searchCriteria.value = {
$gte: filters.startValue,
$lte: filters.endValue,
};
}
return searchCriteria;
}
public async paginate(filters: PaginateDiscountsParams): Promise<PaginateDiscountsResults> {
const searchCriteria = this.buildSearchCriteria(filters);
const count = await this.discountsList.countDocuments(searchCriteria).exec();
const skip = (filters.page - 1) * filters.limit;
const results = await this.discountsList
.find(searchCriteria)
.skip(skip)
.limit(filters.limit)
.sort({ createdAt: -1 })
.exec();
let pages = Math.ceil(count / filters.limit);
if (pages == 0) {
pages = 1;
}
return { data: results, page: filters.page, pages: pages };
}
}
@@ -0,0 +1,81 @@
import { Document } from "mongoose";
import { TextObjectFilterResult } from "../TextObjectFilter.model";
export enum DiscountType {
FIXED = "fixed",
PERCENTAGE = "percentage",
}
export interface IDiscount {
id?: string;
_id?: string;
companyId: string;
name: string;
code: string;
type: DiscountType;
value: number;
createdAt: Date;
}
export interface IDiscountDocument extends Omit<IDiscount, "id" | "_id">, Document {
_id: string;
}
export interface IDiscountsAdapter {
create(data: Omit<CreateDiscountParams, "sessionUser">): Promise<IDiscount>;
delete(data: Omit<DeleteDiscountParams, "sessionUser">): Promise<void>;
find(filters: Omit<FindDiscountsParams, "sessionUser">): Promise<IDiscount[]>;
findOne(filters: Omit<FindDiscountsParams, "sessionUser">): Promise<IDiscountDocument | null>;
paginate(filters: PaginateDiscountsParams): Promise<PaginateDiscountsResults>;
}
export type FindDiscountsParams = {
id?: string;
companyId: string;
code?: string;
type?: DiscountType;
sessionUser: string;
startDate?: Date;
endDate?: Date;
startValue?: number;
endValue?: number;
};
export type PaginateDiscountsParams = FindDiscountsParams & {
page: number;
limit: number;
};
export type PaginateDiscountsResults = {
data: IDiscount[];
page: number;
pages: number;
};
export type CreateDiscountParams = {
companyId: string;
name: string;
code: string;
type: DiscountType;
value: number;
sessionUser: string;
};
export type DeleteDiscountParams = {
id: string;
companyId: string;
sessionUser: string;
};
export type DeleteDiscountsByCompanyParams = {
companyId: string;
};
export interface IDiscountsManager {
discounts: IDiscountsAdapter;
deleteDiscountsByCompany(data: DeleteDiscountsByCompanyParams): Promise<void>;
createDiscount(data: CreateDiscountParams): Promise<IDiscount>;
deleteDiscount(data: DeleteDiscountParams): Promise<void>;
paginate(data: PaginateDiscountsParams): Promise<PaginateDiscountsResults>;
textObjectFilter(data: FindDiscountsParams): Promise<TextObjectFilterResult[]>;
}
+126
View File
@@ -0,0 +1,126 @@
import { isNull } from "../../helpers/IsNull";
import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check";
import { TextObjectFilterResult } from "../TextObjectFilter.model";
import { DiscountsAdapterMongoose } from "./Discounts.Adapter.Mongoose";
import {
IDiscountsManager,
CreateDiscountParams,
DeleteDiscountParams,
IDiscount,
PaginateDiscountsParams,
PaginateDiscountsResults,
FindDiscountsParams,
DiscountType,
DeleteDiscountsByCompanyParams,
} from "./Discounts.Interface";
export class DiscountsManager implements IDiscountsManager {
discounts: DiscountsAdapterMongoose;
constructor() {
this.discounts = new DiscountsAdapterMongoose();
}
public async deleteDiscountsByCompany(data: DeleteDiscountsByCompanyParams): Promise<void> {
await this.discounts.discountsList.deleteMany({
companyId: data.companyId,
});
}
async createDiscount(data: CreateDiscountParams): Promise<IDiscount> {
await validateSessionUser({
sessionUser: data.sessionUser,
});
await validatePermissionsByCompany({
companyId: data.companyId,
sessionUser: data.sessionUser,
});
const checkDiscount = await this.discounts.findOne({
companyId: data.companyId,
code: data.code,
});
if (checkDiscount) {
throw new Error("Ya existe un descuento con el mismo código");
}
// La lógica de negocio se implementará aquí
const discount = this.discounts.create(data);
if (!discount) {
throw new Error("Error creating discount");
}
return discount;
}
async deleteDiscount(data: DeleteDiscountParams): Promise<void> {
await validateSessionUser({
sessionUser: data.sessionUser,
});
const discount = await this.discounts.findOne({ id: data.id, companyId: data.companyId });
if (!discount) {
throw new Error("El descuento que intenta eliminar no existe");
}
await validatePermissionsByCompany({
companyId: String(discount.companyId),
sessionUser: data.sessionUser,
});
await this.discounts.delete(data);
}
async paginate(data: PaginateDiscountsParams): Promise<PaginateDiscountsResults> {
return this.discounts.paginate(data);
}
public async textObjectFilter(data: FindDiscountsParams): Promise<TextObjectFilterResult[]> {
const discounts = await this.discounts.find(data);
const formattedResult: TextObjectFilterResult[] = [];
for (const item of discounts) {
formattedResult.push({
id: isNull<string>(item.id, ""),
name: item.name,
image: "",
});
}
return formattedResult;
}
public async getDiscountData(
discountId: string | undefined,
companyId: string
): Promise<IDiscount> {
const noDiscount = {
name: "",
value: 0,
type: DiscountType.PERCENTAGE,
code: "",
companyId: companyId,
} as IDiscount;
if (!discountId) {
return noDiscount;
}
const discount = await this.discounts.discountsList.findOne({
_id: discountId,
companyId: companyId,
});
if (!discount) {
return noDiscount;
}
return (await discount) as IDiscount;
}
}
export default new DiscountsManager();
@@ -0,0 +1,114 @@
import { Schema, model, Model, FilterQuery } from "mongoose";
import {
CreateDiscountEmailParams,
DeleteDiscountEmailParams,
FindDiscountsEmailParams,
IDiscountEmail,
IDiscountEmailDocument,
IDiscountsEmailAdapter,
PaginateDiscountsEmailParams,
PaginateDiscountsEmailResults,
} from "./DiscountsEmail.Interface";
export class DiscountsEmailAdapterMongoose implements IDiscountsEmailAdapter {
private schema: Schema;
private discountsEmailList: Model<IDiscountEmailDocument>;
constructor() {
this.schema = new Schema({
companyId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Company",
},
discountId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Discount",
},
email: { type: String, required: true },
startDate: { type: Date, required: true },
endDate: { type: Date, required: true },
createdAt: { type: Date, default: Date.now, required: true },
});
this.discountsEmailList = model<IDiscountEmailDocument>("DiscountsEmail", this.schema);
}
public async create(data: Omit<CreateDiscountEmailParams, "sessionUser">): Promise<IDiscountEmail> {
await this.cleanUp();
return await this.discountsEmailList.create(data);
}
public async delete(data: Omit<DeleteDiscountEmailParams, "sessionUser">): Promise<void> {
await this.cleanUp();
await this.discountsEmailList.deleteOne({ _id: data.id });
}
public async find(filters: Omit<FindDiscountsEmailParams, "sessionUser">): Promise<IDiscountEmail[]> {
await this.cleanUp();
const searchCriteria = this.buildSearchCriteria(filters);
return await this.discountsEmailList.find(searchCriteria).exec();
}
public async deleteMany(filters: Omit<FindDiscountsEmailParams, "sessionUser">): Promise<void> {
await this.cleanUp();
const searchCriteria = this.buildSearchCriteria(filters);
await this.discountsEmailList.deleteMany(searchCriteria).exec();
}
public async findOne(
filters: Omit<FindDiscountsEmailParams, "sessionUser">
): Promise<IDiscountEmailDocument | null> {
await this.cleanUp();
const searchCriteria = this.buildSearchCriteria(filters);
return await this.discountsEmailList.findOne(searchCriteria).exec();
}
private async cleanUp() {
await this.discountsEmailList.deleteMany({ endDate: { $lt: new Date() } });
}
private buildSearchCriteria(
filters: Omit<FindDiscountsEmailParams, "sessionUser">
): FilterQuery<IDiscountEmail> {
const searchCriteria: FilterQuery<IDiscountEmail> = {};
if (filters.id) searchCriteria._id = filters.id;
if (filters.companyId) searchCriteria.companyId = filters.companyId;
if (filters.discountId) searchCriteria.discountId = filters.discountId;
if (filters.email) searchCriteria.email = filters.email;
if (filters.searchDate) {
searchCriteria.startDate = {
$lte: filters.searchDate,
};
searchCriteria.endDate = {
$gte: filters.searchDate,
};
}
return searchCriteria;
}
public async paginate(filters: PaginateDiscountsEmailParams): Promise<PaginateDiscountsEmailResults> {
await this.cleanUp();
const searchCriteria = this.buildSearchCriteria(filters);
const count = await this.discountsEmailList.countDocuments(searchCriteria).exec();
const skip = (filters.page - 1) * filters.limit;
const results = await this.discountsEmailList
.find(searchCriteria)
.skip(skip)
.limit(filters.limit)
.sort({ createdAt: -1 })
.exec();
let pages = Math.ceil(count / filters.limit);
if (pages == 0) {
pages = 1;
}
return { data: results, page: filters.page, pages: pages };
}
}
@@ -0,0 +1,90 @@
import { Document } from "mongoose";
export interface IDiscountEmail {
id?: string;
_id?: string;
companyId: string;
discountId: string;
email: string;
startDate: Date;
endDate: Date;
createdAt: Date;
}
export interface IDiscountEmailDocument extends Omit<IDiscountEmail, "id" | "_id">, Document {
_id: string;
}
export interface IDiscountsEmailAdapter {
create(data: Omit<CreateDiscountEmailParams, "sessionUser">): Promise<IDiscountEmail>;
delete(data: Omit<DeleteDiscountEmailParams, "sessionUser">): Promise<void>;
find(filters: Omit<FindDiscountsEmailParams, "sessionUser">): Promise<IDiscountEmail[]>;
findOne(filters: Omit<FindDiscountsEmailParams, "sessionUser">): Promise<IDiscountEmailDocument | null>;
paginate(filters: PaginateDiscountsEmailParams): Promise<PaginateDiscountsEmailResults>;
}
export type DiscountEmailView = {
id: string;
companyId: string;
discountId: string;
discountName: string;
discountType: string;
discountValue: number;
discountCode: string;
email: string;
startDate: Date;
endDate: Date;
};
export type FindDiscountsEmailParams = {
id?: string;
companyId: string;
discountId?: string;
email?: string;
sessionUser: string;
searchDate?: Date;
};
export type PaginateDiscountsEmailParams = FindDiscountsEmailParams & {
page: number;
limit: number;
};
export type PaginateDiscountsEmailResults = {
data: IDiscountEmail[];
page: number;
pages: number;
};
export type PaginateDiscountsEmailViewResults = {
data: DiscountEmailView[];
page: number;
pages: number;
};
export type CreateDiscountEmailParams = {
companyId: string;
discountId: string;
email: string;
startDate: Date;
endDate: Date;
sessionUser: string;
};
export type DeleteDiscountEmailParams = {
id: string;
companyId: string;
sessionUser: string;
};
export type DeleteDiscountsEmailByCompanyParams = {
companyId: string;
};
export interface IDiscountsEmailManager {
discountsEmail: IDiscountsEmailAdapter;
deleteDiscountsEmailByCompany(data: DeleteDiscountsEmailByCompanyParams): Promise<void>;
createDiscountEmail(data: CreateDiscountEmailParams): Promise<IDiscountEmail>;
deleteDiscountEmail(data: DeleteDiscountEmailParams): Promise<void>;
paginate(data: PaginateDiscountsEmailParams): Promise<PaginateDiscountsEmailViewResults>;
}
@@ -0,0 +1,115 @@
import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check";
import {
CreateDiscountEmailParams,
DeleteDiscountEmailParams,
DeleteDiscountsEmailByCompanyParams,
DiscountEmailView,
IDiscountEmail,
IDiscountsEmailManager,
PaginateDiscountsEmailParams,
PaginateDiscountsEmailViewResults,
} from "./DiscountsEmail.Interface";
import { DiscountsEmailAdapterMongoose } from "./DiscountsEmail.Adapter.Mongoose";
import Discounts from "../Discounts/Discounts";
import { isNull } from "../../helpers/IsNull";
export class DiscountsEmailManager implements IDiscountsEmailManager {
discountsEmail: DiscountsEmailAdapterMongoose;
constructor() {
this.discountsEmail = new DiscountsEmailAdapterMongoose();
}
public async deleteDiscountsEmailByCompany(data: DeleteDiscountsEmailByCompanyParams): Promise<void> {
await this.discountsEmail.deleteMany({
companyId: data.companyId,
});
}
async createDiscountEmail(data: CreateDiscountEmailParams): Promise<IDiscountEmail> {
await validateSessionUser({
sessionUser: data.sessionUser,
});
await validatePermissionsByCompany({
companyId: data.companyId,
sessionUser: data.sessionUser,
});
const checkDiscount = await this.discountsEmail.findOne({
companyId: data.companyId,
email: data.email,
});
if (checkDiscount) {
throw new Error(
"Ya existe un descuento registrado para el correo electrónico. Si el mismo no esta vigente, debe ser eliminado para crear uno nuevo."
);
}
// La lógica de negocio se implementará aquí
const discount = this.discountsEmail.create(data);
if (!discount) {
throw new Error("No se ha podido crear el descuento.");
}
return discount;
}
async deleteDiscountEmail(data: DeleteDiscountEmailParams): Promise<void> {
await validateSessionUser({
sessionUser: data.sessionUser,
});
const discount = await this.discountsEmail.findOne({ id: data.id, companyId: data.companyId });
if (!discount) {
throw new Error("El descuento que intenta eliminar no existe");
}
await validatePermissionsByCompany({
companyId: String(discount.companyId),
sessionUser: data.sessionUser,
});
await this.discountsEmail.delete(data);
}
async paginate(data: PaginateDiscountsEmailParams): Promise<PaginateDiscountsEmailViewResults> {
const paginationData = await this.discountsEmail.paginate(data);
const returnData: PaginateDiscountsEmailViewResults = {
data: [],
page: paginationData.page,
pages: paginationData.pages,
};
let items: DiscountEmailView[] = [];
for (const item of paginationData.data) {
const discount = await Discounts.discounts.discountsList.findOne({ _id: item.discountId });
if (!discount) continue;
const returnItem: DiscountEmailView = {
id: isNull<string>(item.id, ""),
email: item.email,
companyId: item.companyId,
discountId: item.discountId,
discountName: discount.name,
discountType: discount.type,
discountValue: discount.value,
discountCode: discount.code,
startDate: item.startDate,
endDate: item.endDate,
};
items.push(returnItem);
}
returnData.data = items;
return returnData;
}
}
export default new DiscountsEmailManager();
+529
View File
@@ -0,0 +1,529 @@
import { EmployeesAdapterMongoose } from "./Employees.Adapter.Mongoose";
import {
CreateEmployeeParams,
IEmployee,
IEmployeesManager,
EmployeeRoles,
ValidateEmployeeParams,
PaginateEmployeesParams,
PaginateEmployeesResults,
CompanyEmployeesView,
FindEmployeesParams,
FindEmployeesByIdParams,
UpdateEmployeeRolesParams,
UpdateEmployeeParams,
DeleteEmployeesByCompany,
DeleteEmployeeParams,
} from "./Employees.Interface";
import UsersManager from "../Users/Users";
import { ICompanyDocument } from "../Companies/Companies.Adapter.Mongoose";
import CompaniesManager from "../Companies/Companies";
import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
import { joinStrings } from "../../helpers/String";
import { isNull } from "../../helpers/IsNull";
import getAvatar from "../../helpers/getAvatar";
import { TextObjectFilterResult } from "../../Models/TextObjectFilter.model";
import MetricsList from "../../Models/Metrics/Metrics";
import { NotificationsManager } from "../Notifications/Notifications";
import EmployeesServicesList from "../EmployeesServices/EmployeesServices";
import SchedulesList from "../Schedules/Schedules";
import { IncompleteCollaboratorView } from "./Employees.Interface";
import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface";
class EmployeeManager implements IEmployeesManager {
employees: EmployeesAdapterMongoose;
constructor() {
this.employees = new EmployeesAdapterMongoose();
}
public async deleteEmployee(data: DeleteEmployeeParams): Promise<void> {
await this.employees.employeeList.deleteOne({ _id: data.employeeId });
}
public async deleteEmployeesByCompany(data: DeleteEmployeesByCompany): Promise<void> {
await this.employees.employeeList.deleteMany({ companyId: data.companyId });
}
public async checkRoleById(
companyId: string,
employeeId: string,
role: EmployeeRoles
): Promise<boolean> {
//Check if role exist in roles array.
const check = await this.employees.findOne({ companyId: companyId, userId: employeeId });
if (!check) {
return false;
}
if (check && check.roles) {
return check.roles.includes(role);
}
return false;
}
public async checkPermission(
company: ICompanyDocument,
employeeId: string,
role: EmployeeRoles
): Promise<boolean> {
//Si el data.sessionUser es el owner de la empresa tiene permiso.
if (String(company.ownerId) == String(employeeId)) {
return true;
}
//Si el data.sessionUser tiene el rol admin de la empresa tiene permiso.
return await this.checkRoleById(String(company._id), employeeId, role);
}
public async createEmployee(data: CreateEmployeeParams): Promise<IEmployee> {
//Primero tengo que verificar que el usuario que intenta registrar un empleado
//tenga permisos para hacerlo y que sea un usuario valido. Este dato viene de
//el campo data.sessionUser.
const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId });
if (!companyCheck) {
throw new Error("La compañia no existe");
}
if (!(await this.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN))) {
throw new Error(NoPermissionMessage());
}
const userCheck = await UsersManager.users.findOne({ _id: data.userId });
if (!userCheck) {
throw new Error("El usuario no existe");
}
const employeeCheck = await this.employees.findOne({
companyId: data.companyId,
userId: data.userId,
});
if (employeeCheck) {
throw new Error("El usuario ya pertenece a la compañia");
}
const canAdd = await MetricsList.canAddEmployee(companyCheck.ownerId);
if (!canAdd) {
throw new Error(
"Ha alcanzado el limite de colaboradores permitidos de acuerdo a su plan."
);
}
const newEmplyee = await this.employees.create(data);
await NotificationsManager.sendSystemNotification({
userId: String(data.userId),
subject: "Su usuario ha sido vinculado a una organización",
message: `La compañía ${companyCheck.name} lo ha vinculado como colaborador. Es necesario que acepte la invitación para poder formar parte de esta organización.`,
type: NotificationType.EMPLOYEE,
code: String((newEmplyee as any)._id)
});
await MetricsList.addEmployee({
userId: companyCheck.ownerId,
quantity: 1,
});
return newEmplyee;
}
public async updateEmployeeRoles(data: UpdateEmployeeRolesParams): Promise<void> {
const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId });
if (!companyCheck) {
throw new Error("La compañia no existe");
}
const checkPermission = await this.checkPermission(
companyCheck,
data.sessionUser,
EmployeeRoles.ADMIN
);
if (!checkPermission) {
throw new Error(NoPermissionMessage());
}
const employee = await this.employees.findOne({
_id: data.employeeId,
});
if (!employee) {
throw new Error("Ha ocurrido un error al validar el empleado.");
}
if (String(employee.userId) == data.sessionUser) {
if (String(companyCheck.ownerId) != data.sessionUser) {
throw new Error(NoPermissionMessage());
}
}
employee.roles = data.roles;
await employee.save();
}
//No exponer este metodo.
public async validateFull(data: ValidateEmployeeParams): Promise<void> {
const employee = await this.employees.findOne({
_id: data.employeeId,
});
if (!employee) {
throw new Error("Ha ocurrido un error al validar el empleado.");
}
employee.hostOk = true;
employee.guestOk = true;
await employee.save();
}
public async validateGuest(data: ValidateEmployeeParams): Promise<void> {
const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const employee = await this.employees.findOne({
_id: data.employeeId,
});
if (!employee) {
throw new Error("Ha ocurrido un error al validar el empleado.");
}
if (String(sessionUser.id) != employee.userId) {
throw new Error("Solo el destinatario puede aceptar la vinculación.");
}
employee.guestOk = true;
const checkCompany = await CompaniesManager.companies.findOne({
_id: employee.companyId,
});
if (!checkCompany) {
throw new Error("La organización no existe");
}
await employee.save();
await NotificationsManager.sendSystemNotification({
userId: String(employee.userId),
subject: "Has aceptado la invitación!",
message: `Ahora que haz aceptado la invitación ya estas habilitado para trabajar con ${checkCompany.name}.`,
type: NotificationType.EMPLOYEE,
code: String(employee._id)
});
}
public async rejectGuest(data: ValidateEmployeeParams): Promise<void> {
const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const employee = await this.employees.findOne({
_id: data.employeeId,
});
if (!employee) {
throw new Error("Ha ocurrido un error al validar el empleado.");
}
if (String(sessionUser.id) != employee.userId) {
throw new Error("Solo el destinatario puede rechazar la vinculación.");
}
const checkCompany = await CompaniesManager.companies.findOne({
_id: employee.companyId,
});
if (!checkCompany) {
throw new Error("La organización no existe");
}
await this.employees.employeeList.deleteOne({ _id: data.employeeId });
await NotificationsManager.sendSystemNotification({
userId: String(checkCompany.ownerId),
subject: "Invitación rechazada",
message: `El usuario ${UsersManager.getUserFullName(sessionUser)} ha rechazado la invitación para unirse a ${checkCompany.name}.`,
type: NotificationType.EMPLOYEE,
code: String(employee._id)
});
}
public async validateHost(data: ValidateEmployeeParams): Promise<void> {
const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const employee = await this.employees.findOne({
_id: data.employeeId,
});
if (!employee) {
throw new Error("Ha ocurrido un error al validar el empleado.");
}
//Si el data.sessionUser tiene el rol admin de la empresa tiene permiso.
const checkPermission = await this.checkRoleById(
employee.companyId,
data.sessionUser,
EmployeeRoles.ADMIN
);
if (!checkPermission) {
throw new Error(NoPermissionMessage());
}
const employUserCheck = await UsersManager.users.findOne({
_id: employee.userId,
});
if (!employUserCheck) {
throw new Error("El usuario no existe");
}
const companyCheck = await CompaniesManager.companies.findOne({
_id: employee.companyId,
});
if (!companyCheck) {
throw new Error("La organización no existe");
}
employee.hostOk = true;
await employee.save();
await NotificationsManager.sendSystemNotification({
userId: String(data.sessionUser),
subject: "Has habilitado a un colaborador!",
message: `Se ha enviado una notificación a ${UsersManager.getUserFullName(
employUserCheck
)} para formar parte de ${companyCheck.name}.`,
type: NotificationType.EMPLOYEE,
code: String(employee._id)
});
}
public async paginate(filters: PaginateEmployeesParams): Promise<PaginateEmployeesResults> {
return await this.employees.paginate(filters);
}
public async findByCompanyId(data: FindEmployeesParams): Promise<CompanyEmployeesView[]> {
const employees = await this.employees.find(data);
const view = await Promise.all(
employees.map(async (employee) => {
const userData = await UsersManager.users.findOne({ _id: employee.userId });
const strFirstName = userData ? isNull<string>(userData.firstName, "") : "";
const strLastName = userData ? isNull<string>(userData.lastName, "") : "";
const strFullName = joinStrings([strFirstName, strLastName], " ");
const strAvatar = userData
? getAvatar(userData.id, userData.avatar, strFullName)
: strFullName;
const boolGuestOk = employee.guestOk || false;
const boolHostOk = employee.hostOk || false;
const boolFullOk = boolGuestOk && boolHostOk;
return {
id: isNull<string>(employee.id, ""),
companyId: employee.companyId,
userId: employee.userId,
firstName: strFirstName,
lastName: strLastName,
fullName: strFullName,
avatar: strAvatar,
email: userData ? userData.email : "",
roles: employee.roles ? employee.roles : [],
guestOk: boolGuestOk,
hostOk: boolHostOk,
fullOk: boolFullOk,
calendarColor: isNull<string>(employee.calendarColor, "#ff9900"),
};
})
);
return view;
}
public async findById(data: FindEmployeesByIdParams): Promise<CompanyEmployeesView> {
const employee = await this.employees.findOne({ _id: data.id });
if (!employee) {
throw new Error("El colaborador no existe");
}
const userData = await UsersManager.users.findOne({ _id: employee.userId });
const strFirstName = userData ? isNull<string>(userData.firstName, "") : "";
const strLastName = userData ? isNull<string>(userData.lastName, "") : "";
const strFullName = joinStrings([strFirstName, strLastName], " ");
const strAvatar = userData
? getAvatar(userData.id, userData.avatar, strFullName)
: strFullName;
const boolGuestOk = employee.guestOk || false;
const boolHostOk = employee.hostOk || false;
const boolFullOk = boolGuestOk && boolHostOk;
return {
id: isNull<string>(employee.id, ""),
companyId: employee.companyId,
userId: employee.userId,
firstName: strFirstName,
lastName: strLastName,
fullName: strFullName,
avatar: strAvatar,
email: userData ? userData.email : "",
roles: employee.roles ? employee.roles : [],
guestOk: boolGuestOk,
hostOk: boolHostOk,
fullOk: boolFullOk,
calendarColor: isNull<string>(employee.calendarColor, "#ff9900"),
};
}
public async textObjectFilter(data: FindEmployeesParams): Promise<TextObjectFilterResult[]> {
const employees = await this.employees.find(data);
const formattedResult: TextObjectFilterResult[] = [];
for (const colaborador of employees) {
const userInfo = await UsersManager.users.findOne({ _id: colaborador.userId });
if (!userInfo) {
continue;
}
const userFullName = joinStrings([userInfo.firstName, userInfo.lastName], " ");
const strAvatar = userInfo
? getAvatar(userInfo.id, userInfo.avatar, userFullName)
: userFullName;
formattedResult.push({
id: isNull<string>(colaborador.id, ""),
name: userFullName,
image: strAvatar,
});
}
return formattedResult;
}
public async update(data: UpdateEmployeeParams): Promise<void> {
const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const employee = await this.employees.findOne({
_id: data.id,
});
if (!employee) {
throw new Error("Ha ocurrido un error al validar el empleado.");
}
const companyCheck = await CompaniesManager.companies.findOne({ _id: employee.companyId });
if (!companyCheck) {
throw new Error("La organización no existe");
}
//Si el data.sessionUser tiene el rol admin de la empresa o es owner tiene permiso.
const checkPermission = await this.checkPermission(
companyCheck,
data.sessionUser,
EmployeeRoles.ADMIN
);
if (!checkPermission) {
throw new Error(NoPermissionMessage());
}
if (data.calendarColor) {
employee.calendarColor = data.calendarColor;
}
if (data.limit) {
employee.limit = data.limit;
}
await employee.save();
}
public async getIncompleteSetup(sessionUser: string): Promise<IncompleteCollaboratorView[]> {
const incompleteCollaborators: IncompleteCollaboratorView[] = [];
const userCompanies = await CompaniesManager.getByUserId({ sessionUser });
for (const company of userCompanies) {
if (company.isAdmin || company.isOwner) {
const employees = await this.findByCompanyId({ companyId: company.id } as any);
for (const employee of employees) {
if (employee.hostOk && employee.guestOk) {
const services = await EmployeesServicesList.find({ employeeId: employee.id } as any);
const schedules = await SchedulesList.find({ employeeId: employee.id, companyId: company.id } as any);
const missingServices = services.length === 0;
const missingSchedules = schedules.length === 0;
if (missingServices || missingSchedules) {
incompleteCollaborators.push({
employeeId: employee.id,
companyId: company.id,
companyName: company.name,
name: employee.fullName,
missingServices,
missingSchedules
});
}
}
}
}
}
return incompleteCollaborators;
}
}
const EmployeesList = new EmployeeManager();
export default EmployeesList;
@@ -0,0 +1,65 @@
import {
FindEmployeesParams,
IEmployeesAdapter,
IEmployee,
CreateEmployeeParams,
PaginateEmployeesParams,
PaginateEmployeesResults,
} from "./Employees.Interface";
import { Document, Model, Schema, model } from "mongoose";
export interface IEmployeeDocument extends Omit<IEmployee, "id">, Document {}
export class EmployeesAdapterMongoose implements IEmployeesAdapter {
schema: Schema;
employeeList: Model<IEmployeeDocument>;
constructor() {
this.schema = new Schema({
userId: { type: Schema.Types.ObjectId, required: true, ref: "User" },
companyId: { type: Schema.Types.ObjectId, required: true, ref: "Companie" },
roles: { type: Array, required: true },
calendarColor: { type: String, required: false },
limit: { type: Number, required: true, default: -1 },
hostOk: { type: Boolean, required: true, default: false },
guestOk: { type: Boolean, required: true, default: false },
creationDate: { type: Date, required: true, default: Date.now },
});
this.employeeList = model<IEmployeeDocument>("Employee", this.schema);
}
public async create(data: CreateEmployeeParams): Promise<IEmployee> {
return await this.employeeList.create(data);
}
public async delete(id: string): Promise<void> {
console.log("delete employee", id);
}
public async find(filters: FindEmployeesParams): Promise<IEmployee[]> {
return this.employeeList.find(filters).exec();
}
public async findOne(filters: FindEmployeesParams): Promise<IEmployeeDocument | null> {
return this.employeeList.findOne(filters).exec();
}
public async paginate(filters: PaginateEmployeesParams): Promise<PaginateEmployeesResults> {
const { page, limit, ...findFilters } = filters; //Extract page and limit from filters
const count = await this.employeeList.countDocuments(findFilters).exec();
const skip = (page - 1) * limit;
const results = await this.employeeList
.find(findFilters)
.skip(skip)
.limit(filters.limit)
.exec();
return {
data: results,
page: filters.page,
pages: Math.ceil(count / filters.limit),
};
}
}
@@ -0,0 +1,128 @@
import { TextObjectFilterResult } from "../../Models/TextObjectFilter.model";
import { IEmployeeDocument } from "./Employees.Adapter.Mongoose";
export type FindEmployeesParams = {
_id?: string;
name?: string;
companyId?: string;
userId?: string;
};
export type PaginateEmployeesParams = FindEmployeesParams & {
page: number;
limit: number;
};
export type PaginateEmployeesResults = {
data: IEmployee[];
page: number;
pages: number;
};
export enum EmployeeRoles {
ADMIN = "admin",
OWNER = "owner",
EMPLOYEE = "employee",
}
export type CreateEmployeeParams = {
companyId: string;
userId: string;
roles?: EmployeeRoles[];
calendarColor?: string;
limit?: number;
hostOk?: boolean;
sessionUser: string;
};
export type UpdateEmployeeParams = {
id: string;
calendarColor?: string;
limit?: number;
sessionUser: string;
};
export type UpdateEmployeeRolesParams = {
employeeId: string;
companyId: string;
roles: EmployeeRoles[];
calendarColor?: string;
limit?: number;
sessionUser: string;
};
export type FindEmployeesByIdParams = {
id?: string;
};
export type ValidateEmployeeParams = {
employeeId: string;
sessionUser: string;
};
export type DeleteEmployeesByCompany = {
companyId: string;
};
export type DeleteEmployeeParams = {
employeeId: string;
};
export interface IEmployee {
id?: string;
companyId: string;
userId: string;
roles?: EmployeeRoles[];
calendarColor?: string;
limit?: number;
guestOk?: boolean;
hostOk?: boolean;
}
export interface CompanyEmployeesView {
id: string;
companyId: string;
userId: string;
firstName: string;
lastName: string;
fullName: string;
avatar: string;
email: string;
roles: EmployeeRoles[];
guestOk: boolean;
hostOk: boolean;
fullOk: boolean;
calendarColor: string;
}
export interface IncompleteCollaboratorView {
employeeId: string;
companyId: string;
companyName: string;
name: string;
missingServices: boolean;
missingSchedules: boolean;
}
export interface IEmployeesAdapter {
create(data: CreateEmployeeParams): Promise<IEmployee>;
delete(id: string): Promise<void>;
find(filters: FindEmployeesParams): Promise<IEmployee[]>;
findOne(filters: FindEmployeesParams): Promise<IEmployeeDocument | null>;
paginate(filters: PaginateEmployeesParams): Promise<PaginateEmployeesResults>;
}
export interface IEmployeesManager {
employees: IEmployeesAdapter;
checkRoleById(companyId: string, employeeId: string, role: EmployeeRoles): Promise<boolean>;
createEmployee(data: CreateEmployeeParams): Promise<IEmployee>;
updateEmployeeRoles(data: UpdateEmployeeRolesParams): Promise<void>;
update(data: UpdateEmployeeParams): Promise<void>;
findByCompanyId(data: FindEmployeesParams): Promise<CompanyEmployeesView[]>;
findById(data: FindEmployeesByIdParams): Promise<CompanyEmployeesView>;
textObjectFilter(data: FindEmployeesParams): Promise<TextObjectFilterResult[]>;
deleteEmployeesByCompany(data: DeleteEmployeesByCompany): Promise<void>;
deleteEmployee(data: DeleteEmployeeParams): Promise<void>;
rejectGuest(data: ValidateEmployeeParams): Promise<void>;
getIncompleteSetup(sessionUser: string): Promise<IncompleteCollaboratorView[]>;
}
@@ -0,0 +1,84 @@
import {
FindEmployeesServicesParams,
IEmployeesServicesAdapter,
IEmployeeService,
CreateEmployeeServiceParams,
UpdateEmployeeServiceParams,
PaginateEmployeesServicesParams,
PaginateEmployeesServicesResults,
} from "./EmployeesServices.Interface";
import { Document, Model, Schema, model } from "mongoose";
export interface IEmployeeServiceDocument extends Omit<IEmployeeService, "id">, Document {}
export class EmployeesServicesAdapterMongoose implements IEmployeesServicesAdapter {
schema: Schema;
employeesServicesList: Model<IEmployeeServiceDocument>;
constructor() {
this.schema = new Schema({
serviceId: { type: Schema.Types.ObjectId, required: true, ref: "Service" },
employeeId: { type: Schema.Types.ObjectId, required: true, ref: "Employee" },
companyId: { type: Schema.Types.ObjectId, required: true, ref: "Companie" },
price: { type: Number, required: false },
creationDate: { type: Date, required: true, default: Date.now },
});
this.employeesServicesList = model<IEmployeeServiceDocument>(
"EmployeeService",
this.schema
);
}
public async create(data: CreateEmployeeServiceParams): Promise<IEmployeeService> {
return await this.employeesServicesList.create(data);
}
public async update(data: UpdateEmployeeServiceParams): Promise<void> {
const updateEmployeeService = await this.employeesServicesList.findOne({ _id: data.id });
if (!updateEmployeeService) {
throw new Error("Employee service not found");
}
if (data.price) {
updateEmployeeService.price = data.price;
}
await updateEmployeeService.save();
}
public async delete(id: string): Promise<void> {
await this.employeesServicesList.deleteOne({ _id: id }).exec();
}
public async find(filters: FindEmployeesServicesParams): Promise<IEmployeeService[]> {
return this.employeesServicesList.find(filters).exec();
}
public async findOne(
filters: FindEmployeesServicesParams
): Promise<IEmployeeServiceDocument | null> {
return this.employeesServicesList.findOne(filters).exec();
}
public async paginate(
filters: PaginateEmployeesServicesParams
): Promise<PaginateEmployeesServicesResults> {
const { page, limit, ...findFilters } = filters; //Extract page and limit from filters
const count = await this.employeesServicesList.countDocuments(findFilters).exec();
const skip = (page - 1) * limit;
const results = await this.employeesServicesList
.find(findFilters)
.skip(skip)
.limit(filters.limit)
.exec();
return {
data: results,
page: filters.page,
pages: Math.ceil(count / filters.limit),
};
}
}
@@ -0,0 +1,119 @@
import { TextObjectFilterResult } from "../../Models/TextObjectFilter.model";
export type FindEmployeesServicesParams = {
_id?: string;
companyId?: string;
employeeId?: string;
serviceId?: string;
};
export type FindEmployeesByServiceParams = {
serviceId?: string;
};
export type PaginateEmployeesServicesParams = FindEmployeesServicesParams & {
page: number;
limit: number;
};
export type PaginateEmployeesServicesResults = {
data: IEmployeeService[];
page: number;
pages: number;
};
export type CreateEmployeeServiceParams = {
companyId: string;
serviceId: string;
employeeId: string;
price?: number;
sessionUser: string;
};
export type UpdateEmployeeServiceParams = CreateEmployeeServiceParams & {
id: string;
};
export type DeleteEmployeeServiceParams = {
companyId: string;
employeeId: string;
serviceId: string;
sessionUser: string;
};
export interface IEmployeeService {
id?: string;
companyId: string;
employeeId: string;
serviceId: string;
price?: number;
}
export type FindCollaboratorServicesParams = {
companyId: string;
employeeId: string;
sessionUser: string;
};
export type UpdateCollaboratorServicesParams = {
companyId: string;
employeeId: string;
services: ServiceStatusItem[];
sessionUser: string;
};
export type DeleteEmployeeServiceByCompanyParams = {
companyId: string;
};
export type DeleteEmployeeServiceByEmployeeParams = {
employeeId: string;
};
export interface CollaboratorServiceItems {
id: string;
name: string;
description: string;
length: number;
price: number;
limit: number;
active: boolean;
}
export interface CollaboratorServiceView {
companyId: string;
employeeId: string;
fullName: string;
avatar: string;
services: CollaboratorServiceItems[];
}
export interface ServiceStatusItem {
serviceId: string;
active: boolean;
}
export interface IEmployeesServicesAdapter {
create(data: CreateEmployeeServiceParams): Promise<IEmployeeService>;
update(data: UpdateEmployeeServiceParams): Promise<void>;
delete(id: string): Promise<void>;
find(filters: FindEmployeesServicesParams): Promise<IEmployeeService[]>;
findOne(filters: FindEmployeesServicesParams): Promise<IEmployeeService | null>;
paginate(filters: PaginateEmployeesServicesParams): Promise<PaginateEmployeesServicesResults>;
}
export interface IEmployeesServicesManager {
employeesServices: IEmployeesServicesAdapter;
createEmployeeService(data: CreateEmployeeServiceParams): Promise<IEmployeeService>;
updateEmployeeService(data: UpdateEmployeeServiceParams): Promise<void>;
updateCollaboratorServices(data: UpdateCollaboratorServicesParams): Promise<void>;
find(data: FindEmployeesServicesParams): Promise<IEmployeeService[]>;
findAllServicesByCollaborator(
data: FindEmployeesServicesParams
): Promise<CollaboratorServiceView>;
delete(data: DeleteEmployeeServiceParams): Promise<void>;
textObjectFilter(data: FindEmployeesServicesParams): Promise<TextObjectFilterResult[]>;
findByService(data: FindEmployeesByServiceParams): Promise<TextObjectFilterResult[]>;
deleteEmployeeServiceByCompany(data: DeleteEmployeeServiceByCompanyParams): Promise<void>;
deleteEmployeeServiceByEmployee(data: DeleteEmployeeServiceByEmployeeParams): Promise<void>;
}
@@ -0,0 +1,434 @@
import { EmployeesServicesAdapterMongoose } from "./EmployeesServices.Adapter.Mongoose";
import {
CreateEmployeeServiceParams,
UpdateEmployeeServiceParams,
DeleteEmployeeServiceParams,
FindEmployeesServicesParams,
IEmployeeService,
IEmployeesServicesManager,
PaginateEmployeesServicesParams,
PaginateEmployeesServicesResults,
CollaboratorServiceView,
FindCollaboratorServicesParams,
CollaboratorServiceItems,
UpdateCollaboratorServicesParams,
FindEmployeesByServiceParams,
DeleteEmployeeServiceByCompanyParams,
DeleteEmployeeServiceByEmployeeParams,
} from "./EmployeesServices.Interface";
import { EmployeeRoles } from "../Employees/Employees.Interface";
import UsersManager from "../Users/Users";
import CompaniesManager from "../Companies/Companies";
import EmployeesList from "../Employees/Employee";
import ServicesManager from "../Services/Service";
import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
import { isNull } from "../../helpers/IsNull";
import { joinStrings } from "../../helpers/String";
import getAvatar from "../../helpers/getAvatar";
import { TextObjectFilterResult } from "../../Models/TextObjectFilter.model";
import { NotificationsManager } from "../Notifications/Notifications";
import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface";
class EmployeesServicesManager implements IEmployeesServicesManager {
employeesServices: EmployeesServicesAdapterMongoose;
constructor() {
this.employeesServices = new EmployeesServicesAdapterMongoose();
}
public async deleteEmployeeServiceByEmployee(
data: DeleteEmployeeServiceByEmployeeParams
): Promise<void> {
await this.employeesServices.employeesServicesList.deleteMany({
employeeId: data.employeeId,
});
}
public async deleteEmployeeServiceByCompany(
data: DeleteEmployeeServiceByCompanyParams
): Promise<void> {
await this.employeesServices.employeesServicesList.deleteMany({
companyId: data.companyId,
});
}
public async createEmployeeService(
data: CreateEmployeeServiceParams
): Promise<IEmployeeService> {
const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId });
if (!companyCheck) {
throw new Error("La compañia no existe");
}
const serviceCheck = await ServicesManager.services.findOne({
_id: data.serviceId,
companyId: data.companyId,
});
if (!serviceCheck) {
throw new Error("El servicio no existe o no pertenece a la compañia.");
}
if (
!(await EmployeesList.checkPermission(
companyCheck,
data.sessionUser,
EmployeeRoles.ADMIN
))
) {
throw new Error(NoPermissionMessage());
}
const employeeCheck = await EmployeesList.employees.findOne({
companyId: data.companyId,
_id: data.employeeId,
});
if (!employeeCheck) {
throw new Error("El empleado no existe o no pertenece a la compañia.");
}
const employeeServiceCheck = await this.employeesServices.findOne({
employeeId: data.employeeId,
serviceId: data.serviceId,
companyId: data.companyId,
});
if (employeeServiceCheck) {
throw new Error("El servicio ya fue asignado para este empleado");
}
const newEmplyeeService = await this.employeesServices.create(data);
await NotificationsManager.sendSystemNotification({
userId: String(employeeCheck.userId),
subject: "Servicio asignado",
message: `Su usuario ha sido asignado para brindar el servicio ${serviceCheck.name} para la organización ${companyCheck.name}.`,
type: NotificationType.EMPLOYEE,
code: String(employeeCheck._id)
});
return newEmplyeeService;
}
public async updateEmployeeService(data: UpdateEmployeeServiceParams): Promise<void> {
const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId });
if (!companyCheck) {
throw new Error("La compañia no existe");
}
const serviceCheck = await ServicesManager.services.findOne({
_id: data.serviceId,
companyId: data.companyId,
});
if (!serviceCheck) {
throw new Error("El servicio no existe o no pertenece a la compañia.");
}
if (
!(await EmployeesList.checkPermission(
companyCheck,
data.sessionUser,
EmployeeRoles.ADMIN
))
) {
throw new Error(NoPermissionMessage());
}
const employeeCheck = await EmployeesList.employees.findOne({
companyId: data.companyId,
_id: data.employeeId,
});
if (!employeeCheck) {
throw new Error("El empleado no existe o no pertenece a la compañia.");
}
await this.employeesServices.update(data);
}
public async updateCollaboratorServices(data: UpdateCollaboratorServicesParams): Promise<void> {
const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId });
if (!companyCheck) {
throw new Error("La compañia no existe");
}
if (
!(await EmployeesList.checkPermission(
companyCheck,
data.sessionUser,
EmployeeRoles.ADMIN
))
) {
throw new Error(NoPermissionMessage());
}
const employeeCheck = await EmployeesList.employees.findOne({
companyId: data.companyId,
_id: data.employeeId,
});
if (!employeeCheck) {
throw new Error("El empleado no existe o no pertenece a la compañia.");
}
for (const service of data.services) {
const serviceCheck = await ServicesManager.services.findOne({
_id: service.serviceId,
companyId: data.companyId,
});
if (!serviceCheck) {
continue;
}
try {
if (!service.active) {
//Aca no envio notificacion porque ya lo hace internamente createEmployeeService
await this.delete({
employeeId: data.employeeId,
serviceId: service.serviceId,
companyId: data.companyId,
sessionUser: data.sessionUser,
});
}
} catch (error) {
continue;
}
try {
if (service.active) {
//Aca no envio notificacion porque ya lo hace internamente createEmployeeService
await this.createEmployeeService({
employeeId: data.employeeId,
serviceId: service.serviceId,
companyId: data.companyId,
sessionUser: data.sessionUser,
});
}
} catch (error) {
continue;
}
}
}
public async paginate(
filters: PaginateEmployeesServicesParams
): Promise<PaginateEmployeesServicesResults> {
return await this.employeesServices.paginate(filters);
}
public async findAllServicesByCollaborator(
filters: FindCollaboratorServicesParams
): Promise<CollaboratorServiceView> {
const employeeCheck = await EmployeesList.employees.findOne({
_id: filters.employeeId,
});
if (!employeeCheck) {
throw new Error("El empleado no existe o no pertenece a la compañia.");
}
const userCheck = await UsersManager.users.findOne({
_id: employeeCheck.userId,
});
if (!userCheck) {
throw new Error("El usuario no existe");
}
const services = await ServicesManager.services.find({ companyId: filters.companyId });
const returnServices: CollaboratorServiceItems[] = [];
for (const service of services) {
const hasService = await this.employeesServices.findOne({
employeeId: filters.employeeId,
serviceId: String(service.id),
companyId: filters.companyId,
});
returnServices.push({
id: isNull<string>(service.id, ""),
name: isNull<string>(service.name, ""),
description: isNull<string>(service.description, ""),
length: isNull<number>(service.length, 0),
price: isNull<number>(service.price, 0),
limit: isNull<number>(service.limit, 1),
active: hasService ? true : false,
});
}
const fullName = joinStrings([userCheck.firstName, userCheck.lastName], " ");
return {
companyId: filters.companyId,
employeeId: filters.employeeId,
services: returnServices,
fullName: fullName,
avatar: getAvatar(userCheck.id, userCheck.avatar, fullName),
};
}
public async find(filters: FindEmployeesServicesParams): Promise<IEmployeeService[]> {
return await this.employeesServices.find(filters);
}
public async delete(data: DeleteEmployeeServiceParams): Promise<void> {
const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId });
if (!companyCheck) {
throw new Error("La compañia no existe");
}
if (
!(await EmployeesList.checkPermission(
companyCheck,
data.sessionUser,
EmployeeRoles.ADMIN
))
) {
throw new Error(NoPermissionMessage());
}
const employeeCheck = await EmployeesList.employees.findOne({
companyId: data.companyId,
_id: data.employeeId,
});
if (!employeeCheck) {
throw new Error("El empleado no existe o no pertenece a la compañia.");
}
const serviceCheck = await ServicesManager.services.findOne({
_id: data.serviceId,
companyId: data.companyId,
});
if (!serviceCheck) {
throw new Error("El servicio no existe o no pertenece a la compañia.");
}
const deleteItem = await this.employeesServices.findOne({
companyId: data.companyId,
employeeId: data.employeeId,
serviceId: data.serviceId,
});
if (!deleteItem) {
throw new Error("Ha ocurrido un error al quitar el servicio");
}
await this.employeesServices.delete(String(deleteItem._id));
await NotificationsManager.sendSystemNotification({
userId: String(employeeCheck.userId),
subject: "Servicio quitado",
message: `E servicio ${serviceCheck.name} de la organización ${companyCheck.name} ya no se encuentra asignado a su usuario.`,
type: NotificationType.EMPLOYEE,
code: String(employeeCheck._id)
});
}
public async textObjectFilter(
data: FindEmployeesServicesParams
): Promise<TextObjectFilterResult[]> {
const employeeCheck = await EmployeesList.employees.findOne({
_id: data.employeeId,
});
if (!employeeCheck) {
throw new Error("El empleado no existe o no pertenece a la compañia.");
}
const userCheck = await UsersManager.users.findOne({
_id: employeeCheck.userId,
});
if (!userCheck) {
throw new Error("El usuario no existe");
}
const services = await this.employeesServices.find({
employeeId: data.employeeId,
companyId: data.companyId,
});
const returnServices: TextObjectFilterResult[] = [];
for (const service of services) {
const serviceData = await ServicesManager.services.findOne({
_id: String(service.serviceId),
});
if (!serviceData) {
continue;
}
returnServices.push({
id: isNull<string>(service.serviceId, ""),
name: isNull<string>(serviceData.name, ""),
description: isNull<string>(serviceData.description, ""),
});
}
return returnServices;
}
public async findByService(
data: FindEmployeesByServiceParams
): Promise<TextObjectFilterResult[]> {
const returnData: TextObjectFilterResult[] = [];
const servicesByEmployee = await this.employeesServices.find({
serviceId: data.serviceId,
});
for (const service of servicesByEmployee) {
const employeeData = await EmployeesList.findById({ id: service.employeeId });
returnData.push({
id: isNull<string>(service.employeeId, ""),
name: isNull<string>(employeeData.fullName, ""),
image: isNull<string>(employeeData.avatar, ""),
});
}
return returnData;
}
}
const EmployeesServicesList = new EmployeesServicesManager();
export default EmployeesServicesList;
@@ -0,0 +1,77 @@
export type ErrorReportParams = {
message: string;
url: string;
userName: string;
userEmail: string;
userId: string;
dateError: Date;
};
class ErrorReporting {
constructor() {
}
/**
* Adds an error to the queue to be reported later.
* In development, it logs the error to the console instead.
* @param data The error details.
*/
public report(data: ErrorReportParams): void {
//if (process.env.NODE_ENV !== "production") {
console.log("--- ERROR REPORT (DEV) ---", data, "\n");
//return;
//}
//this.errorQueue.push(data);
}
/*
private async sendErrorQueue(): Promise<void> {
if (this.errorQueue.length === 0) {
return;
}
// Copy the current queue and clear it immediately
const errorsToSend = [...this.errorQueue];
this.errorQueue = [];
const formattedErrors = errorsToSend
.map(
(error) =>
`----------------------------------------\n` +
`Message: ${error.message}\n` +
`URL: ${error.url}\n` +
`User: ${error.userName} - ${error.userEmail} - ${error.userId}\n` +
`Date: ${error.dateError.toISOString()}`
)
.join("\n\n");
const message =
`Se han acumulado ${errorsToSend.length} errores desde el último reporte.\n\n` +
formattedErrors;
try {
await NotificationsManager.sendEmail({
email: "info@turnosxpress.com.ar",
subject: `Reporte de Errores (${errorsToSend.length}) en turnosXpress`,
message: message,
});
console.log(
`[ErrorReporting] Successfully sent a report with ${errorsToSend.length} errors.`
);
} catch (emailError) {
console.error(
"[ErrorReporting] Failed to send error report email. Errors will be re-queued.",
emailError
);
// If sending fails, add the errors back to the front of the queue to be retried.
this.errorQueue.unshift(...errorsToSend);
}
}
*/
}
const ErrorReportingManager = new ErrorReporting();
export { ErrorReportingManager };
@@ -0,0 +1,89 @@
import {
IFavourite,
IFavouriteDocument,
IFavouritesAdapter,
CreateFavouriteParams,
DeleteFavouriteParams,
FindFavouritesParams,
PaginateFavouritesParams,
PaginateFavouritesResults,
} from "./Favourites.interface";
import { Schema, model, Model, FilterQuery } from "mongoose";
export class FavouritesAdapterMongoose implements IFavouritesAdapter {
schema: Schema;
favouritesList: Model<IFavouriteDocument>;
constructor() {
this.schema = new Schema({
userId: {
type: Schema.Types.ObjectId,
required: true,
ref: "User",
},
serviceId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Service",
},
createdAt: { type: Date, default: Date.now, required: true },
});
this.favouritesList = model<IFavouriteDocument>("Favourite", this.schema);
}
public async create(data: Omit<CreateFavouriteParams, "sessionUser">): Promise<IFavourite> {
return await this.favouritesList.create(data);
}
public async delete(data: Omit<DeleteFavouriteParams, "sessionUser">): Promise<void> {
await this.favouritesList.deleteOne({ _id: data.id });
}
public async find(filters: Omit<FindFavouritesParams, "sessionUser">): Promise<IFavourite[]> {
const searchCriteria = this.buildSearchCriteria(filters);
return await this.favouritesList.find(searchCriteria).exec();
}
public async findOne(
filters: Omit<FindFavouritesParams, "sessionUser">
): Promise<IFavouriteDocument | null> {
const searchCriteria = this.buildSearchCriteria(filters);
return await this.favouritesList.findOne(searchCriteria).exec();
}
private buildSearchCriteria(
filters: Omit<FindFavouritesParams, "sessionUser">
): FilterQuery<IFavourite> {
const searchCriteria: FilterQuery<IFavourite> = {};
if (filters.id) searchCriteria._id = filters.id;
if (filters.userId) searchCriteria.userId = filters.userId;
if (filters.serviceId) searchCriteria.serviceId = filters.serviceId;
if (filters.startDate && filters.endDate) {
searchCriteria.createdAt = {
$gte: filters.startDate,
$lte: filters.endDate,
};
}
return searchCriteria;
}
public async paginate(filters: PaginateFavouritesParams): Promise<PaginateFavouritesResults> {
const searchCriteria = this.buildSearchCriteria(filters);
const count = await this.favouritesList.countDocuments(searchCriteria).exec();
const skip = (filters.page - 1) * filters.limit;
const results = await this.favouritesList
.find(searchCriteria)
.skip(skip)
.limit(filters.limit)
.sort({ createdAt: -1 })
.exec();
return { data: results, page: filters.page, pages: Math.ceil(count / filters.limit) };
}
}
@@ -0,0 +1,59 @@
import { Document } from "mongoose";
export interface IFavourite {
id?: string;
_id?: string;
userId: string;
serviceId: string;
createdAt: Date;
}
export interface IFavouriteDocument extends Omit<IFavourite, "id" | "_id">, Document {
_id: string;
}
export interface IFavouritesAdapter {
create(data: Omit<CreateFavouriteParams, "sessionUser">): Promise<IFavourite>;
delete(data: Omit<DeleteFavouriteParams, "sessionUser">): Promise<void>;
find(filters: Omit<FindFavouritesParams, "sessionUser">): Promise<IFavourite[]>;
findOne(filters: Omit<FindFavouritesParams, "sessionUser">): Promise<IFavouriteDocument | null>;
paginate(filters: PaginateFavouritesParams): Promise<PaginateFavouritesResults>;
}
export type FindFavouritesParams = {
id?: string;
userId?: string;
serviceId?: string;
sessionUser: string;
startDate?: Date;
endDate?: Date;
};
export type PaginateFavouritesParams = FindFavouritesParams & {
page: number;
limit: number;
};
export type PaginateFavouritesResults = {
data: IFavourite[];
page: number;
pages: number;
};
export type CreateFavouriteParams = {
userId: string;
serviceId: string;
sessionUser: string;
};
export type DeleteFavouriteParams = {
id: string;
sessionUser: string;
};
export interface IFavouritesManager {
favourites: IFavouritesAdapter;
createFavourite(data: CreateFavouriteParams): Promise<IFavourite>;
deleteFavourite(data: DeleteFavouriteParams): Promise<void>;
paginate(data: PaginateFavouritesParams): Promise<PaginateFavouritesResults>;
}
@@ -0,0 +1,50 @@
import { validateSessionUser } from "../../helpers/check";
import {
IFavouritesManager,
IFavouritesAdapter,
CreateFavouriteParams,
DeleteFavouriteParams,
IFavourite,
PaginateFavouritesParams,
PaginateFavouritesResults,
} from "./Favourites.interface";
export class FavouritesManager implements IFavouritesManager {
favourites: IFavouritesAdapter;
constructor(favouritesAdapter: IFavouritesAdapter) {
this.favourites = favouritesAdapter;
}
async createFavourite(data: CreateFavouriteParams): Promise<IFavourite> {
await validateSessionUser({
sessionUser: data.sessionUser,
});
const favourite = await this.favourites.create(data);
if (!favourite) {
throw new Error("Ha ocurrido un error al crear el favorito");
}
return favourite;
}
async deleteFavourite(data: DeleteFavouriteParams): Promise<void> {
await validateSessionUser({
sessionUser: data.sessionUser,
});
const favourite = await this.favourites.findOne({ id: data.id });
if (!favourite) {
throw new Error("El favorito que intenta eliminar no existe");
}
await this.favourites.delete(data);
}
async paginate(data: PaginateFavouritesParams): Promise<PaginateFavouritesResults> {
return this.favourites.paginate(data);
}
}
@@ -0,0 +1,21 @@
export type UploadPostFileParams = {
companyId: string;
sessionUser: string;
token: string; //Session User token. Para verificar que sea un usuario loguedo.
file: Express.Multer.File;
};
export type UploadedFileInfo = {
originalName: string;
objectName: string;
};
export type UploadFileResponse = {
message: string;
files: UploadedFileInfo[];
};
export interface IFileManager {
uploadPostFile(data: UploadPostFileParams): Promise<UploadFileResponse>;
downloadFile(objectName: string): Promise<Buffer>;
}
+53
View File
@@ -0,0 +1,53 @@
import axios from "axios";
import FormData from "form-data";
import { IFileManager, UploadFileResponse, UploadPostFileParams } from "./Files.Interface";
class UploadFileManager implements IFileManager {
constructor() {}
public async uploadPostFile(data: UploadPostFileParams): Promise<UploadFileResponse> {
try {
const apiUrl = `${process.env.FILE_SERVER_URL}upload-by-organization`;
const accessToken = `${data.token}`;
// Crear el formulario
const form = new FormData();
form.append("files", data.file.buffer, {
filename: data.file.originalname, // necesario para que el servidor sepa el nombre
contentType: data.file.mimetype, // necesario para que Multer entienda el tipo
});
form.append("sessionUser", data.sessionUser);
form.append("organizationId", data.companyId);
// Enviar la solicitud
const response = await axios.post(apiUrl, form, {
headers: {
...form.getHeaders(),
Authorization: `Bearer ${accessToken}`,
},
});
if (response.status !== 200) {
throw new Error(`Error al subir el archivo: ${response.statusText}`);
}
console.log(">>>>Respuesta del servidor de archivos:>>>>", response.data);
return response.data as UploadFileResponse;
} catch (error) {
//aaas
console.error("Error al enviar el archi:", error);
throw error;
}
}
public async downloadFile(objectName: string): Promise<Buffer> {
const imageUrl = `${process.env.FILE_SERVER_URL}download/${objectName}`;
const response = await axios.get(imageUrl, { responseType: "arraybuffer" });
return response.data;
}
}
const UploadFiles = new UploadFileManager();
export default UploadFiles;
@@ -0,0 +1,81 @@
import {
FindHeatMapParams,
IHeatMapAdapter,
IHeatMap,
CreateHeatMapParams,
UpdateHeatMapParams,
DeleteHeatMapParams,
} from "./HeatMap.Interface";
import { Document, Model, Schema, model } from "mongoose";
export interface IHeatMapDocument extends Omit<IHeatMap, "id">, Document {}
export class HeatMapAdapterMongoose implements IHeatMapAdapter {
schema: Schema;
heatMapList: Model<IHeatMapDocument>;
constructor() {
this.schema = new Schema({
companyId: { type: Schema.Types.ObjectId, required: true, ref: "Companie" },
employeeId: { type: Schema.Types.ObjectId, required: true, ref: "Employee" },
serviceId: { type: Schema.Types.ObjectId, required: true, ref: "Service" },
heatMap: { type: Array, required: true },
heatMapDate: { type: Date, required: true },
});
this.heatMapList = model<IHeatMapDocument>("HeatMap", this.schema);
}
public async create(data: CreateHeatMapParams): Promise<IHeatMap> {
return await this.heatMapList.create({
companyId: data.companyId,
employeeId: data.employeeId,
serviceId: data.serviceId,
heatMap: data.heatmap.heatMap,
heatMapDate: data.heatMapDate,
});
}
public async update(data: UpdateHeatMapParams): Promise<void> {
await this.heatMapList.updateOne(
{ _id: data.id },
{
companyId: data.companyId,
employeeId: data.employeeId,
serviceId: data.serviceId,
heatMap: data.heatmap.heatMap,
heatMapDate: data.heatMapDate,
}
);
}
public async delete(data: DeleteHeatMapParams): Promise<void> {
let deleteFilter = {};
if (data.serviceId) {
deleteFilter = { ...deleteFilter, serviceId: data.serviceId };
}
if (data.companyId) {
deleteFilter = { ...deleteFilter, companyId: data.companyId };
}
if (data.employeeId) {
deleteFilter = { ...deleteFilter, employeeId: data.employeeId };
}
if (data.heatMapDate) {
deleteFilter = { ...deleteFilter, heatMapDate: { $lt: data.heatMapDate } };
}
await this.heatMapList.deleteMany(deleteFilter);
}
public async findOne(filters: FindHeatMapParams): Promise<IHeatMapDocument | null> {
return this.heatMapList.findOne(filters).exec();
}
public async find(filters: FindHeatMapParams): Promise<IHeatMap[]> {
return this.heatMapList.find(filters).exec();
}
}
@@ -0,0 +1,67 @@
export type HeatMapItem = {
keyDate: Date;
count: number;
};
export type FindHeatMapParams = {
_id?: string;
companyId?: string;
employeeId?: string;
serviceId?: string;
heatMapDate?: Date;
heatmap?: Array<HeatMapItem>;
};
export type DeleteHeatMapParams = {
_id?: string;
companyId?: string;
employeeId?: string;
serviceId?: string;
heatMapDate: Date;
};
export type CreateHeatMapParams = {
companyId: string;
employeeId: string;
serviceId: string;
heatmap: IHeatMap;
heatMapDate: Date;
};
export type DeleteHeatMapByCompanyParams = {
companyId: string;
};
export type UpdateHeatMapParams = CreateHeatMapParams & {
id: string;
};
export type DeleteHeatMapByEmployeeParams = {
employeeId: string;
};
export interface IHeatMap {
id?: string;
companyId?: string;
employeeId?: string;
serviceId?: string;
heatMap?: Array<HeatMapItem>;
heatMapDate?: Date;
}
export interface IHeatMapAdapter {
create(data: CreateHeatMapParams): Promise<IHeatMap>;
update(data: UpdateHeatMapParams): Promise<void>;
delete(data: DeleteHeatMapParams): Promise<void>;
findOne(filters: FindHeatMapParams): Promise<IHeatMap | null>;
}
export interface IHeatMapManager {
heatMaps: IHeatMapAdapter;
createHeatMap(data: CreateHeatMapParams): Promise<IHeatMap>;
updateHeatMap(data: UpdateHeatMapParams): Promise<void>;
deleteHeatMapByDate(data: DeleteHeatMapParams): Promise<void>;
findOne(filters: FindHeatMapParams): Promise<IHeatMap | null>;
deleteHeatMapByCompany(data: DeleteHeatMapByCompanyParams): Promise<void>;
deleteHeatMapByEmployee(data: DeleteHeatMapByEmployeeParams): Promise<void>;
cleanUpHeatMap(): Promise<void>;
}
+91
View File
@@ -0,0 +1,91 @@
import dayjs from "dayjs";
import { HeatMapAdapterMongoose } from "./HeatMap.Adapter.Mongoose";
import {
CreateHeatMapParams,
UpdateHeatMapParams,
FindHeatMapParams,
IHeatMap,
IHeatMapManager,
DeleteHeatMapParams,
DeleteHeatMapByCompanyParams,
DeleteHeatMapByEmployeeParams,
} from "./HeatMap.Interface";
class HeatMapManager implements IHeatMapManager {
heatMaps: HeatMapAdapterMongoose;
constructor() {
this.heatMaps = new HeatMapAdapterMongoose();
}
public async deleteHeatMapByEmployee(data: DeleteHeatMapByEmployeeParams): Promise<void> {
await this.cleanUpHeatMap();
await this.heatMaps.heatMapList.deleteMany({ employeeId: data.employeeId });
}
public async deleteHeatMapByCompany(data: DeleteHeatMapByCompanyParams): Promise<void> {
await this.cleanUpHeatMap();
await this.heatMaps.heatMapList.deleteMany({ companyId: data.companyId });
}
public async createHeatMap(data: CreateHeatMapParams): Promise<IHeatMap> {
await this.cleanUpHeatMap();
const checkHeatMap = await this.heatMaps.findOne({
companyId: data.companyId,
serviceId: data.serviceId,
employeeId: data.employeeId,
heatMapDate: data.heatMapDate,
});
if (checkHeatMap) {
throw new Error("El mapa de calor ya existe");
}
const newHeatMap = await this.heatMaps.create(data);
return newHeatMap;
}
public async updateHeatMap(data: UpdateHeatMapParams): Promise<void> {
await this.cleanUpHeatMap();
const checkHeatMap = await this.heatMaps.findOne({
companyId: data.companyId,
serviceId: data.serviceId,
employeeId: data.employeeId,
heatMapDate: data.heatMapDate,
});
if (!checkHeatMap) {
throw new Error("El mapa de calor no existe");
}
await this.heatMaps.update(data);
}
public async findOne(filters: FindHeatMapParams): Promise<IHeatMap | null> {
await this.cleanUpHeatMap();
return this.heatMaps.findOne(filters);
}
public async find(filters: FindHeatMapParams): Promise<IHeatMap[]> {
await this.cleanUpHeatMap();
return this.heatMaps.find(filters);
}
public async deleteHeatMapByDate(data: DeleteHeatMapParams): Promise<void> {
await this.cleanUpHeatMap();
await this.heatMaps.heatMapList.deleteMany(data);
}
public async cleanUpHeatMap(): Promise<void> {
const today = dayjs();
const yesterday = today.add(-1, "day");
await this.heatMaps.heatMapList.deleteMany({ heatMapDate: { $lt: yesterday.toDate() } });
}
}
const HeatMapList = new HeatMapManager();
export default HeatMapList;
@@ -0,0 +1,69 @@
import {
FindLocalidadesParams,
ILocalidadesAdapter,
ILocalidad,
PaginateFilterBlockParams,
PaginateFilterBlockResults,
} from "./Localidades.interface";
import mongoose, { Document, Model, Schema, model } from "mongoose";
export interface ILocalidadDocument extends Omit<ILocalidad, "id">, Document {}
export class LocalidadesAdapterMongoose implements ILocalidadesAdapter {
schema: Schema;
localidadList: Model<ILocalidadDocument>;
constructor() {
this.schema = new Schema({
name: { type: String, required: true, unique: true },
code: { type: Number, required: true, default: 0 },
partido: { type: String, required: true },
provincia: { type: String, required: true },
latitude: { type: Number, required: true, default: 0 },
longitude: { type: Number, required: true, default: 0 },
});
this.localidadList = model<ILocalidadDocument>("Localidade", this.schema);
}
public async find(filters: FindLocalidadesParams): Promise<ILocalidad[]> {
return this.localidadList.find(filters).exec();
}
public async paginate(data: PaginateFilterBlockParams): Promise<PaginateFilterBlockResults> {
const skip = (data.page - 1) * data.limit;
// Construye los criterios de búsqueda
const searchCriteria: mongoose.FilterQuery<ILocalidad> = {};
// Agrega filtro por query usando expresiones regulares para 'name' y 'description'
/*if (data.name) {
const regex = new RegExp(data.name, "i");
searchCriteria.$or = [{ name: regex }, { partido: regex }, { provincia: regex }];
}*/
if (data.name) {
const words = data.name.split(" ").map((word) => new RegExp(word, "i"));
searchCriteria.$and = words.map((word) => ({
$or: [{ name: word }, { partido: word }, { provincia: word }],
}));
}
// Realiza la consulta con paginación
const results = await this.localidadList
.find(searchCriteria)
.sort({ price: 1 }) // Ordena por precio ascendente
.skip(skip)
.limit(data.limit)
.exec();
// Cuenta el total de documentos que coinciden con los criterios de búsqueda
const count = await this.localidadList.countDocuments(searchCriteria).exec();
return {
data: results,
page: data.page,
pages: Math.ceil(count / data.limit),
};
}
}
@@ -0,0 +1,42 @@
import { TextObjectFilterResult } from "../TextObjectFilter.model";
export type FindLocalidadesParams = {
_id?: string;
name?: string;
code?: number;
partido: string;
provincia: string;
};
export interface ILocalidad {
id?: string;
name: string;
code: number;
partido: string;
provincia: string;
latitude: number;
longitude: number;
}
export type PaginateFilterBlockParams = {
name?: string;
page: number;
limit: number;
};
export type PaginateFilterBlockResults = {
data: ILocalidad[];
page: number;
pages: number;
};
export interface ILocalidadesAdapter {
find(filters: FindLocalidadesParams): Promise<ILocalidad[]>;
paginate(filters: PaginateFilterBlockParams): Promise<PaginateFilterBlockResults>;
}
export interface ILocalidadesManager {
localidades: ILocalidadesAdapter;
filterAll(data: FindLocalidadesParams): Promise<TextObjectFilterResult[]>;
paginate(data: PaginateFilterBlockParams): Promise<TextObjectFilterResult[]>;
}
@@ -0,0 +1,67 @@
import PartidosList from "../Partidos/Partidos";
import ProvinciasList from "../Provincias/Provincias";
import { TextObjectFilterResult } from "../TextObjectFilter.model";
import { LocalidadesAdapterMongoose } from "./Localidades.Adapter.Mongoose";
import {
FindLocalidadesParams,
ILocalidadesManager,
PaginateFilterBlockParams,
} from "./Localidades.interface";
class LocalidadesManager implements ILocalidadesManager {
localidades: LocalidadesAdapterMongoose;
constructor() {
this.localidades = new LocalidadesAdapterMongoose();
}
public async filterAll(data: FindLocalidadesParams): Promise<TextObjectFilterResult[]> {
const localidades = await this.localidades.find(data);
const returnData: TextObjectFilterResult[] = [];
for (const localidad of localidades) {
returnData.push({
id: localidad.name,
name: localidad.name.toLowerCase(),
});
}
return returnData;
}
public async paginate(data: PaginateFilterBlockParams): Promise<TextObjectFilterResult[]> {
const localidades = await this.localidades.paginate(data);
const resultados: TextObjectFilterResult[] = [];
for (const localidad of localidades.data) {
const provincia = await ProvinciasList.provincias.provinciaList.findOne({
name: localidad.provincia,
});
const partido = await PartidosList.partidos.partidoList.findOne({
name: localidad.partido,
});
let partidoName = "";
let provinciaName = "";
if (partido) {
partidoName = partido.name;
}
if (provincia) {
provinciaName = provincia.name;
}
resultados.push({
id: localidad.name,
name: localidad.name.toLowerCase(),
description: partidoName + " - " + provinciaName,
});
}
return resultados;
}
}
const LocalidadesList = new LocalidadesManager();
export default LocalidadesList;
@@ -0,0 +1,110 @@
import mongoose, { Document, Model, Schema, model } from "mongoose";
import {
CreateMessageConversationParams,
FindMessageConversationsParams,
IMessageConversation,
IMessageConversationsAdapter,
PaginateMessageConversationsParams,
PaginateMessageConversationsResults,
} from "./MessagesConversations.Interface";
import EmployeesList from "../Employees/Employee";
export interface IMessageConversationDocument extends Omit<IMessageConversation, "id">, Document {}
export class MessageConversationsAdapterMongoose implements IMessageConversationsAdapter {
schema: Schema;
conversationList: Model<IMessageConversationDocument>;
constructor() {
this.schema = new Schema({
userIdFrom: { type: Schema.Types.ObjectId, required: false, ref: "User" },
userIdTo: { type: Schema.Types.ObjectId, required: false, ref: "User" },
companyId: { type: Schema.Types.ObjectId, required: false, ref: "Companie" },
type: { type: String, required: true },
});
this.conversationList = model<IMessageConversationDocument>(
"MessagesConversation",
this.schema
);
}
public async create(data: CreateMessageConversationParams): Promise<IMessageConversation> {
return await this.conversationList.create(data);
}
public async delete(id: string): Promise<void> {
await this.conversationList.findByIdAndDelete(id).exec();
}
public async find(
filters: Omit<FindMessageConversationsParams, "sessionUser">
): Promise<IMessageConversation[]> {
return this.conversationList.find(filters).exec();
}
public async findOne(
filters: Omit<FindMessageConversationsParams, "sessionUser">
): Promise<IMessageConversationDocument | null> {
return this.conversationList.findOne(filters).exec();
}
// Método para búsqueda con expresiones regulares y paginación
public async paginate(
data: PaginateMessageConversationsParams
): Promise<PaginateMessageConversationsResults> {
const skip = (data.page - 1) * data.limit;
// Construye los criterios de búsqueda
const searchCriteria: mongoose.FilterQuery<IMessageConversation> = {};
// Agrega filtro por query usando expresiones regulares para 'name' y 'description'
if (data.query) {
const regex = new RegExp(data.query, "i");
searchCriteria.$or = [{ subject: regex }, { message: regex }];
}
//Busca las organizaciones en las que el usuario es colaborador
const collaboratorOrgs = await EmployeesList.employees.find({
userId: data.sessionUser,
});
const orgIds = collaboratorOrgs.map((org) => org.companyId);
searchCriteria.$or = [
{ userIdFrom: data.sessionUser },
{ userIdTo: data.sessionUser },
{ companyId: { $in: orgIds } },
];
// Filtra por categoryId si está presente
if (data.userIdFrom !== undefined) {
searchCriteria.userIdFrom = data.userIdFrom;
}
if (data.userIdTo !== undefined) {
searchCriteria.userIdTo = data.userIdTo;
}
if (data.companyId !== undefined) {
searchCriteria.companyId = data.companyId;
}
// Realiza la consulta con paginación
const results = await this.conversationList
.find(searchCriteria)
.sort({ notificationDate: -1 })
.skip(skip)
.limit(data.limit)
.exec();
// Cuenta el total de documentos que coinciden con los criterios de búsqueda
const count = await this.conversationList.countDocuments(searchCriteria).exec();
return {
data: results,
page: data.page,
pages: Math.ceil(count / data.limit),
};
}
}
@@ -0,0 +1,133 @@
export enum MESSAGE_CONVERSATION_TYPES {
"USER_TO_USER" = "user-to-user",
"USER_TO_ORG" = "user-to-org",
}
export type FindMessageConversationsParams = {
_id?: string;
userIdFrom?: string;
userIdTo?: string;
companyId?: string;
sessionUser: string;
};
export type PaginateMessageConversationsParams = {
query?: string;
userIdFrom?: string;
userIdTo?: string;
companyId?: string;
page: number;
limit: number;
sessionUser: string;
};
export type PaginateMessageConversationsResults = {
data: IMessageConversation[];
page: number;
pages: number;
};
export type FindConversationsResults = {
data: FindConversationItem[];
page: number;
pages: number;
};
export type CreateMessageConversationParams = {
userIdFrom?: string;
userIdTo?: string;
companyId?: string;
type: MESSAGE_CONVERSATION_TYPES;
sessionUser: string;
};
export type DeleteMessageConversationParams = {
id: string;
sessionUser: string;
};
export interface IMessageConversation {
id?: string;
userIdFrom?: string; //Indica quien envía el mensaje
userIdTo?: string; // Indica a que usuario va dirigido el mensaje.
// Cuando el mensaje va dirigido a una organizacion,
// habrá un colaborador de la organizacion que de una respuesta
// y ese id de usuario se almacenara en userIdTo.
companyId?: string; //Indica que el mensaje va dirigido a una organizacion.
type: MESSAGE_CONVERSATION_TYPES;
newMessages: number; //Indica si hay nuevos mensajes en la conversacion.
}
export interface FindConversationItem {
conversation: IMessageConversation;
name: string;
description: string;
image: string;
orderWeight: number;
}
export type PaginateFindContactsParams = {
query?: string;
page: number;
limit: number;
sessionUser: string;
};
export type PaginateFindContactsResults = {
data: FindConversationItem[];
page: number;
pages: number;
};
export type MarkConversationAsReadParams = {
conversationId: string;
sessionUser: string;
};
export type CountUnreadConversationParams = {
conversationId: string;
sessionUser: string;
};
export type ConvertU2UMessageConversationParams = {
conversationId: string;
sessionUser: string;
};
export type DeleteConversationsByCompanyParams = {
companyId: string;
};
export type DeleteConversationsByUserParams = {
userId: string;
};
export interface IMessageConversationsAdapter {
create(data: CreateMessageConversationParams): Promise<IMessageConversation>;
delete(id: string): Promise<void>;
find(
filters: Omit<FindMessageConversationsParams, "sessionUser">
): Promise<IMessageConversation[]>;
findOne(
filters: Omit<FindMessageConversationsParams, "sessionUser">
): Promise<IMessageConversation | null>;
paginate(
filters: PaginateMessageConversationsParams
): Promise<PaginateMessageConversationsResults>;
}
export interface IMessageConversationsManager {
conversations: IMessageConversationsAdapter;
createConversation(data: CreateMessageConversationParams): Promise<IMessageConversation>;
deleteConversation(data: DeleteMessageConversationParams): Promise<void>;
paginateConversations(
data: PaginateMessageConversationsParams
): Promise<PaginateMessageConversationsResults>;
paginateFindContacts(data: PaginateFindContactsParams): Promise<PaginateFindContactsResults>;
findConversations(data: PaginateFindContactsParams): Promise<FindConversationsResults>;
markConversationAsRead(data: MarkConversationAsReadParams): Promise<void>;
countUnreadMessages(data: CountUnreadConversationParams): Promise<number>;
convertUserToUser(data: ConvertU2UMessageConversationParams): Promise<void>;
deleteConversationsByCompany(data: DeleteConversationsByCompanyParams): Promise<void>;
deleteConversationsByUser(data: DeleteConversationsByUserParams): Promise<void>;
}
@@ -0,0 +1,504 @@
import UsersManager from "../Users/Users";
import CompaniesManager from "../Companies/Companies";
import {
ConvertU2UMessageConversationParams,
CountUnreadConversationParams,
CreateMessageConversationParams,
DeleteConversationsByCompanyParams,
DeleteConversationsByUserParams,
DeleteMessageConversationParams,
FindConversationItem,
FindConversationsResults,
IMessageConversation,
IMessageConversationsManager,
MarkConversationAsReadParams,
MESSAGE_CONVERSATION_TYPES,
PaginateFindContactsParams,
PaginateFindContactsResults,
PaginateMessageConversationsParams,
PaginateMessageConversationsResults,
} from "./MessagesConversations.Interface";
import { MessageConversationsAdapterMongoose } from "./MessagesConversations.Adapter.Mongoose";
import EmployeesList from "../Employees/Employee";
import ClientsList from "../Clients/Clients";
import getAvatar from "../../helpers/getAvatar";
import UserList from "../Users/Users";
import MessagesList from "../Messages/Messages";
import { isNull } from "../../helpers/IsNull";
import SystemNoficationsList from "../SystemNotifications/SystemNotification";
import mongoose from "mongoose";
type GetConversationParams = {
userId: string;
};
class MessageConversationsManager implements IMessageConversationsManager {
conversations: MessageConversationsAdapterMongoose;
constructor() {
this.conversations = new MessageConversationsAdapterMongoose();
}
public async deleteConversationsByUser(data: DeleteConversationsByUserParams): Promise<void> {
const conversationsFrom = await this.conversations.find({ userIdFrom: data.userId });
for (const conv of conversationsFrom) {
await MessagesList.messages.messageList.deleteMany({ conversationId: conv.id });
}
const conversationsTo = await this.conversations.find({ userIdTo: data.userId });
for (const conv of conversationsTo) {
await MessagesList.messages.messageList.deleteMany({ conversationId: conv.id });
}
await this.conversations.conversationList.deleteMany({ userIdFrom: data.userId });
await this.conversations.conversationList.deleteMany({ userIdTo: data.userId });
}
public async deleteConversationsByCompany(
data: DeleteConversationsByCompanyParams
): Promise<void> {
const conversations = await this.conversations.find({ companyId: data.companyId });
for (const conv of conversations) {
await MessagesList.messages.messageList.deleteMany({ conversationId: conv.id });
}
await this.conversations.conversationList.deleteMany({ companyId: data.companyId });
}
public async createConversation(
data: CreateMessageConversationParams
): Promise<IMessageConversation> {
if (data.userIdFrom) {
const userFromCheck = await UsersManager.users.findOne({ _id: data.userIdFrom });
if (!userFromCheck) {
throw new Error("Usuario invalido");
}
}
if (data.userIdTo) {
const userToCheck = await UsersManager.users.findOne({ _id: data.userIdTo });
if (!userToCheck) {
throw new Error("Usuario invalido");
}
}
if (data.companyId) {
const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId });
if (!companyCheck) {
throw new Error("La compañia no existe");
}
}
const newConversation = await this.conversations.create(data);
return newConversation;
}
public async deleteConversation(data: DeleteMessageConversationParams): Promise<void> {
const checkUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!checkUser) {
throw new Error("Usuario invalido");
}
const checkConversation = await this.conversations.conversationList.findOne({
_id: data.id,
});
if (!checkConversation) {
throw new Error("Conversacion invalida");
}
if (checkConversation.type == MESSAGE_CONVERSATION_TYPES.USER_TO_USER) {
if (String(checkConversation.userIdFrom) !== data.sessionUser) {
throw new Error("No tienes permiso para eliminar esta conversacion");
}
} else if (checkConversation.type == MESSAGE_CONVERSATION_TYPES.USER_TO_ORG) {
if (String(checkConversation.userIdFrom) !== data.sessionUser) {
throw new Error("No tienes permiso para eliminar esta conversacion");
}
}
await this.conversations.delete(data.id);
}
public async paginateConversations(
filters: PaginateMessageConversationsParams
): Promise<PaginateMessageConversationsResults> {
return await this.conversations.paginate(filters);
}
private async getUserRelationWithMe(userId: string, sessionUser: string): Promise<string> {
const myOrgs = await EmployeesList.employees.find({ userId: sessionUser });
for (const org of myOrgs) {
const organization = await CompaniesManager.companies.findOne({ _id: org.companyId });
if (!organization) {
continue;
}
//Buscamos si es un colaborador en una de mis organizaciones.
const employee = await EmployeesList.employees.findOne({
userId,
companyId: org.companyId,
});
if (employee) {
return "Colaborador en " + organization.name;
}
//Buscamos si es un cliente en una de mis organizaciones.
const client = await ClientsList.clients.findOne({
userId: userId,
companyId: org.companyId,
});
if (client) {
return "Cliente de " + organization.name;
}
}
const orgsAsClient = await ClientsList.clients.find({
userId: sessionUser,
});
for (const organization of orgsAsClient) {
const checkColaIsCollaborator = await EmployeesList.employees.findOne({
companyId: organization.companyId,
userId: userId,
});
if (checkColaIsCollaborator) {
const org = await CompaniesManager.companies.findOne({
_id: organization.companyId,
});
if (!org) {
continue;
}
return "Miembro de " + org.name;
}
}
return "Otros contactos";
}
public async findConversations(
data: PaginateFindContactsParams
): Promise<FindConversationsResults> {
const items: FindConversationItem[] = [];
const conversations = await this.conversations.paginate(data);
for (const conversation of conversations.data) {
let contactName = "";
let contactUserId: string | undefined = "";
let contactAvatar: string = "";
let conversationName = "";
let conversationDescription = "";
let conversationImage = "";
if (conversation.type == MESSAGE_CONVERSATION_TYPES.USER_TO_USER) {
if (conversation.userIdFrom == data.sessionUser) {
const user = await UserList.users.findOne({ _id: conversation.userIdTo });
if (!user) {
continue;
}
contactName = UserList.getUserFullName(user);
contactUserId = String(conversation.userIdTo);
contactAvatar = user.avatar ? user.avatar : "";
} else if (conversation.userIdTo == data.sessionUser) {
const user = await UserList.users.findOne({ _id: conversation.userIdFrom });
if (!user) {
continue;
}
contactName = UserList.getUserFullName(user);
contactUserId = String(conversation.userIdFrom);
contactAvatar = user.avatar ? user.avatar : "";
}
if (contactUserId) {
conversationName = contactName;
conversationDescription = await this.getUserRelationWithMe(
contactUserId,
data.sessionUser
);
conversationImage = getAvatar(contactUserId, contactAvatar, contactName);
}
} else if (conversation.type == MESSAGE_CONVERSATION_TYPES.USER_TO_ORG) {
if (conversation.userIdFrom == data.sessionUser) {
const org = await CompaniesManager.companies.findOne({
_id: conversation.companyId,
});
if (!org) {
continue;
}
conversationName = org.name;
conversationDescription = org.description;
conversationImage = isNull<string>(org.iconFile, org.name);
} else if (conversation.companyId) {
const user = await UserList.users.findOne({ _id: conversation.userIdFrom });
if (!user) {
continue;
}
conversationName = UserList.getUserFullName(user);
conversationDescription = "Consulta a la organización";
conversationImage = getAvatar(user.id, user.avatar, conversationName);
}
}
let unreadCount = 0;
if (conversation && conversation.id) {
unreadCount = await this.countUnreadMessages({
conversationId: conversation.id,
sessionUser: data.sessionUser,
});
}
items.push({
conversation: {
id: conversation.id,
userIdFrom: conversation.userIdFrom,
userIdTo: conversation.userIdTo,
companyId: conversation.companyId,
type: conversation.type,
newMessages: unreadCount,
},
name: conversationName,
description: conversationDescription,
image: conversationImage,
orderWeight: 1,
});
}
return {
data: items,
page: conversations.page,
pages: conversations.pages,
} as FindConversationsResults;
}
private async getConversationIfExists(
data: GetConversationParams
): Promise<IMessageConversation | null> {
let conversation = await this.conversations.conversationList.findOne({
userIdFrom: data.userId,
});
if (!conversation) {
conversation = await this.conversations.conversationList.findOne({
userIdTo: data.userId,
});
}
return conversation;
}
public async paginateFindContacts(
data: PaginateFindContactsParams
): Promise<PaginateFindContactsResults> {
const items: FindConversationItem[] = [];
//Mostrar las organizaciones en las que el usuario es cliente.
const companiesAsClient = await CompaniesManager.getByClientId({
clientId: data.sessionUser,
sessionUser: data.sessionUser,
});
for (const org of companiesAsClient) {
items.push({
conversation: {
companyId: org.id,
type: MESSAGE_CONVERSATION_TYPES.USER_TO_ORG,
newMessages: 0,
},
name: org.name,
description: org.description,
image: org.logoFile,
orderWeight: 1,
});
//Mostrar los colaboradores de esa organizacion.
const employeesByOrganization = await EmployeesList.findByCompanyId({
companyId: org.id,
});
for (const employee of employeesByOrganization) {
const conversation = await this.getConversationIfExists({
userId: employee.userId,
});
let unreadCount = 0;
if (conversation && conversation.id) {
unreadCount = await this.countUnreadMessages({
conversationId: conversation.id,
sessionUser: data.sessionUser,
});
}
items.push({
conversation: conversation
? ({
id: conversation.id,
userIdFrom: conversation.userIdFrom,
userIdTo: conversation.userIdTo,
companyId: conversation.companyId,
type: conversation.type,
newMessages: unreadCount,
} as IMessageConversation)
: {
userIdTo: employee.userId,
type: MESSAGE_CONVERSATION_TYPES.USER_TO_USER,
newMessages: 0,
},
name: employee.fullName,
description: org.name,
image: employee.avatar,
orderWeight: 2,
});
}
}
//Mostras los clientes y colaboradores de las organizaciones donde el usuario es colaborador.
const companiesAsCollaborator = await EmployeesList.employees.find({
userId: data.sessionUser,
});
for (const org of companiesAsCollaborator) {
const organization = await CompaniesManager.companies.findOne({ _id: org.companyId });
if (!organization) {
continue;
}
//Mostras los clientes de las organizaciones donde el usuario es colaborador.
const organizationClients = await ClientsList.clients.find({
companyId: org.companyId,
});
for (const client of organizationClients) {
if (!client.userId) {
continue;
}
const clientName = ClientsList.getClientFullName(client);
items.push({
conversation: {
userIdFrom: data.sessionUser,
userIdTo: client.userId,
type: MESSAGE_CONVERSATION_TYPES.USER_TO_USER,
newMessages: 0,
},
name: clientName,
description: "Cliente de " + organization.name,
image: getAvatar(client.userId, "", clientName),
orderWeight: 2,
});
}
//Mostrar los colaboradores de las organizaciones donde el usuario es colaborador.
const organizationEmployees = await EmployeesList.employees.find({
companyId: org.companyId,
});
for (const employee of organizationEmployees) {
const employeeUser = await UserList.users.findOne({ _id: employee.userId });
if (!employeeUser) {
continue;
}
if (employee.userId == data.sessionUser) {
continue;
}
const employeeName = UserList.getUserFullName(employeeUser);
items.push({
conversation: {
userIdFrom: data.sessionUser,
userIdTo: employee.userId,
type: MESSAGE_CONVERSATION_TYPES.USER_TO_USER,
newMessages: 0,
},
name: employeeName,
description: "Colaborador en " + organization.name,
image: getAvatar(employee.userId, "", employeeName),
orderWeight: 2,
});
}
}
const query = data.query ? data.query.toUpperCase() : "";
return {
data: items.filter(
(i) =>
String(i.name + i.description)
.toUpperCase()
.indexOf(query) >= 0
),
page: 1,
pages: 1,
} as PaginateFindContactsResults;
}
public async markConversationAsRead(data: MarkConversationAsReadParams): Promise<void> {
const checkUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!checkUser) {
throw new Error("Usuario invalido");
}
const checkConversation = await this.conversations.conversationList.findOne({
_id: data.conversationId,
});
if (!checkConversation) {
throw new Error("Conversacion invalida");
}
await MessagesList.markAsRead({
conversationId: data.conversationId,
sessionUser: data.sessionUser,
});
await SystemNoficationsList.deleteConversationNotifications({
conversationId: data.conversationId,
userId: data.sessionUser,
sessionUser: data.sessionUser,
});
}
public async convertUserToUser(data: ConvertU2UMessageConversationParams): Promise<void> {
const checkUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!checkUser) {
throw new Error("Usuario invalido");
}
const checkConversation = await this.conversations.conversationList.findOne({
_id: data.conversationId,
});
if (!checkConversation) {
throw new Error("Conversacion invalida");
}
checkConversation.type = MESSAGE_CONVERSATION_TYPES.USER_TO_USER;
checkConversation.userIdTo = data.sessionUser;
await checkConversation.save();
}
public async countUnreadMessages(data: CountUnreadConversationParams): Promise<number> {
const searchCriteria: mongoose.FilterQuery<IMessageConversation> = {};
searchCriteria.conversationId = data.conversationId;
searchCriteria.readed = false;
searchCriteria.userId = { $ne: data.sessionUser };
const unreadMessages = await MessagesList.messages.find(searchCriteria);
return unreadMessages.length;
}
}
const MessageConversationList = new MessageConversationsManager();
export default MessageConversationList;
@@ -0,0 +1,107 @@
import mongoose, { Document, Model, Schema, model } from "mongoose";
import {
CreateMessageParams,
FindMessageParams,
IMessage,
IMessagesAdapter,
PaginateMessageParams,
PaginateMessageResults,
} from "./Messages.Interface";
export interface IMessageDocument extends Omit<IMessage, "id">, Document {}
export class MessagesAdapterMongoose implements IMessagesAdapter {
schema: Schema;
messageList: Model<IMessageDocument>;
constructor() {
this.schema = new Schema({
conversationId: {
type: Schema.Types.ObjectId,
required: true,
ref: "MessagesConversation",
},
userId: {
type: Schema.Types.ObjectId,
required: true,
ref: "User",
},
message: { type: String, required: true },
messageDate: { type: Date, required: true },
readed: { type: Boolean, default: false },
});
this.messageList = model<IMessageDocument>("Message", this.schema);
}
public async create(data: CreateMessageParams): Promise<IMessage> {
return await this.messageList.create({
conversationId: data.conversationId,
userId: data.sessionUser,
message: data.message,
messageDate: new Date(),
readed: false,
});
}
public async delete(id: string): Promise<void> {
await this.messageList.findByIdAndDelete(id).exec();
}
public async find(filters: Omit<FindMessageParams, "sessionUser">): Promise<IMessage[]> {
return this.messageList.find(filters).exec();
}
public async findOne(
filters: Omit<FindMessageParams, "sessionUser">
): Promise<IMessage | null> {
return this.messageList.findOne(filters).exec();
}
// Método para búsqueda con expresiones regulares y paginación
public async paginate(data: PaginateMessageParams): Promise<PaginateMessageResults> {
const skip = (data.page - 1) * data.limit;
// Construye los criterios de búsqueda
const searchCriteria: mongoose.FilterQuery<IMessage> = {};
// Agrega filtro por query usando expresiones regulares para 'name' y 'description'
if (data.query) {
const regex = new RegExp(data.query, "i");
searchCriteria.$or = [{ subject: regex }, { message: regex }];
}
// Filtra por categoryId si está presente
if (data.conversationId !== undefined) {
searchCriteria.conversationId = data.conversationId;
}
if (data.readed) {
searchCriteria.readed = data.readed;
}
// Realiza la consulta con paginación
const results = await this.messageList
.find(searchCriteria)
.sort({ messageDate: -1 })
.skip(skip)
.limit(data.limit)
.exec();
// Cuenta el total de documentos que coinciden con los criterios de búsqueda
const count = await this.messageList.countDocuments(searchCriteria).exec();
return {
data: results,
page: data.page,
pages: Math.ceil(count / data.limit),
};
}
public async markAsRead(data: FindMessageParams): Promise<void> {
await this.messageList.updateMany(
{ conversationId: data.conversationId },
{ $set: { readed: true } }
);
}
}
@@ -0,0 +1,78 @@
import {
CreateMessageConversationParams,
IMessageConversation,
MESSAGE_CONVERSATION_TYPES,
} from "../MessageConversations/MessagesConversations.Interface";
export type FindMessageParams = {
id?: string;
conversationId?: string;
messageDate?: Date;
readed?: boolean;
sessionUser: string;
};
export type PaginateMessageParams = {
query?: string;
conversationId?: string;
dateFrom?: Date;
dateTo?: Date;
readed?: boolean;
page: number;
limit: number;
sessionUser: string;
};
export type PaginateMessageResults = {
data: IMessage[];
page: number;
pages: number;
};
export type SendMessageParams = Omit<CreateMessageConversationParams, "type"> & {
conversationId?: string;
type?: MESSAGE_CONVERSATION_TYPES;
message: string;
};
export type CreateMessageParams = {
conversationId: string;
message: string;
sessionUser: string;
};
export type DeleteMessageParams = {
id: string;
sessionUser: string;
};
export interface IMessage {
id?: string;
conversationId?: string;
messageDate?: Date; //Fecha de envío del mensaje
readed: boolean; //Indica si el mensaje fue leido por el receptor.
message: string; //Contenido del mensaje.
userId: string; //Propietario del mensaje.
}
export interface ISendMessageResult {
message: IMessage;
conversation: IMessageConversation;
}
export interface IMessagesAdapter {
create(data: CreateMessageParams): Promise<IMessage>;
delete(id: string): Promise<void>;
find(filters: FindMessageParams): Promise<IMessage[]>;
findOne(filters: FindMessageParams): Promise<IMessage | null>;
paginate(filters: PaginateMessageParams): Promise<PaginateMessageResults>;
markAsRead(data: FindMessageParams): Promise<void>;
}
export interface IMessageManager {
messages: IMessagesAdapter;
sendMessage(data: SendMessageParams): Promise<ISendMessageResult>;
deleteMessage(data: DeleteMessageParams): Promise<void>;
paginateMessages(data: PaginateMessageParams): Promise<PaginateMessageResults>;
markAsRead(data: FindMessageParams): Promise<void>;
}
+211
View File
@@ -0,0 +1,211 @@
import UsersManager from "../Users/Users";
import {
DeleteMessageParams,
FindMessageParams,
IMessageManager,
ISendMessageResult,
PaginateMessageParams,
PaginateMessageResults,
SendMessageParams,
} from "./Messages.Interface";
import { MessagesAdapterMongoose } from "./Messages.Adapter.Mongoose";
import {
IMessageConversation,
MESSAGE_CONVERSATION_TYPES,
} from "../MessageConversations/MessagesConversations.Interface";
import MessageConversationList from "../MessageConversations/MessagesConversations";
import { NotificationsManager } from "../Notifications/Notifications";
import EmployeesList from "../Employees/Employee";
import CompaniesList from "../Companies/Companies";
import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface";
class MessageManager implements IMessageManager {
messages: MessagesAdapterMongoose;
constructor() {
this.messages = new MessagesAdapterMongoose();
}
public async sendMessage(data: SendMessageParams): Promise<ISendMessageResult> {
/*
* Cuando envio un mensaje tengo dos opciones:
* 1. Enviar un mensaje a una conversacion existente
* - En este caso debo proporcionar un conversationId.
* 2. Crear una nueva conversacion
* - En este caso debo proporcionar el tipo de conversacion que se va ha crear
* y los datos necesarios para crearla.
*/
if (data.sessionUser) {
const userCheck = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!userCheck) {
throw new Error("Usuario invalido");
}
}
let conversation: IMessageConversation | null = null;
if (data.conversationId) {
conversation = await MessageConversationList.conversations.findOne({
_id: data.conversationId,
});
if (!conversation) {
throw new Error("Conversacion invalida");
}
}
if (!data.conversationId) {
if (!data.type) {
throw new Error("El tipo de la conversación es requerido");
}
if (data.type === MESSAGE_CONVERSATION_TYPES.USER_TO_USER) {
conversation = await MessageConversationList.conversations.findOne({
userIdFrom: data.sessionUser,
userIdTo: data.userIdTo,
});
if (!conversation) {
conversation = await MessageConversationList.createConversation({
userIdFrom: data.sessionUser,
userIdTo: data.userIdTo,
type: MESSAGE_CONVERSATION_TYPES.USER_TO_USER,
sessionUser: data.sessionUser,
});
}
} else if (data.type === MESSAGE_CONVERSATION_TYPES.USER_TO_ORG) {
conversation = await MessageConversationList.conversations.findOne({
userIdFrom: data.sessionUser,
companyId: data.companyId,
});
if (!conversation) {
conversation = await MessageConversationList.createConversation({
userIdFrom: data.sessionUser,
companyId: data.companyId,
type: MESSAGE_CONVERSATION_TYPES.USER_TO_ORG,
sessionUser: data.sessionUser,
});
}
}
}
if (!conversation) {
throw new Error("No se pudo crear la conversacion");
}
if (!conversation.id) {
throw new Error("No se pudo crear la conversacion");
}
//Cuando alguien contesta la conversacion se pasa a user to user.
//De esta manera el userTo pasa a ser el que contesto la conversacion (data.sessionUser).
if (
conversation.companyId &&
!conversation.userIdTo &&
conversation.userIdFrom != data.sessionUser
) {
//Convertir la conversation en user to user
await MessageConversationList.convertUserToUser({
conversationId: conversation.id,
sessionUser: data.sessionUser,
});
}
const newMessage = await this.messages.create({
conversationId: conversation.id,
message: data.message,
sessionUser: data.sessionUser,
});
const shortMessage =
data.message.length > 100 ? data.message.substring(0, 100) + "..." : data.message;
if (conversation.userIdTo) {
const toUser =
String(data.sessionUser) === String(conversation.userIdTo)
? conversation.userIdFrom
: conversation.userIdTo;
await NotificationsManager.sendSystemNotification({
userId: String(toUser),
conversationId: conversation.id,
subject: "Nuevo mensaje recibido",
message: shortMessage,
type: NotificationType.MESSAGE,
code: String((newMessage as any)._id)
});
}
if (!conversation.userIdTo && conversation.companyId) {
const org = await CompaniesList.companies.findOne({
_id: conversation.companyId,
});
if (org) {
const employees = await EmployeesList.employees.find({
companyId: conversation.companyId,
});
for (const employee of employees) {
await NotificationsManager.sendSystemNotification({
userId: String(employee.userId),
conversationId: conversation.id,
subject: "Nueva consulta a la organización " + org.name,
message: shortMessage,
type: NotificationType.MESSAGE,
code: String((newMessage as any)._id)
});
}
}
}
return {
message: newMessage,
conversation: {
id: conversation.id,
userIdFrom: conversation.userIdFrom,
userIdTo: conversation.userIdTo,
companyId: conversation.companyId,
type: conversation.type,
newMessages: conversation.newMessages,
},
};
}
public async deleteMessage(data: DeleteMessageParams): Promise<void> {
const checkUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!checkUser) {
throw new Error("Usuario invalido");
}
const messacheCheck = await this.messages.messageList.findOne({
_id: data.id,
});
if (!messacheCheck) {
throw new Error("Mensaje invalido");
}
if (String(messacheCheck.userId) !== data.sessionUser) {
throw new Error("No tienes permiso para eliminar este mensaje");
}
await this.messages.delete(data.id);
}
public async paginateMessages(filters: PaginateMessageParams): Promise<PaginateMessageResults> {
return await this.messages.paginate(filters);
}
public async markAsRead(data: FindMessageParams): Promise<void> {
const checkUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!checkUser) {
throw new Error("Usuario invalido");
}
this.messages.markAsRead(data);
}
}
const MessagesList = new MessageManager();
export default MessagesList;
@@ -0,0 +1,163 @@
import { Document, Model, Schema, model } from "mongoose";
import { IMetrics, IMetricsAdapter, MetricsParams } from "./Metrics.Interface";
import dayjs from "dayjs";
import { isNull } from "../../helpers/IsNull";
export interface IMetricsDocument extends Omit<IMetrics, "id">, Document {}
export class MetricsAdapterMongoose implements IMetricsAdapter {
schema: Schema;
metricsList: Model<IMetricsDocument>;
constructor() {
this.schema = new Schema({
userId: { type: Schema.Types.ObjectId, required: true, ref: "User" },
organizationsCount: { type: Number, required: true },
employeesCount: { type: Number, required: true },
servicesCount: { type: Number, required: true },
appointmentsCount: { type: Number, required: true },
clientsCount: { type: Number, required: true },
repeatsCount: { type: Number, required: false },
month: { type: Number, required: true },
year: { type: Number, required: true },
creationDate: { type: Date, required: true, default: Date.now },
});
this.metricsList = model<IMetricsDocument>("Metric", this.schema);
}
private getDate(): { month: number; year: number } {
const dateNow = dayjs(new Date());
const month = dateNow.month();
const year = dateNow.year();
return { month, year };
}
public async create(userId: string): Promise<IMetricsDocument> {
const { month, year } = this.getDate();
return await this.metricsList.create({
userId,
month,
year,
organizationsCount: 0,
employeesCount: 0,
servicesCount: 0,
appointmentsCount: 0,
clientsCount: 0,
repeatsCount: 0,
});
}
public async reset(userId: string): Promise<IMetricsDocument> {
const { month, year } = this.getDate();
let metrics: IMetricsDocument | null = await this.metricsList.findOne({ userId });
if (metrics) {
metrics.appointmentsCount = 0;
// metrics.organizationsCount = 0;
// metrics.employeesCount = 0;
// metrics.servicesCount = 0;
// metrics.clientsCount = 0;
// metrics.repeatsCount = 0;
metrics.year = year;
metrics.month = month;
await metrics.save();
}
if (!metrics) {
metrics = await this.create(userId);
}
return metrics;
}
public async getMetrics(userId: string): Promise<IMetricsDocument> {
let metrics: IMetricsDocument | null = await this.metricsList.findOne({ userId });
if (!metrics) {
metrics = await this.create(userId);
}
return metrics;
}
public async addOrganization(data: MetricsParams): Promise<void> {
const { month, year } = this.getDate();
if (!data.userId) {
return;
}
let metrics = await this.getMetrics(data.userId);
if (metrics.year !== year || metrics.month !== month) {
metrics = await this.reset(data.userId);
}
metrics.organizationsCount += isNull<number>(data.quantity, 1);
await metrics.save();
}
public async addEmployee(data: MetricsParams): Promise<void> {
const { month, year } = this.getDate();
if (!data.userId) {
return;
}
let metrics = await this.getMetrics(data.userId);
if (metrics.year !== year || metrics.month !== month) {
metrics = await this.reset(data.userId);
}
metrics.employeesCount += isNull<number>(data.quantity, 1);
await metrics.save();
}
public async addService(data: MetricsParams): Promise<void> {
const { month, year } = this.getDate();
if (!data.userId) {
return;
}
let metrics = await this.getMetrics(data.userId);
if (metrics.year !== year || metrics.month !== month) {
metrics = await this.reset(data.userId);
}
metrics.servicesCount += isNull<number>(data.quantity, 1);
await metrics.save();
}
public async addAppointment(data: MetricsParams): Promise<void> {
const { month, year } = this.getDate();
if (!data.userId) {
return;
}
let metrics = await this.getMetrics(data.userId);
if (metrics.year !== year || metrics.month !== month) {
metrics = await this.reset(data.userId);
}
metrics.appointmentsCount += isNull<number>(data.quantity, 1);
await metrics.save();
}
public async addClient(data: MetricsParams): Promise<void> {
const { month, year } = this.getDate();
if (!data.userId) {
return;
}
let metrics = await this.getMetrics(data.userId);
if (metrics.year !== year || metrics.month !== month) {
metrics = await this.reset(data.userId);
}
metrics.clientsCount += isNull<number>(data.quantity, 1);
await metrics.save();
}
public async addRepeat(data: MetricsParams): Promise<void> {
const { month, year } = this.getDate();
if (!data.userId) {
return;
}
let metrics = await this.getMetrics(data.userId);
if (metrics.year !== year || metrics.month !== month) {
metrics = await this.reset(data.userId);
}
if (!metrics.repeatsCount) {
metrics.repeatsCount = 0;
}
metrics.repeatsCount += isNull<number>(data.quantity, 1);
await metrics.save();
}
}
@@ -0,0 +1,56 @@
import { IMetricsDocument } from "./Metrics.Adapter.Mongoose";
export type MetricsParams = {
userId?: string;
quantity?: number;
};
export type DeleteMetricsByCompany = {
companyId: string;
};
export type CalculateMetricsParams = {
userId: string;
};
export interface IMetrics {
id?: string;
userId: string;
organizationsCount: number;
employeesCount: number;
servicesCount: number;
appointmentsCount: number;
clientsCount: number;
repeatsCount: number;
month: number;
year: number;
}
export interface IMetricsAdapter {
create(userId: string): Promise<IMetricsDocument>;
reset(userId: string): Promise<IMetricsDocument>;
getMetrics(userId: string): Promise<IMetricsDocument>;
addOrganization(data: MetricsParams): Promise<void>;
addEmployee(data: MetricsParams): Promise<void>;
addService(data: MetricsParams): Promise<void>;
addAppointment(data: MetricsParams): Promise<void>;
addClient(data: MetricsParams): Promise<void>;
}
export interface IMetricsManager {
metrics: IMetricsAdapter;
reset(userId: string): Promise<void>;
getMetrics(userId: string): Promise<IMetricsDocument>;
addOrganization(data: MetricsParams): Promise<void>;
addEmployee(data: MetricsParams): Promise<void>;
addService(data: MetricsParams): Promise<void>;
addAppointment(data: MetricsParams): Promise<void>;
addClient(data: MetricsParams): Promise<void>;
canAddOrganization(userId: string): Promise<boolean>;
canAddEmployee(userId: string): Promise<boolean>;
canAddService(userId: string): Promise<boolean>;
canAddAppointment(userId: string): Promise<boolean>;
canAddClient(userId: string): Promise<boolean>;
deleteMetricsByCompany(data: DeleteMetricsByCompany): Promise<void>;
calculateMetrics(data: CalculateMetricsParams): Promise<void>;
}
+268
View File
@@ -0,0 +1,268 @@
import {
CalculateMetricsParams,
DeleteMetricsByCompany,
IMetricsManager,
MetricsParams,
} from "./Metrics.Interface";
import { IMetricsDocument, MetricsAdapterMongoose } from "./Metrics.Adapter.Mongoose";
import PlansList from "../../Models/Plans/Plans";
import PlanSubscriptionsList from "../../Models/PlanSubscriptions/PlanSubscriptons";
import CompaniesList from "../Companies/Companies";
import EmployeesList from "../Employees/Employee";
import ServiceList from "../Services/Service";
import AppointmentList from "../Appointments/Appointments";
import ClientsList from "../Clients/Clients";
import RepeatsList from "../Repeats/Repeats";
import { isNull } from "../../helpers/IsNull";
class MetricsManager implements IMetricsManager {
metrics: MetricsAdapterMongoose;
constructor() {
this.metrics = new MetricsAdapterMongoose();
}
public async calculateMetrics(data: CalculateMetricsParams): Promise<void> {
//TODO: calcular las metricas...
const metrics = await this.getMetrics(data.userId);
const organizations = await CompaniesList.companies.find({
ownerId: data.userId,
});
let employeesLength = 0;
let servicesLength = 0;
let appointmentsLength = 0;
let clientsLength = 0;
let repeatsLength = 0;
for (const org of organizations) {
const employees = await EmployeesList.employees.find({
companyId: String(org.id),
});
employeesLength += employees.length;
const services = await ServiceList.services.find({
companyId: String(org.id),
});
servicesLength += services.length;
//TODO: Revisar si el parametro del mes esta bien o necesita incrementar en uno.
//ya que no recuerdo si empieza en 0 o en 1.
appointmentsLength += await AppointmentList.countAppointmentsByMonth({
year: metrics.year,
month: metrics.month,
companyId: String(org.id),
});
const clients = await ClientsList.clients.find({
companyId: String(org.id),
});
clientsLength += clients.length;
const repeats = await RepeatsList.repeats.find({
companyId: String(org.id),
});
repeatsLength += repeats.length;
}
metrics.organizationsCount = organizations.length;
metrics.employeesCount = employeesLength;
metrics.servicesCount = servicesLength;
metrics.appointmentsCount = appointmentsLength;
metrics.clientsCount = clientsLength;
metrics.repeatsCount = repeatsLength;
await metrics.save();
}
public async deleteMetricsByCompany(data: DeleteMetricsByCompany): Promise<void> {
const companyCheck = await CompaniesList.companies.findOne({ _id: data.companyId });
if (!companyCheck) {
throw new Error("La compañia no existe");
}
await this.metrics.reset(String(companyCheck.ownerId));
//await this.metrics.metricsList.deleteMany({ userId: String(companyCheck.ownerId) });
}
public async reset(userId: string): Promise<void> {
await this.metrics.reset(userId);
}
public async getMetrics(userId: string): Promise<IMetricsDocument> {
return await this.metrics.getMetrics(userId);
}
public async canAddOrganization(userId: string): Promise<boolean> {
const subscription = await PlanSubscriptionsList.findOne({
sessionUser: userId,
});
if (!subscription) {
return false;
}
const plan = await PlansList.plans.findOne({
_id: subscription.planId,
});
if (!plan) {
return false;
}
if (plan.limitOrganizations < 0) {
return true;
}
const metrics = await this.getMetrics(userId);
if (metrics.organizationsCount >= plan.limitOrganizations) {
return false;
}
return true;
}
public async canAddService(userId: string): Promise<boolean> {
const subscription = await PlanSubscriptionsList.findOne({
sessionUser: userId,
});
if (!subscription) {
return false;
}
const plan = await PlansList.plans.findOne({
_id: subscription.planId,
});
if (!plan) {
return false;
}
if (plan.limitServices < 0) {
return true;
}
const metrics = await this.getMetrics(userId);
if (metrics.servicesCount >= plan.limitServices) {
return false;
}
return true;
}
public async canAddEmployee(userId: string): Promise<boolean> {
const subscription = await PlanSubscriptionsList.findOne({
sessionUser: userId,
});
if (!subscription) {
return false;
}
const plan = await PlansList.plans.findOne({
_id: subscription.planId,
});
if (!plan) {
return false;
}
if (plan.limitEmployees < 0) {
return true;
}
const metrics = await this.getMetrics(userId);
if (metrics.employeesCount >= plan.limitEmployees) {
return false;
}
return true;
}
public async canAddClient(userId: string): Promise<boolean> {
const subscription = await PlanSubscriptionsList.findOne({
sessionUser: userId,
});
if (!subscription) {
return false;
}
const plan = await PlansList.plans.findOne({
_id: subscription.planId,
});
if (!plan) {
return false;
}
if (plan.limitClients < 0) {
return true;
}
const metrics = await this.getMetrics(userId);
if (metrics.clientsCount >= plan.limitClients) {
return false;
}
return true;
}
public async canAddRepeat(userId: string): Promise<boolean> {
const subscription = await PlanSubscriptionsList.findOne({
sessionUser: userId,
});
if (!subscription) {
return false;
}
const plan = await PlansList.plans.findOne({
_id: subscription.planId,
});
if (!plan) {
return false;
}
if (plan.limitRepeats < 0) {
return true;
}
const metrics = await this.getMetrics(userId);
if (isNull<number>(metrics.repeatsCount, 0) >= plan.limitRepeats) {
return false;
}
return true;
}
public async canAddAppointment(userId: string): Promise<boolean> {
const subscription = await PlanSubscriptionsList.findOne({
sessionUser: userId,
});
if (!subscription) {
return false;
}
const plan = await PlansList.plans.findOne({
_id: subscription.planId,
});
if (!plan) {
return false;
}
if (plan.limitAppointments < 0) {
return true;
}
const metrics = await this.getMetrics(userId);
if (metrics.appointmentsCount >= plan.limitAppointments) {
return false;
}
return true;
}
public async addAppointment(data: MetricsParams): Promise<void> {
await this.metrics.addAppointment(data);
}
public async addClient(data: MetricsParams): Promise<void> {
await this.metrics.addClient(data);
}
public async addRepeat(data: MetricsParams): Promise<void> {
await this.metrics.addRepeat(data);
}
public async addEmployee(data: MetricsParams): Promise<void> {
await this.metrics.addEmployee(data);
}
public async addOrganization(data: MetricsParams): Promise<void> {
await this.metrics.addOrganization(data);
}
public async addService(data: MetricsParams): Promise<void> {
await this.metrics.addService(data);
}
}
const MetricsList = new MetricsManager();
export default MetricsList;
@@ -0,0 +1,22 @@
import WapServerList from "../WapServer/WapServer";
import { SendBotMessageParams } from "../WapServer/WapServer.Interface";
import { INotificationsAdapter, NotificationDataWap } from "./Notifications.Interface";
export class NotificationsWhatsappBotAdapter implements INotificationsAdapter<NotificationDataWap> {
async send(data: NotificationDataWap): Promise<void> {
if (!data.phoneNumber) {
throw new Error("La notificación de Whatsapp no se ha enviado. No se ha especificado el número de contacto.");
}
const sendData: SendBotMessageParams = {
number: data.phoneNumber,
message: data.message,
companyId: data.companyId,
sessionUser: data.sessionUser
}
await WapServerList.sendMessage(sendData);
}
}
@@ -0,0 +1,33 @@
import { INotificationsAdapter, NotificationDataEmail } from "./Notifications.Interface";
import nodemailer, { SentMessageInfo } from "nodemailer";
export class NotificationsNodemailerAdapter implements INotificationsAdapter<NotificationDataEmail> {
async send(data: NotificationDataEmail): Promise<void> {
const mailTransporter = nodemailer.createTransport({
service: "gmail",
secure: true,
port: 465,
auth: {
user: process.env.GMAIL_EMAIL,
pass: process.env.GMAIL_PASSWORD,
},
});
const mailOptions = {
from: process.env.GMAIL_EMAIL,
to: data.email,
subject: data.subject,
text: data.message,
};
await mailTransporter.sendMail(
mailOptions,
(error: Error | null, info: SentMessageInfo) => {
if (error) {
return console.log(error);
}
console.log("Correo enviado: " + info.response);
}
);
}
}
@@ -0,0 +1,28 @@
import { io } from "../../index";
import SystemNotificationsList from "../SystemNotifications/SystemNotification";
import { CreateSystemNotificationParams } from "../SystemNotifications/SystemNotification.Interface";
import { INotificationsAdapter, NotificationDataSystem } from "./Notifications.Interface";
export class NotificationsSystemAdapter implements INotificationsAdapter<NotificationDataSystem> {
async send(data: NotificationDataSystem): Promise<void> {
const sendData: CreateSystemNotificationParams = {
userId: data.userId,
subject: data.subject,
conversationId: data.conversationId,
message: data.message,
type: data.type,
code: data.code,
};
const notif = await SystemNotificationsList.createNotification(sendData);
if (notif.userId) {
const countUnreadNotifications = await SystemNotificationsList.finUnreadNotifications({
userId: notif.userId,
sessionUser: notif.userId,
});
io.to(`user:${data.userId}`).emit("new_system_notification", countUnreadNotifications);
}
}
}
@@ -0,0 +1,26 @@
import axios from "axios";
import { INotificationsAdapter, NotificationDataEmail } from "./Notifications.Interface";
export class NotificationsDonWebAdapter implements INotificationsAdapter<NotificationDataEmail> {
async send(data: NotificationDataEmail): Promise<void> {
const dataEmail = {
from: "norepply@turnosxpress.com.ar",
to: data.email,
subject: data.subject,
html: data.message,
};
const apiConfig = {
method: "post",
maxBodyLength: Infinity,
url: "https://api.envialosimple.email/api/v1/mail/send",
headers: {
Authorization: `Bearer ${process.env.DONWEB_MAIL_API_KEY}`,
"Content-Type": "application/json",
},
data: dataEmail,
};
await axios(apiConfig);
}
}
@@ -0,0 +1,31 @@
export enum NOTIFICATION_TYPES {
EMAIL = "EMAIL",
WAP = "WAP",
SYSTEM = "SYSTEM",
}
export type NotificationDataEmail = {
email: string;
subject: string;
message: string;
};
export type NotificationDataWap = {
phoneNumber: string;
message: string;
companyId: string;
sessionUser: string;
};
export type NotificationDataSystem = {
userId: string;
conversationId?: string;
subject: string;
message: string;
type?: string;
code?: string;
};
export interface INotificationsAdapter<T> {
send: (data: T) => Promise<void>;
}
@@ -0,0 +1,37 @@
import {
NotificationDataEmail,
NotificationDataSystem,
NotificationDataWap,
} from "./Notifications.Interface";
import { NotificationsWhatsappBotAdapter } from "./Notifications.Adapter.Bot";
import { NotificationsSystemAdapter } from "./Notifications.Adapter.System";
import { NotificationsDonWebAdapter } from "./Notifications.Adapter.donweb";
class NotificationsMngr {
//notificationsNodeMailer: NotificationsNodemailerAdapter;
notificationsDonWeb: NotificationsDonWebAdapter;
notificationsBot: NotificationsWhatsappBotAdapter;
notificationsSystem: NotificationsSystemAdapter;
constructor() {
//this.notificationsNodeMailer = new NotificationsNodemailerAdapter();
this.notificationsDonWeb = new NotificationsDonWebAdapter();
this.notificationsBot = new NotificationsWhatsappBotAdapter();
this.notificationsSystem = new NotificationsSystemAdapter();
}
async sendEmail(data: NotificationDataEmail): Promise<void> {
//await this.notificationsNodeMailer.send(data);
await this.notificationsDonWeb.send(data);
}
async sendWap(data: NotificationDataWap): Promise<void> {
await this.notificationsBot.send(data);
}
async sendSystemNotification(data: NotificationDataSystem): Promise<void> {
await this.notificationsSystem.send(data);
}
}
export const NotificationsManager = new NotificationsMngr();
@@ -0,0 +1,25 @@
import { FindPartidosParams, IPartidosAdapter, IPartido } from "./Partidos.interface";
import { Document, Model, Schema, model } from "mongoose";
export interface IPartidoDocument extends Omit<IPartido, "id">, Document {}
export class PartidosAdapterMongoose implements IPartidosAdapter {
schema: Schema;
partidoList: Model<IPartidoDocument>;
constructor() {
this.schema = new Schema({
name: { type: String, required: true, unique: true },
code: { type: Number, required: true, default: 0 },
provincia: { type: String, required: true },
latitude: { type: Number, required: true, default: 0 },
longitude: { type: Number, required: true, default: 0 },
});
this.partidoList = model<IPartidoDocument>("Partido", this.schema);
}
public async find(filters: FindPartidosParams): Promise<IPartido[]> {
return this.partidoList.find(filters).exec();
}
}
@@ -0,0 +1,26 @@
import { TextObjectFilterResult } from "../TextObjectFilter.model";
export type FindPartidosParams = {
_id?: string;
name?: string;
code?: number;
provincia: string;
};
export interface IPartido {
id?: string;
name: string;
code: number;
provincia: string;
latitude: number;
longitude: number;
}
export interface IPartidosAdapter {
find(filters: FindPartidosParams): Promise<IPartido[]>;
}
export interface IPartidosManager {
partidos: IPartidosAdapter;
filterAll(data: FindPartidosParams): Promise<TextObjectFilterResult[]>;
}
+28
View File
@@ -0,0 +1,28 @@
import { TextObjectFilterResult } from "../../Models/TextObjectFilter.model";
import { PartidosAdapterMongoose } from "./Partidos.Adapter.Mongoose";
import { FindPartidosParams, IPartidosManager } from "./Partidos.interface";
class PartidosManager implements IPartidosManager {
partidos: PartidosAdapterMongoose;
constructor() {
this.partidos = new PartidosAdapterMongoose();
}
public async filterAll(data: FindPartidosParams): Promise<TextObjectFilterResult[]> {
const partidos = await this.partidos.find(data);
const returnData: TextObjectFilterResult[] = [];
for (const partido of partidos) {
returnData.push({
id: partido.name,
name: partido.name.toLowerCase(),
});
}
return returnData;
}
}
const PartidosList = new PartidosManager();
export default PartidosList;
@@ -0,0 +1,158 @@
import { Document, Model, Schema, model, FilterQuery } from "mongoose";
import {
CreatePaymentExtraParams,
FindPaymentsParams,
IPayment,
IPaymentsAdapter,
PaginatePaymentsParams,
PaginatePaymentsResults,
} from "./Payments.Interface";
import {
CLIENT_ACCOUNT_MOVEMENT_STATUS,
CLIENT_ACCOUNT_PAYMENT_METHODS,
} from "../ClientAccountMovements/ClientAccountMovements.Interface";
export interface IPaymentDocument extends Omit<IPayment, "id" | "_id">, Document {
_id: string;
}
export class PaymentsAdapterMongoose implements IPaymentsAdapter {
schema: Schema;
paymentsList: Model<IPaymentDocument>;
constructor() {
this.schema = new Schema({
clientId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Client",
},
companyId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Company",
},
employeeId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Employee",
},
serviceId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Service",
},
appointmentId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Appointment",
},
amount: { type: Number, required: true, default: 0 },
status: {
type: String,
required: true,
default: CLIENT_ACCOUNT_MOVEMENT_STATUS.PENDING,
},
method: {
type: String,
required: true,
default: CLIENT_ACCOUNT_PAYMENT_METHODS.CASH,
},
description: { type: String, required: false, default: "" },
createdAt: { type: Date, required: true, default: Date.now },
updatedAt: { type: Date, required: true, default: Date.now },
});
this.paymentsList = model<IPaymentDocument>("Payment", this.schema);
}
public async create(data: CreatePaymentExtraParams): Promise<IPayment> {
return await this.paymentsList.create(data);
}
public async delete(id: string): Promise<void> {
await this.paymentsList.deleteOne({ _id: id });
}
public async find(filters: Omit<FindPaymentsParams, "sessionUser">): Promise<IPayment[]> {
const searchCriteria = this.buildSearchCriteria(filters);
return await this.paymentsList.find(searchCriteria).exec();
}
public async findOne(
filters: Omit<FindPaymentsParams, "sessionUser">
): Promise<IPaymentDocument | null> {
return await this.paymentsList.findOne(filters).exec();
}
private buildSearchCriteria(
filters: Omit<FindPaymentsParams, "sessionUser">
): FilterQuery<IPayment> {
const searchCriteria: FilterQuery<IPayment> = {};
if (filters.startDate) {
searchCriteria.createdAt = {
$gte: filters.startDate,
$lte: filters.endDate,
};
}
if (filters.status) {
searchCriteria.status = filters.status;
}
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;
}
if (filters.appointmentId) {
searchCriteria.appointmentId = filters.appointmentId;
}
return searchCriteria;
}
public async paginate(filters: PaginatePaymentsParams): Promise<PaginatePaymentsResults> {
const searchCriteria = this.buildSearchCriteria(filters);
const count = await this.paymentsList.countDocuments(searchCriteria).exec();
const skip = (filters.page - 1) * filters.limit;
const results = await this.paymentsList
.find(searchCriteria)
.skip(skip)
.limit(filters.limit)
.sort({ createdAt: -1 })
.exec();
let pages = Math.ceil(count / filters.limit);
if (pages < 1) {
pages = 1;
}
return {
data: results,
page: filters.page,
pages: pages,
};
}
}
@@ -0,0 +1,178 @@
import {
CLIENT_ACCOUNT_MOVEMENT_STATUS,
CLIENT_ACCOUNT_PAYMENT_METHODS,
} from "../ClientAccountMovements/ClientAccountMovements.Interface";
import { IPaymentDocument } from "./Payments.Adapter.Mongoose";
export enum PAYMENT_METHODS_CLIENT_ACCOUNT {
CLIENT_ACCOUNT = "ACCOUNT",
}
export type PAYMENT_METHODS = CLIENT_ACCOUNT_PAYMENT_METHODS | PAYMENT_METHODS_CLIENT_ACCOUNT;
export type FindPaymentsParams = {
id?: string;
clientId?: string;
clientIds?: string[];
companyId?: string;
appointmentId?: string;
employeeId?: string;
serviceId?: string;
status?: CLIENT_ACCOUNT_MOVEMENT_STATUS;
method?: PAYMENT_METHODS;
startDate?: Date;
endDate?: Date;
proccessDate?: PAYMENT_PROCCESS_DATE;
sessionUser?: string;
};
export type PaginatePaymentsParams = FindPaymentsParams & {
page: number;
limit: number;
};
export type PaginatePaymentsResults = {
data: IPayment[];
page: number;
pages: number;
};
export enum PAYMENT_PROCCESS_DATE {
NOW = "now",
APPOINTMENT = "appointment",
}
export type CreatePaymentParams = {
amount: number;
method: PAYMENT_METHODS;
status: CLIENT_ACCOUNT_MOVEMENT_STATUS;
appointmentId: string;
description?: string;
proccessDate: PAYMENT_PROCCESS_DATE;
sessionUser?: string;
};
export type CreatePaymentExtraParams = CreatePaymentParams & {
clientId: string;
companyId: string;
employeeId: string;
serviceId: string;
};
export type DeletePaymentParams = {
id: string;
sessionUser?: string;
};
export type GetAppointmentPaymentsParams = {
appointmentId: string;
sessionUser?: string;
};
export type MarkAsPayedParams = {
appointmentId: string;
method: PAYMENT_METHODS;
sessionUser?: string;
};
export interface PaginatePaymentsView {
id: string;
appointmentId: string;
appointmentDate: Date;
clientId: string;
clientName: string;
employeeId: string;
employeeName: string;
serviceId: string;
serviceName: string;
price: number;
status: CLIENT_ACCOUNT_MOVEMENT_STATUS;
method: PAYMENT_METHODS;
description: string;
createdAt: Date;
}
export type PaginateNotPayedAppointmentsParams = FindPaymentsParams & {
page: number;
limit: number;
};
export type PaginateNotPayedAppointmentsResult = {
data: PaginatePaymentsView[];
page: number;
pages: number;
};
export type PaginatePaymentsViewParams = FindPaymentsParams & {
page: number;
limit: number;
};
export type PaginatePaymentsViewResults = {
data: PaginatePaymentsView[];
page: number;
pages: number;
};
export type DeletePaymentsByCompanyParams = {
companyId: string;
};
export type DeletePaymentsByClientParams = {
clientId: string;
};
export interface IPayment {
id?: string;
_id?: string;
amount: number;
status: CLIENT_ACCOUNT_MOVEMENT_STATUS;
method: PAYMENT_METHODS;
clientId: string;
companyId: string;
employeeId: string;
serviceId: string;
appointmentId: string;
description: string;
createdAt: Date;
updatedAt: Date;
}
export interface IPaymentsAdapter {
create(data: CreatePaymentExtraParams): Promise<IPayment>;
delete(id: string): Promise<void>;
find(filters: FindPaymentsParams): Promise<IPayment[]>;
findOne(filters: FindPaymentsParams): Promise<IPaymentDocument | null>;
paginate(filters: PaginatePaymentsParams): Promise<PaginatePaymentsResults>;
}
export type NotPayedReportResult = {
items: PaginatePaymentsView[];
totalAmount: number;
};
export type PaymentsReportResult = {
items: PaginatePaymentsView[];
totalAmount: number;
};
export interface IPaymentsManager {
payments: IPaymentsAdapter;
createPayment(data: CreatePaymentParams): Promise<IPayment>;
getAppointmentPayments(datas: GetAppointmentPaymentsParams): Promise<IPayment[]>;
deletePayment(data: DeletePaymentParams): Promise<void>;
deletePaymentsByCompany(data: DeletePaymentsByCompanyParams): Promise<void>;
deletePaymentsByClient(data: DeletePaymentsByClientParams): Promise<void>;
markAsPayed(data: MarkAsPayedParams): Promise<void>;
paginatePaymentsView(data: PaginatePaymentsViewParams): Promise<PaginatePaymentsViewResults>;
paginatedPaymentsViewTotal(data: FindPaymentsParams): Promise<number>;
paginateNotPayedAppointments(
data: PaginateNotPayedAppointmentsParams
): Promise<PaginateNotPayedAppointmentsResult>;
paginatedNotPayedAppiontmentsTotal(data: FindPaymentsParams): Promise<number>;
payNotPayedAppointments(data: FindPaymentsParams): Promise<void>;
getNotPayedReport(data: FindPaymentsParams): Promise<NotPayedReportResult>;
getPaymentsReport(data: FindPaymentsParams): Promise<PaymentsReportResult>;
}
+749
View File
@@ -0,0 +1,749 @@
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<PaymentsReportResult> {
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<void> {
await this.payments.paymentsList.deleteMany({
companyId: data.companyId,
});
}
public async deletePaymentsByClient(data: DeletePaymentsByClientParams): Promise<void> {
await this.payments.paymentsList.deleteMany({
clientId: data.clientId,
});
}
public async paginatedPaymentsViewTotal(data: FindPaymentsParams): Promise<number> {
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<PaginatePaymentsViewResults> {
const userSession = await this.validateSessionUser((data.sessionUser || ""));
await this.validatePermissions(String(userSession.id), String(data.companyId));
const results: Array<PaginatePaymentsView> = [];
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<IUserDocument> {
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<void> {
// 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<number> {
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<IPayment> {
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<void> {
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<void> {
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<IPayment[]> {
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<IAppointment> {
const searchCriteria: FilterQuery<IAppointment> = {};
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<PaginateNotPayedAppointmentsResult> {
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<number> {
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<NotPayedReportResult> {
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<void> {
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();
@@ -0,0 +1,23 @@
export type FindPlanPaymentsParams = {
_id?: string;
};
export interface IPlanPayment {
id?: string;
userId: string; // Referencia al ID del usuario
subscriptionId: string; // Referencia al ID de la suscripción
amount: number; // Monto pagado
paymentDate: Date; // Fecha del pago
paymentMethod: string; // Método de pago (tarjeta, PayPal, etc.)
status: "completed" | "failed" | "pending"; // Estado del pago
transactionId?: string; // Identificador de la transacción
}
export interface IPlanPaymentsAdapter {
find(filters: FindPlanPaymentsParams): Promise<IPlanPayment[]>;
}
export interface IPlanPaymentsManager {
planPayments: IPlanPaymentsAdapter;
find(data: FindPlanPaymentsParams): Promise<IPlanPayment[]>;
}
@@ -0,0 +1,106 @@
import dayjs from "dayjs";
import {
FindPlanSuscripcionsParams,
IPlanSuscriptionsAdapter,
IPlanSuscription,
CreatePlanSuscriptionParams,
CancellPlanSuscriptionParams,
} from "./PlanSubscriptions.interface";
import { Document, Model, Schema, model } from "mongoose";
export interface IPlanSuscriptionDocument extends Omit<IPlanSuscription, "id">, Document {}
export enum MP_SUBS_STATUS {
PENDING = "pending",
AUTHORIZED = "authorized",
PAUSED = "paused",
CANCELLED = "cancelled",
}
export class PlanSuscriptionsAdapterMongoose implements IPlanSuscriptionsAdapter {
schema: Schema;
planSuscriptionList: Model<IPlanSuscriptionDocument>;
constructor() {
this.schema = new Schema({
userId: { type: Schema.Types.ObjectId, required: true, ref: "User" },
planId: { type: Schema.Types.ObjectId, required: true, ref: "Plan" },
mpPlanId: { type: String, required: false },
startDate: { type: Date, required: true },
endDate: { type: Date, required: true },
isActive: { type: Boolean, required: true, default: true },
autoRenew: { type: Boolean, required: true, default: true },
paymentMethod: { type: String, required: false },
mpPayerId: { type: Number, required: false },
mpPayerEmail: { type: String, required: false },
mpStatus: { type: String, required: false },
mpDateCreated: { type: Date, required: false },
mpInitPoint: { type: String, required: false },
mpPreferenceId: { type: String, required: false },
});
this.planSuscriptionList = model<IPlanSuscriptionDocument>("PlanSuscription", this.schema);
}
public async find(filters: FindPlanSuscripcionsParams): Promise<IPlanSuscription[]> {
return this.planSuscriptionList.find(filters).exec();
}
public async delete(data: CancellPlanSuscriptionParams): Promise<void> {
await this.planSuscriptionList.deleteOne({ userId: data.sessionUser }).exec();
}
public async findOne(
filters: FindPlanSuscripcionsParams
): Promise<IPlanSuscriptionDocument | null> {
let subscription = await this.planSuscriptionList
.findOne({ userId: filters.sessionUser })
.exec();
if (!subscription) {
return null;
}
const dateNow = dayjs(new Date());
const dateEnd = dayjs(subscription.endDate);
try {
if (subscription.mpPreferenceId) {
if (dateNow.isAfter(dateEnd)) {
if (subscription.mpStatus == MP_SUBS_STATUS.CANCELLED) {
subscription.mpPreferenceId = "";
subscription.isActive = false;
subscription.autoRenew = false;
subscription.mpStatus = MP_SUBS_STATUS.CANCELLED;
subscription.mpInitPoint = "";
await subscription.save();
return subscription;
}
// Without subscriptions, we wait for the webhook to update the status.
// If it's expired and not updated by webhook, we just cancel it.
if (subscription.mpStatus === MP_SUBS_STATUS.PENDING) {
subscription.mpPreferenceId = "";
subscription.isActive = false;
subscription.autoRenew = false;
subscription.mpStatus = MP_SUBS_STATUS.CANCELLED;
subscription.mpInitPoint = "";
await subscription.save();
return subscription;
}
}
}
} catch (e) {
console.log("Ha ocurrido un error al revisar el estado de la suscripcion:", e);
}
return subscription;
}
// updateMpStatus was removed as it relied on MercadoPago Subscriptions API.
// Payments status updates are now handled by the WebhookController.
public async create(data: CreatePlanSuscriptionParams): Promise<IPlanSuscriptionDocument> {
return await this.planSuscriptionList.create(data);
}
}
@@ -0,0 +1,123 @@
import { IPlan, PlanFeatures } from "../../Models/Plans/Plans.interface";
import { IPlanSuscriptionDocument } from "./PlanSubscriptions.Adapter.Mongoose";
export type FindPlanSuscripcionsParams = {
sessionUser: string;
};
export type CreatePlanSuscriptionParams = {
userId: string;
planId: string;
mpPlanId: string;
startDate: Date;
endDate: Date;
autoRenew: boolean;
mpPayerId?: number;
mpPayerEmail?: string;
mpStatus?: string;
mpDateCreated?: Date;
mpInitPoint?: string;
mpPreferenceId?: string;
};
export type CancellPlanSuscriptionParams = {
sessionUser: string;
};
export type CreateMPPreApprovalParams = {
planId: string;
sessionUser: string;
months: number;
};
export type CheckFeatureParams = {
userId: string;
feature: PlanFeatures;
};
export enum MP_PRE_APPROVAL_STATUS {
READY_TO_START = "ready_to_start",
NO_PAYER_EMAIL = "no_payer_email",
}
export type MPPreApprovalResponse = {
init_point: string;
status: MP_PRE_APPROVAL_STATUS;
};
export type ToFreePlanParams = {
sessionUser: string;
};
export type ToMpParams = {
planId: string;
sessionUser: string;
months: number;
};
export type CheckMpStatusParams = {
mpPreferenceId: string;
sessionUser: string;
};
export type DeletePlanSuscriptionByUserParams = {
sessionUser: string;
};
export type GetSubscriptionInitPointParams = {
sessionUser: string;
};
export interface GetSuscriptionInitPointResponse {
init_point: string;
}
export interface IPlanSuscription {
id?: string;
userId: string; // Referencia al ID del usuario
planId: string; // Referencia al ID del plan suscrito
mpPlanId: string; //Referencia al ultimo plan contratado por mercadopago.
startDate: Date; // Fecha de inicio de la suscripción
endDate: Date; // Fecha de finalización de la suscripción
isActive: boolean; // Estado de la suscripción (activa o no)
autoRenew: boolean; // Indica si la suscripción se renueva automáticamente
paymentMethod: string; // Método de pago usado para la suscripción
mpPayerId: number; // ID del pagador en MercadoPago.
mpPayerEmail: string; // Email del pagador en MercadoPago.
mpStatus: string; // Estado de la suscripción en MercadoPago.
mpDateCreated: Date; // Fecha de creación de la suscripción en MercadoPago.
mpInitPoint: string; // URL de inicio de la suscripción en MercadoPago.
mpPreferenceId: string; // ID de la preferencia en MercadoPago.
}
export interface ISubscriptionInfo {
id: string;
plan: IPlan;
startDate: Date;
endDate: Date;
isActive: boolean;
autoRenew: boolean;
mpStatus: string;
mpDateCreated: Date;
}
export interface IPlanSuscriptionsAdapter {
find(filters: FindPlanSuscripcionsParams): Promise<IPlanSuscription[]>;
findOne(data: FindPlanSuscripcionsParams): Promise<IPlanSuscriptionDocument | null>;
create(data: CreatePlanSuscriptionParams): Promise<IPlanSuscriptionDocument>;
delete(data: CancellPlanSuscriptionParams): Promise<void>;
}
export interface IPlanSuscriptionsManager {
planSuscriptions: IPlanSuscriptionsAdapter;
create(data: CreateMPPreApprovalParams): Promise<MPPreApprovalResponse>;
cancell(data: CancellPlanSuscriptionParams): Promise<void>;
findOne(data: FindPlanSuscripcionsParams): Promise<IPlanSuscriptionDocument | null>;
getSubscriptionByUser(data: FindPlanSuscripcionsParams): Promise<ISubscriptionInfo | null>;
checkFeature(data: CheckFeatureParams): Promise<boolean>;
isSubscriptionActive(data: FindPlanSuscripcionsParams): Promise<boolean>;
toFreePlan(data: ToFreePlanParams): Promise<void>;
toMP(data: ToMpParams): Promise<IPlanSuscriptionDocument>;
deletePlanSuscriptionByUser(data: DeletePlanSuscriptionByUserParams): Promise<void>;
getInitPoint(data: GetSubscriptionInitPointParams): Promise<GetSuscriptionInitPointResponse>;
}
@@ -0,0 +1,562 @@
import { io } from "../../index";
import {
IPlanSuscriptionDocument,
MP_SUBS_STATUS,
PlanSuscriptionsAdapterMongoose,
} from "./PlanSubscriptions.Adapter.Mongoose";
import UsersManager from "../Users/Users";
import {
CancellPlanSuscriptionParams,
CheckFeatureParams,
CreateMPPreApprovalParams,
DeletePlanSuscriptionByUserParams,
FindPlanSuscripcionsParams,
GetSubscriptionInitPointParams,
GetSuscriptionInitPointResponse,
IPlanSuscriptionsManager,
ISubscriptionInfo,
MP_PRE_APPROVAL_STATUS,
MPPreApprovalResponse,
ToFreePlanParams,
ToMpParams,
} from "./PlanSubscriptions.interface";
import axios from "axios";
import PlansList from "../../Models/Plans/Plans";
import dayjs from "dayjs";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
import "dayjs/locale/es";
import { isNull } from "../../helpers/IsNull";
import { PlanFeatures } from "../Plans/Plans.interface";
import { NotificationsManager } from "../Notifications/Notifications";
import WapServerList from "../WapServer/WapServer";
import CompaniesList from "../Companies/Companies";
import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface";
dayjs.locale("es");
dayjs.extend(isSameOrAfter);
class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
planSuscriptions: PlanSuscriptionsAdapterMongoose;
constructor() {
this.planSuscriptions = new PlanSuscriptionsAdapterMongoose();
}
public async getInitPoint(
data: GetSubscriptionInitPointParams
): Promise<GetSuscriptionInitPointResponse> {
const sessionUser = await UsersManager.users.findOne({
_id: data.sessionUser,
});
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const subscription = await this.planSuscriptions.findOne({
sessionUser: data.sessionUser,
});
if (!subscription) {
throw new Error("No se ha encontrado una suscripción activa");
}
const plan = await PlansList.plans.findOne({
_id: String(subscription.planId),
});
if (!plan) {
throw new Error("No se ha encontrado el plan");
}
if (plan.price == 0) {
throw new Error("El plan gratuito no requiere el pago de una suscripción");
}
if (!subscription.mpPreferenceId) {
throw new Error("No se ha encontrado una suscripción activa");
}
if (subscription.mpStatus !== MP_SUBS_STATUS.PENDING) {
throw new Error("No se ha encontrado una suscripción pendiente de pago");
}
if (!subscription.mpInitPoint) {
throw new Error("No se ha encontrado una suscripción pendiente de pago");
}
return {
init_point: subscription.mpInitPoint,
};
}
public async deletePlanSuscriptionByUser(
data: DeletePlanSuscriptionByUserParams
): Promise<void> {
await this.planSuscriptions.delete({ sessionUser: data.sessionUser });
}
public async create(data: CreateMPPreApprovalParams): Promise<MPPreApprovalResponse> {
const sessionUser = await UsersManager.users.findOne({
_id: data.sessionUser,
});
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const plan = await PlansList.plans.findOne({ _id: data.planId });
if (!plan) {
throw new Error("El plan no existe");
}
if (!plan.active) {
throw new Error("El plan no esta activo");
}
let subscription = await this.planSuscriptions.findOne({
sessionUser: data.sessionUser,
});
let isNewSubscription = false;
if (!subscription) {
const subscriptionStartDate = dayjs(new Date());
if (plan.price === 0) {
isNewSubscription = true;
subscription = await this.planSuscriptions.create({
userId: data.sessionUser,
planId: data.planId,
mpPlanId: data.planId,
startDate: subscriptionStartDate.toDate(),
endDate: subscriptionStartDate.clone().add(data.months || 1, "months").toDate(),
autoRenew: true,
mpInitPoint: "/landing/dashboard",
});
} else if (plan.price > 0) {
isNewSubscription = true;
subscription = await this.toMP({
planId: data.planId,
sessionUser: data.sessionUser,
months: data.months || 1,
});
}
} else {
const actualPlan = await PlansList.plans.findOne({
_id: subscription.planId,
});
if (!actualPlan) {
throw new Error("El plan no existe");
}
isNewSubscription = true;
const dashboardUrl = "/landing/dashboard";
if (plan.price === 0) {
if (actualPlan.id !== data.planId) {
await this.toFreePlan({ sessionUser: data.sessionUser });
subscription = await this.planSuscriptions.findOne({ sessionUser: data.sessionUser });
}
if (subscription) {
const subscriptionStartDate = dayjs(new Date());
subscription.endDate = subscriptionStartDate.clone().add(data.months || 1, "months").toDate();
subscription.mpInitPoint = dashboardUrl || "";
await subscription.save();
}
} else {
// Actualizamos el plan si actualmente tiene el plan gratuito.
if (actualPlan.id !== data.planId && actualPlan.price === 0) {
//realizar la suscripcion...
subscription = await this.toMP({
planId: data.planId,
sessionUser: data.sessionUser,
months: data.months || 1,
});
} else {
if (subscription.mpStatus != MP_SUBS_STATUS.CANCELLED && actualPlan.id !== data.planId) {
throw new Error(
"No se puede cambiar el plan, ya que el usuario ya tiene una suscripción activa. Para cambiarlo primero debes cancelar tu plan actual."
);
}
subscription = await this.toMP({
planId: data.planId,
sessionUser: data.sessionUser,
months: data.months || 1,
});
}
}
}
if (!subscription) {
throw new Error("No fue posible crear la suscripción");
}
return {
init_point: isNewSubscription
? subscription.mpInitPoint
: isNull<string>(process.env.MP_BACK_URL, ""),
status: MP_PRE_APPROVAL_STATUS.READY_TO_START,
};
}
public async toMP(data: ToMpParams): Promise<IPlanSuscriptionDocument> {
const sessionUser = await UsersManager.users.findOne({
_id: data.sessionUser,
});
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const plan = await PlansList.plans.findOne({ _id: data.planId });
if (!plan) {
throw new Error("El plan no existe");
}
try {
const subscriptionStartDate = dayjs(new Date());
let discount = 0;
if (data.months === 3) discount = plan.discount3Months || 0;
else if (data.months === 6) discount = plan.discount6Months || 0;
else if (data.months === 12) discount = plan.discount12Months || 0;
const totalPrice = plan.price * data.months;
const finalPrice = totalPrice - (totalPrice * (discount / 100));
const response = await axios.post(
"https://api.mercadopago.com/checkout/preferences",
{
items: [
{
title: "TurnosXpress.com.ar - Plan " + plan.name + " (" + data.months + " meses)",
quantity: 1,
currency_id: "ARS",
unit_price: finalPrice
}
],
back_urls: {
success: process.env.MP_BACK_URL,
failure: process.env.MP_BACK_URL,
pending: process.env.MP_BACK_URL
},
auto_return: "approved",
payer: {
email: sessionUser.mpPayerEmail || ""
},
external_reference: String(sessionUser._id),
notification_url: process.env.MP_WEBHOOK_URL
},
{
headers: {
Authorization: `Bearer ${process.env.MP_ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
}
);
const subscriptionData = response.data;
this.planSuscriptions.delete({ sessionUser: data.sessionUser });
const subscription = await this.planSuscriptions.create({
userId: data.sessionUser,
planId: data.planId,
mpPlanId: data.planId,
startDate: subscriptionStartDate.toDate(),
endDate: subscriptionStartDate.clone().add(data.months, "months").toDate(),
autoRenew: false,
mpPayerEmail: sessionUser.mpPayerEmail,
mpStatus: MP_SUBS_STATUS.PENDING,
mpDateCreated: new Date(),
mpInitPoint: subscriptionData.init_point,
mpPreferenceId: subscriptionData.id,
});
await NotificationsManager.sendSystemNotification({
userId: String(data.sessionUser),
subject: "Se ha creado una nueva suscripción",
message: `Se ha creado una nueva suscripción para el plan ${plan.name}.`,
type: NotificationType.BILLING,
code: String(subscription._id)
});
return subscription;
} catch (error) {
console.log("ha ocurrido un error:", error);
throw new Error("No fue posible crear la suscripción en el proveedor de pagos");
}
}
public async cancell(data: CancellPlanSuscriptionParams): Promise<void> {
const sessionUser = await UsersManager.users.findOne({
_id: data.sessionUser,
});
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const subscription = await this.planSuscriptions.findOne({
sessionUser: data.sessionUser,
});
if (!subscription) {
throw new Error("No se ha encontrado una suscripción activa");
}
if (subscription.mpStatus === MP_SUBS_STATUS.CANCELLED) {
throw new Error("La suscripción ya se encuentraba cancelada");
}
try {
if (subscription.mpPreferenceId) {
// Preferences don't need to be explicitly cancelled in MercadoPago
// We just cancel it locally if not paid, or change to free plan if paid.
}
const dateNow = dayjs(new Date()).endOf("day");
const dateEnd = dayjs(subscription.endDate);
if (dateNow.isSameOrAfter(dateEnd)) {
this.toFreePlan({
sessionUser: data.sessionUser,
});
} else {
subscription.mpStatus = MP_SUBS_STATUS.CANCELLED;
subscription.mpInitPoint = "";
subscription.mpPayerEmail = "";
subscription.mpPayerId = -1;
await subscription.save();
}
} catch (error) {
console.log("ha ocurrido un error:", error);
throw new Error("No fue posible crear la suscripción en el proveedor de pagos");
}
}
public async toFreePlan(data: ToFreePlanParams): Promise<void> {
console.log("intentando aplicar el free plan...");
const sessionUser = await UsersManager.users.findOne({
_id: data.sessionUser,
});
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const subscription = await this.planSuscriptions.findOne({
sessionUser: data.sessionUser,
});
if (!subscription) {
throw new Error("No se ha encontrado una suscripción activa");
}
const freePlan = await PlansList.plans.findOne({ price: 0 });
if (!freePlan || !freePlan._id) {
throw new Error("No se ha encontrado el plan gratuito");
}
if (String(subscription.planId) == String(freePlan._id)) {
//console.log("El usuario ya tiene el plan gratuito");
return;
}
const company = await CompaniesList.companies.findOne({
ownerId: data.sessionUser,
});
if (company) {
//Intentamos eliminar el bot de el usuario.
try {
//console.log("intentando borrar el bot.");
await WapServerList.deleteBot({
companyId: String(company._id),
sessionUser: data.sessionUser,
});
} catch (error) {
console.log("ha ocurrido un error al intentar eliminar el bot:", error);
}
}
//console.log("guardando el plan gratuito...");
//try {
console.log("cambiando al free plan:", freePlan.id);
subscription.planId = freePlan.id;
subscription.mpStatus = "";
subscription.isActive = true;
subscription.mpInitPoint = "";
subscription.mpPreferenceId = "";
await subscription.save();
// console.log("plan gratuito guardado:", freePlan.id);
//} catch (error) {
// console.log("no se pudo guardar el plan gratuito.");
//}
await NotificationsManager.sendSystemNotification({
userId: String(data.sessionUser),
subject: "Bienvenido al plan gratuito",
message: `El plan gratuito ha sido asignado a su usuario. Cuando lo desee puede actualizar su plan para acceder más funciones.`,
type: NotificationType.BILLING,
code: String(subscription._id)
});
const susInfo: ISubscriptionInfo = {
id: subscription.id,
plan: freePlan,
startDate: subscription.startDate,
endDate: subscription.endDate,
isActive: subscription.isActive,
autoRenew: subscription.autoRenew,
mpStatus: subscription.mpStatus,
mpDateCreated: subscription.mpDateCreated,
};
io.to(`user:${String(subscription.userId)}`).emit("suscription_status_updated", susInfo);
}
public async findOne(
data: FindPlanSuscripcionsParams
): Promise<IPlanSuscriptionDocument | null> {
const subscription = await this.planSuscriptions.findOne(data);
//console.log("findOne", subscription);
if (!subscription) {
return null;
}
const dateNow = dayjs(new Date()).endOf("day");
const dateEnd = dayjs(subscription.endDate);
if (dateNow.isSameOrAfter(dateEnd)) {
if (subscription.mpStatus === MP_SUBS_STATUS.CANCELLED) {
this.toFreePlan({
sessionUser: data.sessionUser,
});
}
}
return await this.planSuscriptions.findOne(data);
}
public async getSubscriptionByUser(
data: FindPlanSuscripcionsParams
): Promise<ISubscriptionInfo | null> {
const subscription = await this.planSuscriptions.findOne(data);
if (!subscription) {
return null;
}
const dateNow = dayjs(new Date()).endOf("day");
const dateEnd = dayjs(subscription.endDate);
if (dateNow.isSameOrAfter(dateEnd)) {
//Esta condicion era la culpable de que todo el tiempo se queria aplicar el free plan.
//Ya que la fecha de finalizacion queda guardada en la base de datos a pesar de que ya no se use.
if (
subscription.mpStatus !== MP_SUBS_STATUS.AUTHORIZED &&
subscription.mpStatus !== ""
) {
this.toFreePlan({
sessionUser: data.sessionUser,
});
}
}
const plan = await PlansList.plans.findOne({
_id: subscription.planId,
});
if (!plan) {
throw new Error("No se ha encontrado el plan");
}
return {
id: subscription.id,
plan: plan,
startDate: subscription.startDate,
endDate: subscription.endDate,
isActive: subscription.isActive,
autoRenew: subscription.autoRenew,
mpStatus: subscription.mpStatus,
mpDateCreated: subscription.mpDateCreated,
};
}
public async checkFeature(data: CheckFeatureParams): Promise<boolean> {
const subscriptionInfo = await this.getSubscriptionByUser({
sessionUser: data.userId,
});
if (!subscriptionInfo) {
throw new Error("No se encontro una suscripcion activa");
}
const plan = subscriptionInfo.plan;
if (!plan) {
throw new Error("No se encontro el plan de la suscripcion");
}
switch (data.feature) {
case PlanFeatures.WAP_NOTIFICATIONS:
if (plan.wapNotifications) {
return true;
}
break;
case PlanFeatures.MAIL_NOTIFICATIONS:
if (plan.mailNotifications) {
return true;
}
break;
case PlanFeatures.SMS_NOTIFICATIONS:
if (plan.smsNotifications) {
return true;
}
break;
case PlanFeatures.HAS_BOT:
if (plan.bot) {
return true;
}
break;
case PlanFeatures.PAYMENTS:
if (plan.payments) {
return true;
}
break;
}
return false;
}
public async isSubscriptionActive(data: FindPlanSuscripcionsParams): Promise<boolean> {
const subscriptionInfo = await this.getSubscriptionByUser(data);
if (!subscriptionInfo) {
throw new Error("No se encontro una suscripcion activa");
}
return subscriptionInfo.isActive;
}
}
const PlanSubscriptionsList = new PlanSuscriptionsManager();
export default PlanSubscriptionsList;
@@ -0,0 +1,43 @@
import dayjs from "dayjs";
describe("PlanSubscriptions Logic", () => {
it("should calculate correct endDate for 1, 3, 6, 12 months", () => {
const start = dayjs("2024-01-01T10:00:00.000Z");
const end1 = start.clone().add(1, "months");
expect(end1.format("YYYY-MM-DD")).toBe("2024-02-01");
const end3 = start.clone().add(3, "months");
expect(end3.format("YYYY-MM-DD")).toBe("2024-04-01");
const end6 = start.clone().add(6, "months");
expect(end6.format("YYYY-MM-DD")).toBe("2024-07-01");
const end12 = start.clone().add(12, "months");
expect(end12.format("YYYY-MM-DD")).toBe("2025-01-01");
});
it("should calculate total price correctly with discounts", () => {
const planPrice = 1000;
// 1 month, 0 discount
const total1 = planPrice * 1;
const final1 = total1 - (total1 * (0 / 100));
expect(final1).toBe(1000);
// 3 months, 10% discount
const total3 = planPrice * 3; // 3000
const final3 = total3 - (total3 * (10 / 100)); // 3000 - 300 = 2700
expect(final3).toBe(2700);
// 6 months, 20% discount
const total6 = planPrice * 6; // 6000
const final6 = total6 - (total6 * (20 / 100)); // 6000 - 1200 = 4800
expect(final6).toBe(4800);
// 12 months, 30% discount
const total12 = planPrice * 12; // 12000
const final12 = total12 - (total12 * (30 / 100)); // 12000 - 3600 = 8400
expect(final12).toBe(8400);
});
});
@@ -0,0 +1,48 @@
import { FindPlansParams, IPlansAdapter, IPlan } from "./Plans.interface";
import { Document, Model, Schema, model } from "mongoose";
export interface IPlanDocument extends Omit<IPlan, "id">, Document {}
export class PlansAdapterMongoose implements IPlansAdapter {
schema: Schema;
planList: Model<IPlanDocument>;
constructor() {
this.schema = new Schema({
name: { type: String, required: true, unique: true },
description: { type: String, required: true },
features: { type: Array, required: true, default: [] },
code: { type: String, required: true, unique: true },
price: { type: Number, required: true },
annualPrice: { type: Number, required: true },
limitOrganizations: { type: Number, required: true },
limitEmployees: { type: Number, required: true },
limitServices: { type: Number, required: true },
limitAppointments: { type: Number, required: true },
limitClients: { type: Number, required: true },
limitRepeats: { type: Number, required: true },
mailNotifications: { type: Boolean, required: true },
smsNotifications: { type: Boolean, required: true },
wapNotifications: { type: Boolean, required: true },
payments: { type: Boolean, required: false },
bot: { type: Boolean, required: true },
active: { type: Boolean, required: true },
mpPlanId: { type: String, required: false },
dateLimit: { type: Boolean, required: true },
discount3Months: { type: Number, required: false },
discount6Months: { type: Number, required: false },
discount12Months: { type: Number, required: false },
featured: { type: Boolean, required: false },
});
this.planList = model<IPlanDocument>("Plan", this.schema);
}
public async find(filters: FindPlansParams): Promise<IPlan[]> {
return this.planList.find(filters).exec();
}
public async findOne(filters: FindPlansParams): Promise<IPlanDocument | null> {
return this.planList.findOne(filters).exec();
}
}
@@ -0,0 +1,53 @@
export enum PlanFeatures {
MAIL_NOTIFICATIONS = "mail_notifications",
SMS_NOTIFICATIONS = "sms_notifications",
WAP_NOTIFICATIONS = "wap_notifications",
HAS_BOT = "has_bot",
PAYMENTS = "payments",
}
export type FindPlansParams = {
_id?: string;
code?: string;
price?: number;
};
export interface IPlan {
id?: string;
name: string;
description: string;
features: string[];
code: string;
price: number;
annualPrice: number;
limitOrganizations: number;
limitEmployees: number;
limitServices: number;
limitAppointments: number; //Cantidad de turnos por día.
limitClients: number;
limitRepeats: number;
mailNotifications: boolean;
smsNotifications: boolean;
wapNotifications: boolean;
payments: boolean;
bot: boolean;
active: boolean; //Indica si es posible contratar el plan.
dateLimit: boolean; //Indica se el plan tiene un limite de fecha.
//Por ejemplo, el plan gratuito no tiene un limite de fecha.
mpPlanId?: string;
discount3Months?: number;
discount6Months?: number;
discount12Months?: number;
featured?: boolean;
}
export interface IPlansAdapter {
find(filters: FindPlansParams): Promise<IPlan[]>;
findOne(filters: FindPlansParams): Promise<IPlan | null>;
}
export interface IPlansManager {
plans: IPlansAdapter;
find(data: FindPlansParams): Promise<IPlan[]>;
findOne(data: FindPlansParams): Promise<IPlan>;
}
+66
View File
@@ -0,0 +1,66 @@
import { isNull } from "../../helpers/IsNull";
import { PlansAdapterMongoose } from "./Plans.Adapter.Mongoose";
import { FindPlansParams, IPlan, IPlansManager } from "./Plans.interface";
class PlansManager implements IPlansManager {
plans: PlansAdapterMongoose;
constructor() {
this.plans = new PlansAdapterMongoose();
}
private getOnePlan(plan: IPlan): IPlan {
return {
id: isNull<string>(plan.id, ""),
name: isNull<string>(plan.name, ""),
description: isNull<string>(plan.description, ""),
features: plan.features,
code: isNull<string>(plan.code, ""),
price: isNull<number>(plan.price, 0),
annualPrice: isNull<number>(plan.annualPrice, 0),
limitOrganizations: isNull<number>(plan.limitOrganizations, 1),
limitEmployees: isNull<number>(plan.limitEmployees, 1),
limitServices: isNull<number>(plan.limitServices, 1),
limitAppointments: isNull<number>(plan.limitAppointments, 1),
limitClients: isNull<number>(plan.limitClients, 1),
limitRepeats: isNull<number>(plan.limitRepeats, 1),
mailNotifications: isNull<boolean>(plan.mailNotifications, false),
smsNotifications: isNull<boolean>(plan.smsNotifications, false),
wapNotifications: isNull<boolean>(plan.wapNotifications, false),
bot: isNull<boolean>(plan.bot, false),
active: isNull<boolean>(plan.active, false),
dateLimit: isNull<boolean>(plan.dateLimit, false),
mpPlanId: isNull<string>(plan.mpPlanId, ""),
payments: isNull<boolean>(plan.payments, false),
discount3Months: plan.discount3Months,
discount6Months: plan.discount6Months,
discount12Months: plan.discount12Months,
featured: plan.featured,
};
}
public async find(data: FindPlansParams): Promise<IPlan[]> {
const plans = await this.plans.find(data);
const returnPlans: IPlan[] = [];
for (const plan of plans) {
returnPlans.push(this.getOnePlan(plan));
}
return returnPlans;
}
public async findOne(data: FindPlansParams): Promise<IPlan> {
const plan = await this.plans.findOne(data);
if (!plan) {
throw new Error("No se ha encontrado el plan");
}
return this.getOnePlan(plan);
}
}
const PlansList = new PlansManager();
export default PlansList;
@@ -0,0 +1,143 @@
import { Schema, model, Model, FilterQuery } from "mongoose";
import {
CreatePostParams,
DeletePostParams,
FindPostsParams,
IPost,
IPostAdapter,
IPostDocument,
PaginatePostsParams,
PaginatePostsResults,
UpdatePostParams,
} from "./Posts.Interface";
import { getAuthorizedUsers } from "../../helpers/check";
import CompaniesList from "../Companies/Companies";
export class PostsAdapterMongoose implements IPostAdapter {
schema: Schema;
postList: Model<IPostDocument>;
constructor() {
this.schema = new Schema({
companyId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Company",
},
content: { type: String, required: true },
fixed: { type: Boolean, required: false },
fixedOrder: { type: Number, required: false },
createdAt: { type: Date, default: Date.now, required: true },
});
this.postList = model<IPostDocument>("Post", this.schema);
}
public async create(data: Omit<CreatePostParams, "sessionUser">): Promise<IPost> {
return await this.postList.create(data);
}
public async update(data: Omit<UpdatePostParams, "sessionUser">): Promise<IPost> {
await this.postList
.updateOne(
{ _id: data.postId },
{
content: data.content,
fixed: data.fixed,
fixedOrder: data.fixedOrder,
}
)
.exec();
const updatedPost = await this.postList.findById(data.postId).exec();
if (!updatedPost) {
throw new Error("Error al actualizar el post");
}
return updatedPost;
}
public async delete(data: Omit<DeletePostParams, "sessionUser">): Promise<void> {
await this.postList.deleteOne({ _id: data.id });
}
public async find(filters: FindPostsParams): Promise<IPost[]> {
const searchCriteria = this.buildSearchCriteria(filters);
return await this.postList.find(searchCriteria).exec();
}
public async findOne(filters: FindPostsParams): Promise<IPostDocument | null> {
const searchCriteria = this.buildSearchCriteria(filters);
return await this.postList.findOne(searchCriteria).exec();
}
private buildSearchCriteria(filters: FindPostsParams): FilterQuery<IPost> {
const searchCriteria: FilterQuery<IPost> = {};
if (filters.id) searchCriteria._id = filters.id;
if (filters.companyId) searchCriteria.companyId = filters.companyId;
if (filters.fixed) searchCriteria.fixed = filters.fixed;
return searchCriteria;
}
public async paginate(filters: PaginatePostsParams): Promise<PaginatePostsResults> {
const searchCriteria = this.buildSearchCriteria(filters);
const countUnfixed = await this.postList.countDocuments(searchCriteria).exec();
//const skip = (filters.page - 1) * filters.limit;
//Cargar los ids fijos.
const company = await CompaniesList.companies.findOne({
_id: searchCriteria.companyId,
});
const fixedPostIds = company?.fixedPostIds || [];
//const countFixed = fixedPostIds.length;
//const count = countUnfixed + countFixed;
const fixedPosts: Array<IPost> = [];
for (const postId of fixedPostIds) {
const post = await this.postList.findOne({ _id: postId });
if (post) {
fixedPosts.push(post as IPost);
}
}
//searchCriteria._id = { $nin: fixedPostIds };
const resultsUnfixed = await this.postList
.find(searchCriteria)
//.skip(skip)
//.limit(filters.limit)
.sort({ createdAt: -1 })
.exec();
//quitar de resultsUnfixed los que esten en fixedPostIds
const filteredResultsUnfixed = resultsUnfixed.filter(
(post) => !fixedPostIds.includes(String(post._id))
);
const results: Array<IPost> = [...fixedPosts, ...filteredResultsUnfixed];
//obtengo la porcion de posts correspondiente a la pagina
const startIndex = (filters.page - 1) * filters.limit;
const endIndex = startIndex + filters.limit;
const paginatedResults = results.slice(startIndex, endIndex);
let pages = Math.ceil(countUnfixed / filters.limit);
if (pages == 0) {
pages = 1;
}
let authorizedUsers: string[] = [];
if (searchCriteria.companyId) {
const result = await getAuthorizedUsers({
companyId: String(searchCriteria.companyId),
});
authorizedUsers = result.authorizedUsers;
}
return { data: paginatedResults, page: filters.page, pages: pages, authorizedUsers };
}
}
@@ -0,0 +1,92 @@
import { Document } from "mongoose";
export interface IPost {
id?: string;
_id?: string;
companyId: string;
content: string;
fixed: boolean;
fixedOrder: number;
createdAt: Date;
}
export interface IPostDocument extends Omit<IPost, "id" | "_id">, Document {
_id: string;
}
export interface IPostAdapter {
create(data: Omit<CreatePostParams, "sessionUser">): Promise<IPost>;
update(data: Omit<UpdatePostParams, "sessionUser">): Promise<IPost>;
delete(data: Omit<DeletePostParams, "sessionUser">): Promise<void>;
find(filters: Omit<FindPostsParams, "sessionUser">): Promise<IPost[]>;
findOne(filters: Omit<FindPostsParams, "sessionUser">): Promise<IPostDocument | null>;
paginate(filters: PaginatePostsParams): Promise<PaginatePostsResults>;
}
export type FindPostsParams = {
id?: string;
companyId?: string;
fixed?: boolean;
sessionUser?: string;
};
export type GetPostParams = {
id: string;
};
export type PaginatePostsParams = FindPostsParams & {
page: number;
limit: number;
};
export type PaginatePostsResults = {
data: IPost[];
page: number;
pages: number;
authorizedUsers: string[];
};
export type CreatePostParams = {
companyId: string;
content: string;
sessionUser: string;
};
export type UpdatePostParams = {
postId: string;
companyId: string;
content: string;
fixed?: boolean;
fixedOrder?: number;
sessionUser: string;
};
export type SavePostParams = {
postId?: string;
companyId: string;
content: string;
fixed?: boolean;
fixedOrder?: number;
sessionUser: string;
};
export type DeletePostParams = {
id: string;
companyId: string;
sessionUser: string;
};
export type DeletePostsByCompanyParams = {
companyId: string;
};
export interface IPostsManager {
posts: IPostAdapter;
deletePostsByCompany(data: DeletePostsByCompanyParams): Promise<void>;
createPost(data: CreatePostParams): Promise<IPost>;
updatePost(data: UpdatePostParams): Promise<IPost>;
savePost(data: SavePostParams): Promise<IPost>;
deletePost(data: DeletePostParams): Promise<void>;
paginate(data: PaginatePostsParams): Promise<PaginatePostsResults>;
getPost(data: GetPostParams): Promise<IPost>;
}
+186
View File
@@ -0,0 +1,186 @@
import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check";
import { PostsAdapterMongoose } from "./Posts.Adapter.Mongoose";
import {
CreatePostParams,
DeletePostParams,
DeletePostsByCompanyParams,
GetPostParams,
IPost,
IPostsManager,
PaginatePostsParams,
PaginatePostsResults,
SavePostParams,
UpdatePostParams,
} from "./Posts.Interface";
import sanitizeHtml from "sanitize-html";
export class PostsManager implements IPostsManager {
posts: PostsAdapterMongoose;
constructor() {
this.posts = new PostsAdapterMongoose();
}
public async deletePostsByCompany(data: DeletePostsByCompanyParams): Promise<void> {
if (!data.companyId) {
return;
}
await this.posts.postList.deleteMany({
companyId: data.companyId,
});
}
//sdasd
public async createPost(data: CreatePostParams): Promise<IPost> {
await validateSessionUser({
sessionUser: data.sessionUser,
});
await validatePermissionsByCompany({
companyId: data.companyId,
sessionUser: data.sessionUser,
});
// La lógica de negocio se implementará aquí
const post = this.posts.create(data);
if (!post) {
throw new Error("Error al crear la post");
}
return post;
}
public async updatePost(data: UpdatePostParams): Promise<IPost> {
await validateSessionUser({
sessionUser: data.sessionUser,
});
await validatePermissionsByCompany({
companyId: data.companyId,
sessionUser: data.sessionUser,
});
const checkPost = await this.posts.findOne({
companyId: data.companyId,
id: data.postId,
});
if (!checkPost) {
throw new Error("El post que intenta actualizar no existe");
}
const updatedPost = await this.posts.update(data);
if (!updatedPost) {
throw new Error("Error al actualizar el post");
}
return updatedPost;
}
public async savePost(data: SavePostParams): Promise<IPost> {
const sanitizedContent = sanitizeHtml(data.content, {
allowedTags: [
"p",
"strong",
"u",
"s",
"ol",
"ul",
"li",
"span",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"a",
"img",
"br",
"div",
"iframe",
],
allowedAttributes: {
a: ["href", "title", "rel", "target", "style"],
img: ["src", "alt", "title", "width", "height", "style"],
span: ["style", "class", "contenteditable"],
li: ["data-list"],
iframe: ["frameborder", "allowfullscreen", "src"],
"*": ["style", "class"], // habilita style/class en todos los tags
},
allowedIframeHostnames: ["www.youtube.com"],
allowedSchemes: ["http", "https", "mailto"],
allowedStyles: {
"*": {
color: [/^rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)$/i, /^#[0-9a-f]{3,6}$/i],
"background-color": [
/^rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)$/i,
/^#[0-9a-f]{3,6}$/i,
],
"text-align": [/^left$/, /^right$/, /^center$/, /^justify$/],
},
},
});
if (data.postId) {
return this.updatePost({
postId: data.postId,
companyId: data.companyId,
content: sanitizedContent,
fixed: data.fixed,
fixedOrder: data.fixedOrder,
sessionUser: data.sessionUser,
});
} else {
return this.createPost({
companyId: data.companyId,
content: sanitizedContent,
sessionUser: data.sessionUser,
});
}
}
public async deletePost(data: DeletePostParams): Promise<void> {
await validateSessionUser({
sessionUser: data.sessionUser,
});
const post = await this.posts.findOne({ id: data.id, companyId: data.companyId });
if (!post) {
throw new Error("El post que intenta eliminar no existe");
}
const validation = await validatePermissionsByCompany({
companyId: String(post.companyId),
sessionUser: data.sessionUser,
});
//Desvincular de la organizacion.
const companyCheck = validation.company;
if (!companyCheck) {
throw new Error("No se puede determinar la organización a la que pertenece el post");
}
await this.posts.delete(data);
}
public async paginate(data: PaginatePostsParams): Promise<PaginatePostsResults> {
return this.posts.paginate(data);
}
public async getPost(data: GetPostParams): Promise<IPost> {
const post = await this.posts.findOne({ id: data.id });
if (!post) {
throw new Error("El post que intenta obtener no existe");
}
return post;
}
}
export default new PostsManager();
@@ -0,0 +1,22 @@
import { FindProvinciasParams, IProvinciasAdapter, IProvincia } from "./Provincias.interface";
import { Document, Model, Schema, model } from "mongoose";
export interface IProvinciaDocument extends Omit<IProvincia, "id">, Document {}
export class ProvinciasAdapterMongoose implements IProvinciasAdapter {
schema: Schema;
provinciaList: Model<IProvinciaDocument>;
constructor() {
this.schema = new Schema({
name: { type: String, required: true, unique: true },
code: { type: Number, required: true, default: 0 },
});
this.provinciaList = model<IProvinciaDocument>("Provincia", this.schema);
}
public async find(filters: FindProvinciasParams): Promise<IProvincia[]> {
return await this.provinciaList.find(filters).exec();
}
}
@@ -0,0 +1,22 @@
import { TextObjectFilterResult } from "../../Models/TextObjectFilter.model";
export type FindProvinciasParams = {
_id?: string;
name?: string;
code?: number;
};
export interface IProvincia {
id?: string;
name: string;
code: number;
}
export interface IProvinciasAdapter {
find(filters: FindProvinciasParams): Promise<IProvincia[]>;
}
export interface IProvinciasManager {
provincias: IProvinciasAdapter;
filterAll(data: FindProvinciasParams): Promise<TextObjectFilterResult[]>;
}
@@ -0,0 +1,29 @@
import { TextObjectFilterResult } from "../../Models/TextObjectFilter.model";
import { ProvinciasAdapterMongoose } from "./Provincias.Adapter.Mongoose";
import { FindProvinciasParams, IProvinciasManager } from "./Provincias.interface";
class ProvinciasManager implements IProvinciasManager {
provincias: ProvinciasAdapterMongoose;
constructor() {
this.provincias = new ProvinciasAdapterMongoose();
}
public async filterAll(data: FindProvinciasParams): Promise<TextObjectFilterResult[]> {
const provincias = await this.provincias.find(data);
const returnData: TextObjectFilterResult[] = [];
for (const provincia of provincias) {
returnData.push({
id: provincia.name,
name: provincia.name.toLowerCase(),
});
}
return returnData;
}
}
const ProvinciasList = new ProvinciasManager();
export default ProvinciasList;
@@ -0,0 +1,54 @@
import { Document, Model, Schema, model } from "mongoose";
import {
CreateRepeatsParams,
FindRepeatsParams,
IRepeat,
IRepeatsAdapter,
} from "./Repeats.Interface";
export interface IRepeatDocument extends Omit<IRepeat, "id">, Document {}
export class RepeatsAdapterMongoose implements IRepeatsAdapter {
schema: Schema;
repeatsList: Model<IRepeatDocument>;
constructor() {
this.schema = new Schema({
companyId: { type: Schema.Types.ObjectId, required: true, ref: "Companie" },
employeeId: { type: Schema.Types.ObjectId, required: true, ref: "Employee" },
serviceId: { type: Schema.Types.ObjectId, required: true, ref: "Service" },
clientId: { type: Schema.Types.ObjectId, required: true, ref: "Client" },
discountId: { type: Schema.Types.ObjectId, required: false, ref: "Discount" },
weekDay: { type: Number, required: true },
startHour: { type: Number, required: true },
length: { type: Number, required: true },
disabledDates: { type: Array, required: false },
creationDate: { type: Date, required: true, default: Date.now },
});
this.repeatsList = model<IRepeatDocument>("Repeat", this.schema);
}
public async create(data: CreateRepeatsParams): Promise<IRepeat> {
const { discountId, sessionUser, ...repeatData } = data;
const repeat = await this.repeatsList.create(repeatData);
if (discountId) {
repeat.set("discountId", discountId);
await repeat.save();
}
return repeat;
}
public async delete(id: string): Promise<void> {
await this.repeatsList.deleteOne({ _id: id });
}
public async find(filters: FindRepeatsParams): Promise<IRepeat[]> {
return this.repeatsList.find(filters).exec();
}
public async findOne(filters: FindRepeatsParams): Promise<IRepeatDocument | null> {
return this.repeatsList.findOne(filters).exec();
}
}
@@ -0,0 +1,122 @@
import { DiscountType } from "../Discounts/Discounts.Interface";
import { IRepeatDocument } from "./Repeats.Adapter.Mongoose";
export type FindRepeatsParams = {
_id?: string;
companyId?: string;
employeeId?: string;
serviceId?: string;
clientId?: string;
weekDay?: number;
startHour?: number;
};
export type CreateRepeatsParams = {
companyId: string;
employeeId: string;
serviceId: string;
discountId?: string;
clientId: string;
weekDay: number;
startHour: number;
sessionUser: string;
};
export type DeleteRepeatsParams = {
repeatId: string;
sessionUser: string;
};
export type CountRepeatsParams = {
companyId: string;
employeeId: string;
countDate: Date;
heatMapFraction: number;
};
export type AddRepeatDisabledDateParams = {
repeatId: string;
disableDate: Date;
sessionUser: string;
};
export type DeleteRepeatsByCompanyParams = {
companyId: string;
};
export type DeleteRepeatsByClientParams = {
clientId: string;
};
export type DeleteRepeatsByUserParams = {
userId: string;
};
export type GetPendingRepeatsParams = {
companyId?: string;
employeeId?: string;
sessionUser: string;
filterBySessionUser?: boolean;
};
export interface IRepeat {
id?: string;
companyId: string;
employeeId: string;
serviceId: string;
discountId?: string;
clientId: string;
weekDay: number;
startHour: number;
length: number;
disabledDates?: Date[];
}
export interface RepeatView {
id: string;
weekDay: number;
weekDayName: string;
startHour: number;
startHourFormatted: string;
length: number;
companyId: string;
companyName: string;
employeeId: string;
employeeName: string;
employeeCalendarColor: string;
employeeAvatar: string;
serviceId: string;
serviceName: string;
discountId: string;
discountType: DiscountType;
discountValue: number;
discountName: string;
discountCode: string;
clientId: string;
clientName: string;
}
export interface PendingRepeatView extends RepeatView {
pendingDate: Date;
}
export interface IRepeatsAdapter {
create(data: CreateRepeatsParams): Promise<IRepeat>;
delete(id: string): Promise<void>;
find(filters: FindRepeatsParams): Promise<IRepeat[]>;
findOne(filters: FindRepeatsParams): Promise<IRepeatDocument | null>;
}
export interface IRepeatsManager {
repeats: IRepeatsAdapter;
createRepeat(data: CreateRepeatsParams): Promise<IRepeat>;
deleteRepeat(data: DeleteRepeatsParams): Promise<void>;
deleteRepeatsByCompany(data: DeleteRepeatsByCompanyParams): Promise<void>;
deleteRepeatsByClient(data: DeleteRepeatsByClientParams): Promise<void>;
deleteRepeatsByUser(data: DeleteRepeatsByUserParams): Promise<void>;
getRepeatsByClient(data: FindRepeatsParams): Promise<RepeatView[]>;
countRepeats(data: CountRepeatsParams): Promise<number>;
addDisabledDate(data: AddRepeatDisabledDateParams): Promise<void>;
existDisabledDate(checkDate: Date, disabledDates: Date[]): boolean;
getPendingRepeatsCurrentWeek(data: GetPendingRepeatsParams): Promise<PendingRepeatView[]>;
}
+584
View File
@@ -0,0 +1,584 @@
import UsersManager from "../Users/Users";
import CompaniesManager from "../Companies/Companies";
import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
import { NotificationsManager } from "../Notifications/Notifications";
import { RepeatsAdapterMongoose } from "./Repeats.Adapter.Mongoose";
import {
AddRepeatDisabledDateParams,
CountRepeatsParams,
CreateRepeatsParams,
DeleteRepeatsByClientParams,
DeleteRepeatsByCompanyParams,
DeleteRepeatsByUserParams,
DeleteRepeatsParams,
FindRepeatsParams,
IRepeat,
IRepeatsManager,
RepeatView,
GetPendingRepeatsParams,
PendingRepeatView,
} from "./Repeats.Interface";
import { EmployeeRoles } from "../Employees/Employees.Interface";
import ServiceList from "../Services/Service";
import ClientsList from "../Clients/Clients";
import EmployeesList from "../Employees/Employee";
import UserList from "../Users/Users";
import dayjs from "dayjs";
import localeData from "dayjs/plugin/localeData";
import "dayjs/locale/es";
import weekday from "dayjs/plugin/weekday";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
import { isNull } from "../../helpers/IsNull";
import getAvatar from "../../helpers/getAvatar";
import MetricsList from "../Metrics/Metrics";
import AppointmentList from "../Appointments/Appointments";
import Discounts from "../Discounts/Discounts";
import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface";
dayjs.locale("es");
dayjs.extend(localeData);
dayjs.extend(weekday);
dayjs.extend(isSameOrAfter);
dayjs.extend(isSameOrBefore);
class RepeatsManager implements IRepeatsManager {
repeats: RepeatsAdapterMongoose;
constructor() {
this.repeats = new RepeatsAdapterMongoose();
}
private numberToTime(decimal: number): string {
const hours = Math.floor(decimal);
const minutes = Math.round((decimal - hours) * 60);
const paddedMinutes = minutes.toString().padStart(2, "0");
return `${hours}:${paddedMinutes}hs`;
}
public async createRepeat(data: CreateRepeatsParams): Promise<IRepeat> {
//Primero tengo que verificar que el usuario que intenta registrar un empleado
//tenga permisos para hacerlo y que sea un usuario valido. Este dato viene de
//el campo data.sessionUser.
const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId });
if (!companyCheck) {
throw new Error("La compañia no existe");
}
if (
!(await EmployeesList.checkPermission(
companyCheck,
data.sessionUser,
EmployeeRoles.ADMIN
))
) {
throw new Error(NoPermissionMessage());
}
const canAdd = await MetricsList.canAddRepeat(companyCheck.ownerId);
if (!canAdd) {
throw new Error(
"Ha alcanzado el limite de repeticiones permitidas de acuerdo a su plan."
);
}
const checkEmployee = await EmployeesList.employees.findOne({
_id: data.employeeId,
});
if (!checkEmployee) {
throw new Error("El empleado no existe");
}
const employeeUser = await UsersManager.users.findOne({
_id: checkEmployee.userId,
});
if (!employeeUser) {
throw new Error("El empleado no tiene un usuario asociado");
}
const checkService = await ServiceList.services.findOne({
_id: data.serviceId,
});
if (!checkService) {
throw new Error("El servicio no existe");
}
const checkClient = await ClientsList.clients.findOne({
_id: data.clientId,
});
if (!checkClient) {
throw new Error("El cliente no existe");
}
const checkRepeat = await this.repeats.findOne({
companyId: data.companyId,
clientId: data.clientId,
weekDay: data.weekDay,
startHour: data.startHour,
});
if (checkRepeat) {
throw new Error("Ya existe una reserva previa.");
}
const newRepeat = await this.repeats.create({
...data,
...{ length: checkService.length },
});
await MetricsList.addRepeat({
userId: companyCheck.ownerId,
quantity: 1,
});
const humanDay = dayjs().localeData().weekdays()[data.weekDay];
const humanHour = this.numberToTime(data.startHour);
if (checkClient.userId) {
await NotificationsManager.sendSystemNotification({
userId: String(checkClient.userId),
subject: "Se ha creado una nueva reserva con renovación automática",
message: `La organización ${
companyCheck.name
} ha asignado una reserva con renovación automática todos los ${humanDay} a las ${humanHour} para ${
checkService.name
} con ${UserList.getUserFullName(employeeUser)}.`,
type: NotificationType.SYSTEM
});
}
await NotificationsManager.sendSystemNotification({
userId: String(checkEmployee.userId),
subject: "Se ha creado una nueva reserva con renovación automática",
message: `${ClientsList.getClientFullName(
checkClient
)} tiene una reservación con renovación automática todos los ${humanDay} a las ${humanHour} para ${
checkService.name
} con vos.`,
type: NotificationType.SYSTEM
});
return newRepeat;
}
public async deleteRepeat(data: DeleteRepeatsParams): Promise<void> {
const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const checkRepeat = await this.repeats.findOne({
_id: data.repeatId,
});
if (!checkRepeat) {
throw new Error("La repetición no existe");
}
const companyCheck = await CompaniesManager.companies.findOne({
_id: String(checkRepeat.companyId),
});
if (!companyCheck) {
throw new Error("La compañia no existe");
}
if (
!(await EmployeesList.checkPermission(
companyCheck,
data.sessionUser,
EmployeeRoles.ADMIN
))
) {
throw new Error(NoPermissionMessage());
}
await this.repeats.delete(data.repeatId);
}
public async deleteRepeatsByCompany(data: DeleteRepeatsByCompanyParams): Promise<void> {
await this.repeats.repeatsList.deleteMany({ companyId: data.companyId });
}
public async deleteRepeatsByClient(data: DeleteRepeatsByClientParams): Promise<void> {
await this.repeats.repeatsList.deleteMany({ clientId: data.clientId });
}
public async deleteRepeatsByUser(data: DeleteRepeatsByUserParams): Promise<void> {
//obtengo los clientes asociados al usuario.
const clients = await ClientsList.clients.find({
userId: data.userId,
});
for (const client of clients) {
await this.deleteRepeatsByClient({
clientId: String(client.id),
});
}
}
public async getRepeatsByClient(data: FindRepeatsParams): Promise<RepeatView[]> {
const returnValue: RepeatView[] = [];
const repeats = await this.repeats.find(data);
for (const repeat of repeats) {
if (!repeat.id) {
continue;
}
const companyCheck = await CompaniesManager.companies.findOne({
_id: repeat.companyId,
});
if (!companyCheck) {
continue;
}
const employeeCheck = await EmployeesList.employees.findOne({
_id: repeat.employeeId,
});
if (!employeeCheck) {
continue;
}
const employeeUser = await UsersManager.users.findOne({
_id: employeeCheck.userId,
});
if (!employeeUser) {
continue;
}
const serviceCheck = await ServiceList.services.findOne({
_id: repeat.serviceId,
});
if (!serviceCheck) {
continue;
}
const clientCheck = await ClientsList.clients.findOne({
_id: repeat.clientId,
});
if (!clientCheck) {
continue;
}
const discountData = await Discounts.getDiscountData(
repeat.discountId,
repeat.companyId
);
returnValue.push({
id: repeat.id,
weekDay: repeat.weekDay,
weekDayName: dayjs().localeData().weekdays()[repeat.weekDay],
startHour: repeat.startHour,
startHourFormatted: this.numberToTime(repeat.startHour),
length: repeat.length,
companyId: repeat.companyId,
companyName: companyCheck.name,
employeeId: repeat.employeeId,
employeeName: UserList.getUserFullName(employeeUser),
employeeCalendarColor: isNull<string>(employeeCheck.calendarColor, "#000000"),
employeeAvatar: getAvatar(
employeeCheck.userId,
employeeUser.avatar,
UserList.getUserFullName(employeeUser)
),
serviceId: repeat.serviceId,
serviceName: serviceCheck.name,
discountId: isNull<string>(repeat.discountId, ""),
discountName: discountData.name,
discountType: discountData.type,
discountValue: discountData.value,
discountCode: discountData.code,
clientId: repeat.clientId,
clientName: ClientsList.getClientFullName(clientCheck),
});
}
return returnValue;
}
public async countRepeats(data: CountRepeatsParams): Promise<number> {
let count = 0;
const countDateStart = dayjs(data.countDate);
const countDateEnd = countDateStart.clone().add(data.heatMapFraction, "minutes");
const weekDay = dayjs(data.countDate).weekday() + 1;
const weekStart = dayjs(data.countDate).startOf("week");
const repeats = await this.repeats.find({
companyId: data.companyId,
employeeId: data.employeeId,
weekDay: weekDay,
});
for (const repeat of repeats) {
if (!repeat.id) {
continue;
}
const repeatHour = Math.floor(repeat.startHour);
const repeatMinutes = (repeat.startHour - repeatHour) * 60;
const repeatWeekDay = weekDay - 1;
if (repeatWeekDay < 0) {
continue;
}
const repeatDateStart = weekStart
.add(repeatWeekDay, "d")
.startOf("day")
.hour(repeatHour)
.minute(repeatMinutes)
.second(0)
.millisecond(0);
//Si la fecha esta deshabilitada no tengo que contarla.
if (repeat.disabledDates) {
if (this.existDisabledDate(repeatDateStart.toDate(), repeat.disabledDates)) {
continue;
}
}
//TODO: Si el turno ya esta creado no tengo que contarlo...
const searchDate = repeatDateStart.clone().toDate();
//Solo hago push si la repeticion no tiene el turno creado.
const appointmentCreated = await AppointmentList.Appointments.findOne({
companyId: data.companyId,
clientId: String(repeat.clientId),
serviceId: String(repeat.serviceId),
employeeId: String(repeat.employeeId),
start: searchDate,
});
if (appointmentCreated) {
continue;
}
const repeatDateEnd = repeatDateStart.clone().add(repeat.length, "minutes");
if (
repeatDateStart.isSameOrBefore(countDateStart) &&
repeatDateEnd.isSameOrAfter(countDateEnd)
) {
count++;
}
}
return count;
}
public async getPendingRepeatsCurrentWeek(data: GetPendingRepeatsParams): Promise<PendingRepeatView[]> {
const returnValue: PendingRepeatView[] = [];
const weekStart = dayjs().startOf("week");
const filters: any = {};
if (data.companyId) {
const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId });
if (!companyCheck) {
throw new Error("La compañia no existe");
}
filters.companyId = data.companyId;
}
if (data.filterBySessionUser) {
const employeeQuery: any = { userId: data.sessionUser };
if (data.companyId) {
employeeQuery.companyId = data.companyId;
}
const sessionEmployees = await EmployeesList.employees.find(employeeQuery);
if (sessionEmployees && sessionEmployees.length > 0) {
filters.employeeId = { $in: sessionEmployees.map(e => e.id) };
} else {
// If user is not an employee, they can't have personal repeats
return [];
}
} else if (data.employeeId) {
filters.employeeId = data.employeeId;
}
const repeats = await this.repeats.find(filters);
for (const repeat of repeats) {
if (!repeat.id) {
continue;
}
const repeatHour = Math.floor(repeat.startHour);
const repeatMinutes = Math.round((repeat.startHour - repeatHour) * 60);
// dayjs() .weekday() logic: 0 to 6
// repeat.weekDay is likely 1-7 or 0-6?
// In countRepeats: const weekDay = dayjs(data.countDate).weekday() + 1;
// and repeat.weekDay is checked against it. So repeat.weekDay corresponds to weekday()+1
const repeatWeekDay = repeat.weekDay - 1;
if (repeatWeekDay < 0 || repeatWeekDay > 6) {
continue; // Should not happen if data is correct
}
const repeatDateStart = weekStart
.clone()
.add(repeatWeekDay, "d")
.startOf("day")
.hour(repeatHour)
.minute(repeatMinutes)
.second(0)
.millisecond(0);
// Si la fecha es en el pasado, no la mostramos como pendiente para crear
if (repeatDateStart.isBefore(dayjs().startOf("day"))) {
continue;
}
const searchDate = repeatDateStart.toDate();
// Si la fecha esta deshabilitada no tengo que devolverla
if (repeat.disabledDates && this.existDisabledDate(searchDate, repeat.disabledDates)) {
continue;
}
// Check if appointment is already created
const appointmentCreated = await AppointmentList.Appointments.findOne({
companyId: String(repeat.companyId),
clientId: String(repeat.clientId),
serviceId: String(repeat.serviceId),
employeeId: String(repeat.employeeId),
start: searchDate,
});
if (appointmentCreated) {
continue;
}
// Prepare the view
const employeeCheck = await EmployeesList.employees.findOne({ _id: repeat.employeeId });
if (!employeeCheck) continue;
const employeeUser = await UsersManager.users.findOne({ _id: employeeCheck.userId });
if (!employeeUser) continue;
const serviceCheck = await ServiceList.services.findOne({ _id: repeat.serviceId });
if (!serviceCheck) continue;
const clientCheck = await ClientsList.clients.findOne({ _id: repeat.clientId });
if (!clientCheck) continue;
const discountData = await Discounts.getDiscountData(repeat.discountId, repeat.companyId);
const companyCheck = await CompaniesManager.companies.findOne({ _id: repeat.companyId });
returnValue.push({
id: repeat.id,
weekDay: repeat.weekDay,
weekDayName: dayjs().localeData().weekdays()[repeat.weekDay],
startHour: repeat.startHour,
startHourFormatted: this.numberToTime(repeat.startHour),
length: repeat.length,
companyId: repeat.companyId,
companyName: companyCheck ? companyCheck.name : "",
employeeId: repeat.employeeId,
employeeName: UserList.getUserFullName(employeeUser),
employeeCalendarColor: isNull<string>(employeeCheck.calendarColor, "#000000"),
employeeAvatar: getAvatar(employeeCheck.userId, employeeUser.avatar, UserList.getUserFullName(employeeUser)),
serviceId: repeat.serviceId,
serviceName: serviceCheck.name,
discountId: isNull<string>(repeat.discountId, ""),
discountName: discountData.name,
discountType: discountData.type,
discountValue: discountData.value,
discountCode: discountData.code,
clientId: repeat.clientId,
clientName: ClientsList.getClientFullName(clientCheck),
pendingDate: searchDate
});
}
// Sort by pendingDate
returnValue.sort((a, b) => a.pendingDate.getTime() - b.pendingDate.getTime());
return returnValue;
}
public existDisabledDate(checkDate: Date, disabledDates: Date[]): boolean {
return disabledDates.some((fecha) => fecha.getTime() === checkDate.getTime());
}
public async addDisabledDate(data: AddRepeatDisabledDateParams): Promise<void> {
const repeat = await this.repeats.findOne({
_id: data.repeatId,
});
if (!repeat) {
throw new Error("La repetición no existe");
}
const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
const companyCheck = await CompaniesManager.companies.findOne({
_id: String(repeat.companyId),
});
if (!companyCheck) {
throw new Error("La compañia no existe");
}
if (
!(await EmployeesList.checkPermission(
companyCheck,
data.sessionUser,
EmployeeRoles.ADMIN
))
) {
throw new Error(NoPermissionMessage());
}
if (!repeat.disabledDates) {
repeat.disabledDates = [];
}
if (this.existDisabledDate(data.disableDate, repeat.disabledDates)) {
console.log("La fecha ya estaba deshabilitada");
return;
}
repeat.disabledDates.push(new Date(data.disableDate));
await repeat.save();
}
}
const RepeatsList = new RepeatsManager();
export default RepeatsList;
@@ -0,0 +1,81 @@
import {
FindSchedulesParams,
ISchedulesAdapter,
ISchedule,
CreateScheduleParams,
UpdateScheduleParams,
PaginateSchedulesParams,
PaginateSchedulesResults,
} from "./Schedules.Interface";
import { Document, Model, Schema, model } from "mongoose";
export interface IScheduleDocument extends Omit<ISchedule, "id">, Document {}
export class SchedulesAdapterMongoose implements ISchedulesAdapter {
schema: Schema;
schedulesList: Model<IScheduleDocument>;
constructor() {
this.schema = new Schema({
employeeId: { type: Schema.Types.ObjectId, required: true, ref: "Employee" },
companyId: { type: Schema.Types.ObjectId, required: true, ref: "Companie" },
schedules: { type: Array, required: true },
weekDay: { type: Number, required: true },
creationDate: { type: Date, required: true, default: Date.now },
});
this.schedulesList = model<IScheduleDocument>("Schedule", this.schema);
}
public async create(data: CreateScheduleParams): Promise<ISchedule> {
return await this.schedulesList.create(data);
}
public async update(data: UpdateScheduleParams): Promise<void> {
const updateSchedule = await this.schedulesList.findOne({
companyId: data.companyId,
employeeId: data.employeeId,
weekDay: data.weekDay,
});
if (!updateSchedule) {
throw new Error("Schedule not found");
}
if (data.schedules) {
updateSchedule.schedules = data.schedules;
}
updateSchedule.save();
}
public async delete(id: string): Promise<void> {
await this.schedulesList.deleteOne({ _id: id }).exec();
}
public async find(filters: FindSchedulesParams): Promise<IScheduleDocument[]> {
return this.schedulesList.find(filters).exec();
}
public async findOne(filters: FindSchedulesParams): Promise<IScheduleDocument | null> {
return this.schedulesList.findOne(filters).exec();
}
public async paginate(filters: PaginateSchedulesParams): Promise<PaginateSchedulesResults> {
const { page, limit, ...findFilters } = filters; //Extract page and limit from filters
const count = await this.schedulesList.countDocuments(findFilters).exec();
const skip = (page - 1) * limit;
const results = await this.schedulesList
.find(findFilters)
.skip(skip)
.limit(filters.limit)
.exec();
return {
data: results,
page: filters.page,
pages: Math.ceil(count / filters.limit),
};
}
}

Some files were not shown because too many files have changed in this diff Show More