first commit
This commit is contained in:
+70
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEAGRUvdrR899s3X1s08h+uA0pMsxtLZnBJPy2C/D5kfxU=
|
||||
-----END PUBLIC KEY-----
|
||||
Generated
+8903
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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[]>;
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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[]>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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[]>;
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user