Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 4707-tests_workerTimeControl
gitea/salix/pipeline/head There was a failure building this commit
Details
gitea/salix/pipeline/head There was a failure building this commit
Details
This commit is contained in:
commit
97b0a6b1ae
|
@ -1,6 +1,6 @@
|
||||||
extends: [eslint:recommended, google, plugin:jasmine/recommended]
|
extends: [eslint:recommended, google, plugin:jasmine/recommended]
|
||||||
parserOptions:
|
parserOptions:
|
||||||
ecmaVersion: 2018
|
ecmaVersion: 2020
|
||||||
sourceType: "module"
|
sourceType: "module"
|
||||||
plugins:
|
plugins:
|
||||||
- jasmine
|
- jasmine
|
||||||
|
@ -17,7 +17,7 @@ rules:
|
||||||
camelcase: 0
|
camelcase: 0
|
||||||
default-case: 0
|
default-case: 0
|
||||||
no-eq-null: 0
|
no-eq-null: 0
|
||||||
no-console: ["error"]
|
no-console: ["warn"]
|
||||||
no-warning-comments: 0
|
no-warning-comments: 0
|
||||||
no-empty: [error, allowEmptyCatch: true]
|
no-empty: [error, allowEmptyCatch: true]
|
||||||
complexity: 0
|
complexity: 0
|
||||||
|
@ -35,4 +35,4 @@ rules:
|
||||||
space-in-parens: ["error", "never"]
|
space-in-parens: ["error", "never"]
|
||||||
jasmine/no-focused-tests: 0
|
jasmine/no-focused-tests: 0
|
||||||
jasmine/prefer-toHaveBeenCalledWith: 0
|
jasmine/prefer-toHaveBeenCalledWith: 0
|
||||||
arrow-spacing: ["error", { "before": true, "after": true }]
|
arrow-spacing: ["error", { "before": true, "after": true }]
|
||||||
|
|
|
@ -2,10 +2,11 @@ coverage
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
storage
|
storage
|
||||||
|
.idea
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
.eslintcache
|
.eslintcache
|
||||||
datasources.*.json
|
datasources.*.json
|
||||||
print.*.json
|
print.*.json
|
||||||
db.json
|
db.json
|
||||||
junit.xml
|
junit.xml
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
// Coloque su configuración en este archivo para sobrescribir la configuración predeterminada y de usuario.
|
// Coloque su configuración en este archivo para sobrescribir la configuración predeterminada y de usuario.
|
||||||
{
|
{
|
||||||
// Carácter predeterminado de final de línea.
|
// Carácter predeterminado de final de línea.
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": true
|
||||||
}
|
},
|
||||||
}
|
"search.useIgnoreFiles": false,
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
||||||
|
"eslint.format.enable": true,
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,267 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2342.01] - 2023-10-19
|
||||||
|
|
||||||
|
### Added
|
||||||
|
### Changed
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
## [2340.01] - 2023-10-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Usuarios -> Foto) Se muestra la foto del trabajador
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
### Fixed
|
||||||
|
- (Usuarios -> Historial) Abre el descriptor del usuario correctamente
|
||||||
|
|
||||||
|
## [2338.01] - 2023-09-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Ticket -> Servicios) Se pueden abonar servicios
|
||||||
|
- (Facturas -> Datos básicos) Muestra valores por defecto
|
||||||
|
- (Facturas -> Borrado) Notificación al borrar un asiento ya enlazado en Sage
|
||||||
|
### Changed
|
||||||
|
- (Trabajadores -> Calendario) Icono de check arreglado cuando pulsas un tipo de dia
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
## [2336.01] - 2023-09-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
|
||||||
|
## [2334.01] - 2023-08-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (General -> Errores) Botón para enviar cau con los datos del error
|
||||||
|
|
||||||
|
|
||||||
|
## [2332.01] - 2023-08-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Trabajadores -> Gestión documental) Soporte para Docuware
|
||||||
|
- (General -> Agencia) Soporte para Viaexpress
|
||||||
|
- (Tickets -> SMS) Nueva sección en Lilium
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- (General -> Tickets) Devuelve el motivo por el cual no es editable
|
||||||
|
- (Desplegables -> Trabajadores) Mejorados
|
||||||
|
- (General -> Clientes) Razón social y dirección en mayúsculas
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- (Clientes -> SMS) Al pasar el ratón por encima muestra el mensaje completo
|
||||||
|
|
||||||
|
|
||||||
|
## [2330.01] - 2023-07-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Artículos -> Vista Previa) Añadido campo "Plástico reciclado"
|
||||||
|
- (Rutas -> Troncales) Nueva sección
|
||||||
|
- (Tickets -> Opciones) Opción establecer peso
|
||||||
|
- (Clientes -> SMS) Nueva sección
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- (General -> Iconos) Añadidos nuevos iconos
|
||||||
|
- (Clientes -> Razón social) Permite crear clientes con la misma razón social según el país
|
||||||
|
|
||||||
|
|
||||||
|
## [2328.01] - 2023-07-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Clientes -> Morosos) Añadida columna "es trabajador"
|
||||||
|
- (Trabajadores -> Departamentos) Nueva sección
|
||||||
|
- (Trabajadores -> Departamentos) Añadido listado de Trabajadores por departamento
|
||||||
|
- (Trabajadores -> Departamentos) Añadido características de departamento e información
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- (Trabajadores -> Departamentos) Arreglado búscador
|
||||||
|
|
||||||
|
|
||||||
|
## [2326.01] - 2023-06-29
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Entradas -> Correo) Al cambiar el tipo de cambio enviará un correo a las personas designadas
|
||||||
|
- (General -> Históricos) Botón para ver el estado del registro en cada punto
|
||||||
|
- (General -> Históricos) Al filtar por registro se muestra todo el histórial desde que fue creado
|
||||||
|
- (Tickets -> Índice) Permite enviar varios albaranes a Docuware
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- (General -> Históricos) Los registros se muestran agrupados por usuario y entidad
|
||||||
|
- (Facturas -> Facturación global) Optimizada, generación de PDFs y notificaciones en paralelo
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- (General -> Históricos) Duplicidades eliminadas
|
||||||
|
- (Facturas -> Facturación global) Solucionados fallos que paran el proceso
|
||||||
|
|
||||||
|
## [2324.01] - 2023-06-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Tickets -> Abono) Al abonar permite crear el ticket abono con almacén o sin almmacén
|
||||||
|
- (General -> Desplegables) Mejorada eficiencia de carga de datos
|
||||||
|
- (General -> Históricos) Ahora, ademas de los ids, se muestra la descripión de los atributos
|
||||||
|
- (General -> Históricos) Botón para hacer más ágil mostrar sólo los cambios en un registro
|
||||||
|
- (General -> Históricos) Filtro por cambios
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- (General -> Permisos) Mejorada seguridad
|
||||||
|
- (General -> Históricos) Elementos de la interfaz reorganizados para hacerla más ágil e intuitiva
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
-
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2322.01] - 2023-06-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Tickets -> Crear Factura) Al facturar se envia automáticamente el pdf al cliente
|
||||||
|
- (Artículos -> Histórico) Filtro para mostrar lo anterior al inventario
|
||||||
|
- (Trabajadores -> Nuevo trabajador) Permite elegir el método de pago
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- (Trabajadores -> Nuevo trabajador) Los clientes se crean sin 'TR' pero se añade tipo de negocio 'Trabajador'
|
||||||
|
- (Tickets -> Expediciones) Interfaz mejorada y contador añadido
|
||||||
|
### Fixed
|
||||||
|
- (Tickets -> Líneas) Se permite hacer split de líneas al mismo ticket
|
||||||
|
- (Tickets -> Cambiar estado) Ahora muestra la lista completa de todos los estados
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2320.01] - 2023-05-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Tickets -> Crear Factura) Al facturar se envia automáticamente el pdf al cliente
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- (Trabajadores -> Nuevo trabajador) Los clientes se crean sin 'TR' pero se añade tipo de negocio 'Trabajador'
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
-
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2318.01] - 2023-05-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Usuarios -> Histórico) Nueva sección
|
||||||
|
- (Roles -> Histórico) Nueva sección
|
||||||
|
- (Trabajadores -> Dar de alta) Permite elegir el método de pago
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- (Artículo -> Precio fijado) Modificado el buscador superior por uno lateral
|
||||||
|
- (Trabajadores -> Dar de alta) Quitada obligatoriedad del iban
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- (Ticket -> Boxing) Arreglado selección de horas
|
||||||
|
- (Cesta -> Índice) Optimizada búsqueda
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2314.01] - 2023-04-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Clientes -> Morosos) Ahora se puede filtrar por las columnas "Desde" y "Fecha Ú. O.". También se envia un email al comercial cuando se añade una nota.
|
||||||
|
- (Monitor tickets) Muestra un icono al lado de la zona, si el ticket es frágil y se envía por agencia
|
||||||
|
- (Facturas recibidas -> Bases negativas) Nueva sección
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- (Clientes -> Morosos) Ahora se mantienen los elementos seleccionados al hacer sroll.
|
||||||
|
|
||||||
|
## [2312.01] - 2023-04-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Monitor tickets) Muestra un icono al lado de la zona, si el ticket es frágil y se envía por agencia
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- (Monitor tickets) Cuando se filtra por 'Pendiente' ya no muestra los estados de 'Previa'
|
||||||
|
- (Envíos -> Extra comunitarios) Se agrupan las entradas del mismo travel. Añadidos campos Referencia y Importe.
|
||||||
|
- (Envíos -> Índice) Cambiado el buscador superior por uno lateral
|
||||||
|
|
||||||
|
## [2310.01] - 2023-03-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Trabajadores -> Control de horario) Ahora se puede confirmar/no confirmar el registro horario de cada semana desde esta sección
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- (Clientes -> Listado extendido) Resuelto error al filtrar por clientes inactivos desde la columna "Activo"
|
||||||
|
- (General) Al pasar el ratón por encima del icono de "Borrar" en un campo, se hacía más grande afectando a la interfaz
|
||||||
|
|
||||||
|
## [2308.01] - 2023-03-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Proveedores -> Datos fiscales) Añadido checkbox 'Vies'
|
||||||
|
- (Client -> Descriptor) Nuevo icono $ con barrotes para los clientes con impago
|
||||||
|
- (Trabajador -> Datos Básicos) Añadido nuevo campo Taquilla
|
||||||
|
- (Trabajador -> PDA) Nueva sección
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- (Ticket -> Borrar ticket) Restringido el borrado de tickets con abono
|
||||||
|
|
||||||
|
## [2306.01] - 2023-02-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Tickets -> Datos Básicos) Mensaje de confirmación al intentar generar tickets con negativos
|
||||||
|
- (Artículos) El visible y disponible se calcula a partir de un almacén diferente dependiendo de la sección en la que te encuentres. Se ha añadido un icono que informa sobre a partir de que almacén se esta calculando.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- (General -> Inicio) Ahora permite recuperar la contraseña tanto con el correo de recuperación como el usuario
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- (Monitor de tickets) Cuando ordenas por columna, ya no se queda deshabilitado el botón de 'Actualizar'
|
||||||
|
- (Zone -> Días de entrega) Al hacer click en un día, muestra correctamente las zonas
|
||||||
|
- (Artículos) El disponible en la vista previa se muestra correctamente
|
||||||
|
|
||||||
|
## [2304.01] - 2023-02-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (Rutas) Al descargar varias facturas se comprime en un zip
|
||||||
|
- (Trabajadores -> Nuevo trabajador) Nueva sección
|
||||||
|
- (Tickets -> Adelantar tickets) Añadidos campos "líneas" y "litros" al ticket origen
|
||||||
|
- (Tickets -> Adelantar tickets) Nuevo icono muestra cuando las agencias de los tickets origen/destino son distintas
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- (Entradas -> Compras) Cambiados los campos "Precio Grouping/Packing" por "PVP" y "Precio" por "Coste"
|
||||||
|
- (Artículos -> Últimas entradas) Cambiados los campos "P.P.U." y "P.P.P." por "PVP"
|
||||||
|
- (Rutas -> Sumario/Tickets) Actualizados campos de los tickets
|
||||||
|
- (Proveedores -> Crear/Editar) Permite añadir Proveedores con la misma razón social pero con países distintos
|
||||||
|
- (Tickets -> Adelantar tickets) Cambiados selectores de estado por checks "Pendiente origen/destino"
|
||||||
|
- (Tickets -> Adelantar tickets) Cambiado stock de destino a origen.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- (Artículos -> Etiquetas) Permite intercambiar la relevancia entre dos etiquetas.
|
||||||
|
- (Cliente -> Datos Fiscales) No se permite seleccionar 'Notificar vía e-mail' a los clientes sin e-mail
|
||||||
|
- (Tickets -> Datos básicos) Permite guardar la hora de envío
|
||||||
|
- (Tickets -> Añadir pago) Eliminado "null" en las referencias
|
||||||
|
- (Tickets -> Adelantar tickets) Permite ordenar por importe
|
||||||
|
- (Tickets -> Adelantar tickets) El filtrado por encajado muestra también los tickets sin tipo de encajado
|
||||||
|
|
||||||
|
## [2302.01] - 2023-01-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- (General -> Inicio) Permite recuperar la contraseña
|
||||||
|
- (Tickets -> Opciones) Subir albarán a Docuware
|
||||||
|
- (Tickets -> Opciones) Enviar correo con PDF de Docuware
|
||||||
|
- (Artículos -> Datos Básicos) Añadido campo Unidades/Caja
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- (Reclamaciones -> Descriptor) Cambiado el campo Agencia por Zona
|
||||||
|
- (Tickets -> Líneas preparadas) Actualizada sección para que sea más visual
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- (General) Al utilizar el traductor de Google se descuadraban los iconos
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- (Tickets -> Control clientes) Eliminada sección
|
40
Dockerfile
40
Dockerfile
|
@ -1,32 +1,44 @@
|
||||||
FROM debian:stretch-slim
|
FROM debian:bullseye-slim
|
||||||
ENV TZ Europe/Madrid
|
ENV TZ Europe/Madrid
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# NodeJs
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
gnupg2 \
|
gnupg2 \
|
||||||
libfontconfig lftp \
|
graphicsmagick \
|
||||||
&& apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \
|
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
|
&& npm install -g npm@9.6.6
|
||||||
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \
|
|
||||||
libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget \
|
# Puppeteer
|
||||||
&& curl -sL https://deb.nodesource.com/setup_12.x | bash - \
|
|
||||||
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
nodejs \
|
libfontconfig lftp xvfb gconf-service libasound2 libatk1.0-0 libc6 \
|
||||||
&& apt-get purge -y --auto-remove \
|
libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 \
|
||||||
gnupg2 \
|
libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 \
|
||||||
|
libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \
|
||||||
|
libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 \
|
||||||
|
libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
|
||||||
|
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& npm -g install pm2
|
&& npm -g install pm2
|
||||||
|
|
||||||
|
# Salix
|
||||||
|
|
||||||
WORKDIR /salix
|
WORKDIR /salix
|
||||||
|
|
||||||
|
COPY print/package.json print/package-lock.json print/
|
||||||
|
RUN npm --prefix ./print install --omit=dev ./print
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
COPY loopback/package.json loopback/
|
COPY loopback/package.json loopback/
|
||||||
COPY print/package.json print/
|
RUN npm install --omit=dev
|
||||||
RUN npm install --only=prod
|
|
||||||
RUN npm --prefix ./print install --only=prod ./print
|
|
||||||
|
|
||||||
COPY loopback loopback
|
COPY loopback loopback
|
||||||
COPY back back
|
COPY back back
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#!/usr/bin/env groovy
|
#!/usr/bin/env groovy
|
||||||
|
|
||||||
pipeline {
|
pipeline {
|
||||||
agent any
|
agent any
|
||||||
options {
|
options {
|
||||||
|
@ -40,7 +39,7 @@ pipeline {
|
||||||
NODE_ENV = ""
|
NODE_ENV = ""
|
||||||
}
|
}
|
||||||
steps {
|
steps {
|
||||||
nodejs('node-v14') {
|
nodejs('node-v20') {
|
||||||
sh 'npm install --no-audit --prefer-offline'
|
sh 'npm install --no-audit --prefer-offline'
|
||||||
sh 'gulp install --ci'
|
sh 'gulp install --ci'
|
||||||
}
|
}
|
||||||
|
@ -53,18 +52,19 @@ pipeline {
|
||||||
}}}
|
}}}
|
||||||
environment {
|
environment {
|
||||||
NODE_ENV = ""
|
NODE_ENV = ""
|
||||||
|
TZ = 'Europe/Madrid'
|
||||||
}
|
}
|
||||||
parallel {
|
parallel {
|
||||||
stage('Frontend') {
|
stage('Frontend') {
|
||||||
steps {
|
steps {
|
||||||
nodejs('node-v14') {
|
nodejs('node-v20') {
|
||||||
sh 'jest --ci --reporters=default --reporters=jest-junit --maxWorkers=2'
|
sh 'jest --ci --reporters=default --reporters=jest-junit --maxWorkers=2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stage('Backend') {
|
stage('Backend') {
|
||||||
steps {
|
steps {
|
||||||
nodejs('node-v14') {
|
nodejs('node-v20') {
|
||||||
sh 'npm run test:back:ci'
|
sh 'npm run test:back:ci'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ pipeline {
|
||||||
CREDENTIALS = credentials('docker-registry')
|
CREDENTIALS = credentials('docker-registry')
|
||||||
}
|
}
|
||||||
steps {
|
steps {
|
||||||
nodejs('node-v14') {
|
nodejs('node-v20') {
|
||||||
sh 'gulp build'
|
sh 'gulp build'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ Salix is also the scientific name of a beautifull tree! :)
|
||||||
|
|
||||||
Required applications.
|
Required applications.
|
||||||
|
|
||||||
* Node.js = 14.x LTS
|
* Node.js >= 16.x LTS
|
||||||
* Docker
|
* Docker
|
||||||
* Git
|
* Git
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ $ npm run test:e2e
|
||||||
|
|
||||||
Open Visual Studio Code, press Ctrl+P and paste the following commands.
|
Open Visual Studio Code, press Ctrl+P and paste the following commands.
|
||||||
|
|
||||||
In Visual Studio Code we use the ESLint extension.
|
In Visual Studio Code we use the ESLint extension.
|
||||||
```
|
```
|
||||||
ext install dbaeumer.vscode-eslint
|
ext install dbaeumer.vscode-eslint
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
|
|
||||||
module.exports = Self => {
|
|
||||||
Self.remoteMethod('changePassword', {
|
|
||||||
description: 'Changes the user password',
|
|
||||||
accepts: [
|
|
||||||
{
|
|
||||||
arg: 'id',
|
|
||||||
type: 'number',
|
|
||||||
description: 'The user id',
|
|
||||||
http: {source: 'path'}
|
|
||||||
}, {
|
|
||||||
arg: 'oldPassword',
|
|
||||||
type: 'string',
|
|
||||||
description: 'The old password',
|
|
||||||
required: true
|
|
||||||
}, {
|
|
||||||
arg: 'newPassword',
|
|
||||||
type: 'string',
|
|
||||||
description: 'The new password',
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
http: {
|
|
||||||
path: `/:id/changePassword`,
|
|
||||||
verb: 'PATCH'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Self.changePassword = async function(id, oldPassword, newPassword) {
|
|
||||||
await Self.rawSql(`CALL account.user_changePassword(?, ?, ?)`,
|
|
||||||
[id, oldPassword, newPassword]);
|
|
||||||
await Self.app.models.UserAccount.syncById(id, newPassword);
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,70 +0,0 @@
|
||||||
const md5 = require('md5');
|
|
||||||
const UserError = require('vn-loopback/util/user-error');
|
|
||||||
|
|
||||||
module.exports = Self => {
|
|
||||||
Self.remoteMethod('login', {
|
|
||||||
description: 'Login a user with username/email and password',
|
|
||||||
accepts: [
|
|
||||||
{
|
|
||||||
arg: 'user',
|
|
||||||
type: 'String',
|
|
||||||
description: 'The user name or email',
|
|
||||||
required: true
|
|
||||||
}, {
|
|
||||||
arg: 'password',
|
|
||||||
type: 'String',
|
|
||||||
description: 'The password'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
returns: {
|
|
||||||
type: 'object',
|
|
||||||
root: true
|
|
||||||
},
|
|
||||||
http: {
|
|
||||||
path: `/login`,
|
|
||||||
verb: 'POST'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Self.login = async function(user, password) {
|
|
||||||
let $ = Self.app.models;
|
|
||||||
let token;
|
|
||||||
let usesEmail = user.indexOf('@') !== -1;
|
|
||||||
|
|
||||||
let userInfo = usesEmail
|
|
||||||
? {email: user}
|
|
||||||
: {username: user};
|
|
||||||
let instance = await $.User.findOne({
|
|
||||||
fields: ['username', 'password'],
|
|
||||||
where: userInfo
|
|
||||||
});
|
|
||||||
|
|
||||||
let where = usesEmail
|
|
||||||
? {email: user}
|
|
||||||
: {name: user};
|
|
||||||
let account = await Self.findOne({
|
|
||||||
fields: ['active', 'password'],
|
|
||||||
where
|
|
||||||
});
|
|
||||||
|
|
||||||
let validCredentials = instance && (
|
|
||||||
await instance.hasPassword(password) ||
|
|
||||||
account.password == md5(password || '')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (validCredentials) {
|
|
||||||
if (!account.active)
|
|
||||||
throw new UserError('User disabled');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await $.UserAccount.sync(instance.username, password);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let loginInfo = Object.assign({password}, userInfo);
|
|
||||||
token = await $.User.login(loginInfo, 'user');
|
|
||||||
return {token: token.id};
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,58 +0,0 @@
|
||||||
const UserError = require('vn-loopback/util/user-error');
|
|
||||||
|
|
||||||
module.exports = Self => {
|
|
||||||
Self.remoteMethodCtx('privileges', {
|
|
||||||
description: 'Change role and hasGrant if user has privileges',
|
|
||||||
accepts: [
|
|
||||||
{
|
|
||||||
arg: 'id',
|
|
||||||
type: 'number',
|
|
||||||
required: true,
|
|
||||||
description: 'The user id',
|
|
||||||
http: {source: 'path'}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
arg: 'roleFk',
|
|
||||||
type: 'number',
|
|
||||||
description: 'The new role for user',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
arg: 'hasGrant',
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Whether to has grant'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
http: {
|
|
||||||
path: `/:id/privileges`,
|
|
||||||
verb: 'POST'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Self.privileges = async function(ctx, id, roleFk, hasGrant, options) {
|
|
||||||
const models = Self.app.models;
|
|
||||||
const userId = ctx.req.accessToken.userId;
|
|
||||||
|
|
||||||
const myOptions = {};
|
|
||||||
|
|
||||||
if (typeof options == 'object')
|
|
||||||
Object.assign(myOptions, options);
|
|
||||||
|
|
||||||
const user = await models.Account.findById(userId, null, myOptions);
|
|
||||||
|
|
||||||
if (!user.hasGrant)
|
|
||||||
throw new UserError(`You don't have enough privileges`);
|
|
||||||
|
|
||||||
const userToUpdate = await models.Account.findById(id);
|
|
||||||
if (hasGrant != null)
|
|
||||||
return await userToUpdate.updateAttribute('hasGrant', hasGrant, myOptions);
|
|
||||||
if (!roleFk) return;
|
|
||||||
|
|
||||||
const role = await models.Role.findById(roleFk, null, myOptions);
|
|
||||||
const hasRole = await models.Account.hasRole(userId, role.name, myOptions);
|
|
||||||
|
|
||||||
if (!hasRole)
|
|
||||||
throw new UserError(`You don't have enough privileges`);
|
|
||||||
|
|
||||||
await userToUpdate.updateAttribute('roleFk', roleFk, myOptions);
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,12 +0,0 @@
|
||||||
const {models} = require('vn-loopback/server/server');
|
|
||||||
|
|
||||||
describe('account changePassword()', () => {
|
|
||||||
it('should throw an error when old password is wrong', async() => {
|
|
||||||
let err;
|
|
||||||
await models.Account.changePassword(1, 'wrongPassword', 'nightmare.9999')
|
|
||||||
.catch(error => err = error.sqlMessage);
|
|
||||||
|
|
||||||
expect(err).toBeDefined();
|
|
||||||
expect(err).toEqual('Invalid password');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,41 +0,0 @@
|
||||||
const app = require('vn-loopback/server/server');
|
|
||||||
|
|
||||||
describe('account login()', () => {
|
|
||||||
describe('when credentials are correct', () => {
|
|
||||||
it('should return the token', async() => {
|
|
||||||
let login = await app.models.Account.login('salesAssistant', 'nightmare');
|
|
||||||
let accessToken = await app.models.AccessToken.findById(login.token);
|
|
||||||
let ctx = {req: {accessToken: accessToken}};
|
|
||||||
|
|
||||||
expect(login.token).toBeDefined();
|
|
||||||
|
|
||||||
await app.models.Account.logout(ctx);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the token if the user doesnt exist but the client does', async() => {
|
|
||||||
let login = await app.models.Account.login('PetterParker', 'nightmare');
|
|
||||||
let accessToken = await app.models.AccessToken.findById(login.token);
|
|
||||||
let ctx = {req: {accessToken: accessToken}};
|
|
||||||
|
|
||||||
expect(login.token).toBeDefined();
|
|
||||||
|
|
||||||
await app.models.Account.logout(ctx);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when credentials are incorrect', () => {
|
|
||||||
it('should throw a 401 error', async() => {
|
|
||||||
let error;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await app.models.Account.login('IDontExist', 'TotallyWrongPassword');
|
|
||||||
} catch (e) {
|
|
||||||
error = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(error).toBeDefined();
|
|
||||||
expect(error.statusCode).toBe(401);
|
|
||||||
expect(error.code).toBe('LOGIN_FAILED');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,42 +0,0 @@
|
||||||
const app = require('vn-loopback/server/server');
|
|
||||||
|
|
||||||
describe('account logout()', () => {
|
|
||||||
it('should logout and remove token after valid login', async() => {
|
|
||||||
let loginResponse = await app.models.Account.login('buyer', 'nightmare');
|
|
||||||
let accessToken = await app.models.AccessToken.findById(loginResponse.token);
|
|
||||||
let ctx = {req: {accessToken: accessToken}};
|
|
||||||
|
|
||||||
let logoutResponse = await app.models.Account.logout(ctx);
|
|
||||||
let tokenAfterLogout = await app.models.AccessToken.findById(loginResponse.token);
|
|
||||||
|
|
||||||
expect(logoutResponse).toBeTrue();
|
|
||||||
expect(tokenAfterLogout).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a 401 error when token is invalid', async() => {
|
|
||||||
let error;
|
|
||||||
let ctx = {req: {accessToken: {id: 'invalidToken'}}};
|
|
||||||
|
|
||||||
try {
|
|
||||||
response = await app.models.Account.logout(ctx);
|
|
||||||
} catch (e) {
|
|
||||||
error = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(error).toBeDefined();
|
|
||||||
expect(error.statusCode).toBe(401);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error when no token is passed', async() => {
|
|
||||||
let error;
|
|
||||||
let ctx = {req: {accessToken: null}};
|
|
||||||
|
|
||||||
try {
|
|
||||||
response = await app.models.Account.logout(ctx);
|
|
||||||
} catch (e) {
|
|
||||||
error = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(error).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,15 +0,0 @@
|
||||||
const app = require('vn-loopback/server/server');
|
|
||||||
|
|
||||||
describe('account changePassword()', () => {
|
|
||||||
it('should throw an error when password does not meet requirements', async() => {
|
|
||||||
let req = app.models.Account.setPassword(1, 'insecurePass');
|
|
||||||
|
|
||||||
await expectAsync(req).toBeRejected();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update password when it passes requirements', async() => {
|
|
||||||
let req = app.models.Account.setPassword(1, 'Very$ecurePa22.');
|
|
||||||
|
|
||||||
await expectAsync(req).toBeResolved();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -22,15 +22,19 @@ module.exports = Self => {
|
||||||
|
|
||||||
Self.latest = async filter => {
|
Self.latest = async filter => {
|
||||||
const conn = Self.dataSource.connector;
|
const conn = Self.dataSource.connector;
|
||||||
const minDate = new Date();
|
const minDate = Date.vnNew();
|
||||||
minDate.setFullYear(minDate.getFullYear() - 1);
|
minDate.setFullYear(minDate.getFullYear() - 1);
|
||||||
|
|
||||||
const where = {dated: {gte: minDate}};
|
const where = {dated: {gte: minDate}};
|
||||||
filter = mergeFilters(filter, {where});
|
filter = mergeFilters(filter, {where});
|
||||||
|
|
||||||
const stmt = new ParameterizedSQL(
|
const stmt = new ParameterizedSQL(
|
||||||
`SELECT * FROM campaign`);
|
`SELECT * FROM (`);
|
||||||
|
stmt.merge('SELECT * FROM campaign');
|
||||||
stmt.merge(conn.makeWhere(filter.where));
|
stmt.merge(conn.makeWhere(filter.where));
|
||||||
|
stmt.merge('ORDER BY dated ASC');
|
||||||
|
stmt.merge('LIMIT 10000000000000000000');
|
||||||
|
stmt.merge(') sub');
|
||||||
stmt.merge('GROUP BY code');
|
stmt.merge('GROUP BY code');
|
||||||
stmt.merge(conn.makePagination(filter));
|
stmt.merge(conn.makePagination(filter));
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const models = require('vn-loopback/server/server').models;
|
||||||
|
|
||||||
describe('campaign latest()', () => {
|
describe('campaign latest()', () => {
|
||||||
it('should return the campaigns from the last year', async() => {
|
it('should return the campaigns from the last year', async() => {
|
||||||
const now = new Date();
|
const now = Date.vnNew();
|
||||||
const result = await app.models.Campaign.latest();
|
const result = await models.Campaign.latest();
|
||||||
const randomIndex = Math.floor(Math.random() * result.length);
|
const randomIndex = Math.floor(Math.random() * result.length);
|
||||||
const campaignDated = result[randomIndex].dated;
|
const campaignDated = result[randomIndex].dated;
|
||||||
|
|
||||||
|
@ -12,9 +12,9 @@ describe('campaign latest()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the campaigns from the current year', async() => {
|
it('should return the campaigns from the current year', async() => {
|
||||||
const now = new Date();
|
const now = Date.vnNew();
|
||||||
const currentYear = now.getFullYear();
|
const currentYear = now.getFullYear();
|
||||||
const result = await app.models.Campaign.latest({
|
const result = await models.Campaign.latest({
|
||||||
where: {dated: {like: `%${currentYear}%`}}
|
where: {dated: {like: `%${currentYear}%`}}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const {models} = require('vn-loopback/server/server');
|
||||||
|
|
||||||
describe('campaign upcoming()', () => {
|
describe('campaign upcoming()', () => {
|
||||||
it('should return the upcoming campaign but from the last year', async() => {
|
it('should return the upcoming campaign but from the last year', async() => {
|
||||||
const response = await app.models.Campaign.upcoming();
|
const response = await models.Campaign.upcoming();
|
||||||
const campaignDated = response.dated;
|
const campaignDated = response.dated;
|
||||||
const now = new Date();
|
const now = Date.vnNew();
|
||||||
|
|
||||||
expect(campaignDated).toEqual(jasmine.any(Date));
|
expect(campaignDated).toEqual(jasmine.any(Date));
|
||||||
expect(campaignDated).toBeLessThanOrEqual(now);
|
expect(campaignDated).toBeLessThanOrEqual(now);
|
||||||
|
|
|
@ -14,7 +14,7 @@ module.exports = Self => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.upcoming = async() => {
|
Self.upcoming = async() => {
|
||||||
const minDate = new Date();
|
const minDate = Date.vnNew();
|
||||||
minDate.setFullYear(minDate.getFullYear() - 1);
|
minDate.setFullYear(minDate.getFullYear() - 1);
|
||||||
|
|
||||||
return Self.findOne({
|
return Self.findOne({
|
||||||
|
|
|
@ -21,7 +21,7 @@ module.exports = Self => {
|
||||||
|
|
||||||
if (!this.login) return;
|
if (!this.login) return;
|
||||||
|
|
||||||
if (Date.now() > this.login.expires)
|
if (Date.vnNow() > this.login.expires)
|
||||||
this.login = await requestToken();
|
this.login = await requestToken();
|
||||||
|
|
||||||
return this.login;
|
return this.login;
|
||||||
|
@ -48,7 +48,7 @@ module.exports = Self => {
|
||||||
userId: requestData.userId,
|
userId: requestData.userId,
|
||||||
token: requestData.authToken
|
token: requestData.authToken
|
||||||
},
|
},
|
||||||
expires: Date.now() + (1000 * 60 * tokenLifespan)
|
expires: Date.vnNow() + (1000 * 60 * tokenLifespan)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ module.exports = Self => {
|
||||||
let message = $t(`There's a new urgent ticket:`);
|
let message = $t(`There's a new urgent ticket:`);
|
||||||
const ostUri = 'https://cau.verdnatura.es/scp/tickets.php?id=';
|
const ostUri = 'https://cau.verdnatura.es/scp/tickets.php?id=';
|
||||||
tickets.forEach(ticket => {
|
tickets.forEach(ticket => {
|
||||||
message += `\r\n[ID: *${ticket.number}* - ${ticket.subject} (@${ticket.username})](${ostUri + ticket.id})`;
|
message += `\r\n[ID: ${ticket.number} - ${ticket.subject} @${ticket.username}](${ostUri + ticket.id})`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const department = await models.Department.findOne({
|
const department = await models.Department.findOne({
|
||||||
|
@ -42,7 +42,5 @@ module.exports = Self => {
|
||||||
|
|
||||||
if (channelName)
|
if (channelName)
|
||||||
return Self.send(ctx, `#${channelName}`, `@all ➔ ${message}`);
|
return Self.send(ctx, `#${channelName}`, `@all ➔ ${message}`);
|
||||||
|
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,20 +26,27 @@ module.exports = Self => {
|
||||||
Self.send = async(ctx, to, message) => {
|
Self.send = async(ctx, to, message) => {
|
||||||
const models = Self.app.models;
|
const models = Self.app.models;
|
||||||
const accessToken = ctx.req.accessToken;
|
const accessToken = ctx.req.accessToken;
|
||||||
const sender = await models.Account.findById(accessToken.userId);
|
const sender = await models.VnUser.findById(accessToken.userId);
|
||||||
const recipient = to.replace('@', '');
|
const recipient = to.replace('@', '');
|
||||||
|
|
||||||
if (sender.name != recipient) {
|
if (sender.name != recipient) {
|
||||||
await models.Chat.create({
|
const chat = await models.Chat.create({
|
||||||
senderFk: sender.id,
|
senderFk: sender.id,
|
||||||
recipient: to,
|
recipient: to,
|
||||||
dated: new Date(),
|
dated: Date.vnNew(),
|
||||||
checkUserStatus: 0,
|
checkUserStatus: 0,
|
||||||
message: message,
|
message: message,
|
||||||
status: 0,
|
status: 'sending',
|
||||||
attempts: 0
|
attempts: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Self.sendMessage(chat.senderFk, chat.recipient, chat.message);
|
||||||
|
await Self.updateChat(chat, 'sent');
|
||||||
|
} catch (error) {
|
||||||
|
await Self.updateChat(chat, 'error', error);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,18 +24,13 @@ module.exports = Self => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.sendCheckingPresence = async(ctx, recipientId, message, options) => {
|
Self.sendCheckingPresence = async(ctx, recipientId, message) => {
|
||||||
if (!recipientId) return false;
|
if (!recipientId) return false;
|
||||||
|
|
||||||
const myOptions = {};
|
|
||||||
|
|
||||||
if (typeof options == 'object')
|
|
||||||
Object.assign(myOptions, options);
|
|
||||||
|
|
||||||
const models = Self.app.models;
|
const models = Self.app.models;
|
||||||
const userId = ctx.req.accessToken.userId;
|
const userId = ctx.req.accessToken.userId;
|
||||||
const sender = await models.Account.findById(userId);
|
const sender = await models.VnUser.findById(userId, {fields: ['id']});
|
||||||
const recipient = await models.Account.findById(recipientId, null, myOptions);
|
const recipient = await models.VnUser.findById(recipientId, null);
|
||||||
|
|
||||||
// Prevent sending messages to yourself
|
// Prevent sending messages to yourself
|
||||||
if (recipientId == userId) return false;
|
if (recipientId == userId) return false;
|
||||||
|
@ -43,16 +38,26 @@ module.exports = Self => {
|
||||||
if (!recipient)
|
if (!recipient)
|
||||||
throw new Error(`Could not send message "${message}" to worker id ${recipientId} from user ${userId}`);
|
throw new Error(`Could not send message "${message}" to worker id ${recipientId} from user ${userId}`);
|
||||||
|
|
||||||
await models.Chat.create({
|
if (process.env.NODE_ENV == 'test')
|
||||||
|
message = `[Test:Environment to user ${userId}] ` + message;
|
||||||
|
|
||||||
|
const chat = await models.Chat.create({
|
||||||
senderFk: sender.id,
|
senderFk: sender.id,
|
||||||
recipient: `@${recipient.name}`,
|
recipient: `@${recipient.name}`,
|
||||||
dated: new Date(),
|
dated: Date.vnNew(),
|
||||||
checkUserStatus: 1,
|
checkUserStatus: 1,
|
||||||
message: message,
|
message: message,
|
||||||
status: 0,
|
status: 'sending',
|
||||||
attempts: 0
|
attempts: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Self.sendCheckingUserStatus(chat);
|
||||||
|
await Self.updateChat(chat, 'sent');
|
||||||
|
} catch (error) {
|
||||||
|
await Self.updateChat(chat, 'error', error);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,6 @@ module.exports = Self => {
|
||||||
Self.remoteMethodCtx('sendQueued', {
|
Self.remoteMethodCtx('sendQueued', {
|
||||||
description: 'Send a RocketChat message',
|
description: 'Send a RocketChat message',
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
accepts: [],
|
|
||||||
returns: {
|
returns: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
root: true
|
root: true
|
||||||
|
@ -16,14 +15,17 @@ module.exports = Self => {
|
||||||
|
|
||||||
Self.sendQueued = async() => {
|
Self.sendQueued = async() => {
|
||||||
const models = Self.app.models;
|
const models = Self.app.models;
|
||||||
const maxAttempts = 3;
|
|
||||||
const sentStatus = 1;
|
|
||||||
const errorStatus = 2;
|
|
||||||
|
|
||||||
const chats = await models.Chat.find({
|
const chats = await models.Chat.find({
|
||||||
where: {
|
where: {
|
||||||
status: {neq: sentStatus},
|
status: {
|
||||||
attempts: {lt: maxAttempts}
|
nin: [
|
||||||
|
'sent',
|
||||||
|
'sending'
|
||||||
|
]
|
||||||
|
|
||||||
|
},
|
||||||
|
attempts: {lt: 3}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -31,16 +33,16 @@ module.exports = Self => {
|
||||||
if (chat.checkUserStatus) {
|
if (chat.checkUserStatus) {
|
||||||
try {
|
try {
|
||||||
await Self.sendCheckingUserStatus(chat);
|
await Self.sendCheckingUserStatus(chat);
|
||||||
await updateChat(chat, sentStatus);
|
await Self.updateChat(chat, 'sent');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await updateChat(chat, errorStatus, error);
|
await Self.updateChat(chat, 'error', error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
await Self.sendMessage(chat.senderFk, chat.recipient, chat.message);
|
await Self.sendMessage(chat.senderFk, chat.recipient, chat.message);
|
||||||
await updateChat(chat, sentStatus);
|
await Self.updateChat(chat, 'sent');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await updateChat(chat, errorStatus, error);
|
await Self.updateChat(chat, 'error', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +58,7 @@ module.exports = Self => {
|
||||||
const models = Self.app.models;
|
const models = Self.app.models;
|
||||||
|
|
||||||
const recipientName = chat.recipient.slice(1);
|
const recipientName = chat.recipient.slice(1);
|
||||||
const recipient = await models.Account.findOne({
|
const recipient = await models.VnUser.findOne({
|
||||||
where: {
|
where: {
|
||||||
name: recipientName
|
name: recipientName
|
||||||
}
|
}
|
||||||
|
@ -102,7 +104,7 @@ module.exports = Self => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const models = Self.app.models;
|
const models = Self.app.models;
|
||||||
const sender = await models.Account.findById(senderFk);
|
const sender = await models.VnUser.findById(senderFk);
|
||||||
|
|
||||||
const login = await Self.getServiceAuth();
|
const login = await Self.getServiceAuth();
|
||||||
const avatar = `${login.host}/avatar/${sender.name}`;
|
const avatar = `${login.host}/avatar/${sender.name}`;
|
||||||
|
@ -128,15 +130,17 @@ module.exports = Self => {
|
||||||
* @param {object} chat - The chat
|
* @param {object} chat - The chat
|
||||||
* @param {string} status - The new status
|
* @param {string} status - The new status
|
||||||
* @param {string} error - The error
|
* @param {string} error - The error
|
||||||
|
* @param {object} options - Query options
|
||||||
* @return {Promise} - The request promise
|
* @return {Promise} - The request promise
|
||||||
*/
|
*/
|
||||||
async function updateChat(chat, status, error) {
|
|
||||||
|
Self.updateChat = async(chat, status, error) => {
|
||||||
return chat.updateAttributes({
|
return chat.updateAttributes({
|
||||||
status: status,
|
status: status,
|
||||||
attempts: ++chat.attempts,
|
attempts: ++chat.attempts,
|
||||||
error: error
|
error: error
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current user status on Rocketchat
|
* Returns the current user status on Rocketchat
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const {models} = require('vn-loopback/server/server');
|
||||||
|
|
||||||
describe('Chat notifyIssue()', () => {
|
describe('Chat notifyIssue()', () => {
|
||||||
const ctx = {req: {accessToken: {userId: 1}}};
|
const ctx = {req: {accessToken: {userId: 1}}};
|
||||||
ctx.req.__ = value => {
|
ctx.req.__ = value => {
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
const chatModel = app.models.Chat;
|
const chatModel = models.Chat;
|
||||||
const osTicketModel = app.models.OsTicket;
|
const osTicketModel = models.OsTicket;
|
||||||
const departmentId = 31;
|
const departmentId = 31;
|
||||||
|
|
||||||
it(`should not call to the send() method and neither return a response`, async() => {
|
it(`should not call to the send() method and neither return a response`, async() => {
|
||||||
|
@ -27,9 +27,9 @@ describe('Chat notifyIssue()', () => {
|
||||||
subject: 'Issue title'}
|
subject: 'Issue title'}
|
||||||
]);
|
]);
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
const expectedMessage = `@all ➔ There's a new urgent ticket:\r\n[ID: *00001* - Issue title (@batman)](https://cau.verdnatura.es/scp/tickets.php?id=1)`;
|
const expectedMessage = `@all ➔ There's a new urgent ticket:\r\n[ID: 00001 - Issue title @batman](https://cau.verdnatura.es/scp/tickets.php?id=1)`;
|
||||||
|
|
||||||
const department = await app.models.Department.findById(departmentId);
|
const department = await models.Department.findById(departmentId);
|
||||||
let orgChatName = department.chatName;
|
let orgChatName = department.chatName;
|
||||||
await department.updateAttribute('chatName', 'IT');
|
await department.updateAttribute('chatName', 'IT');
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const {models} = require('vn-loopback/server/server');
|
||||||
|
|
||||||
describe('Chat send()', () => {
|
describe('Chat send()', () => {
|
||||||
it('should return true as response', async() => {
|
it('should return true as response', async() => {
|
||||||
let ctx = {req: {accessToken: {userId: 1}}};
|
let ctx = {req: {accessToken: {userId: 1}}};
|
||||||
let response = await app.models.Chat.send(ctx, '@salesPerson', 'I changed something');
|
let response = await models.Chat.send(ctx, '@salesPerson', 'I changed something');
|
||||||
|
|
||||||
expect(response).toEqual(true);
|
expect(response).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false as response', async() => {
|
it('should return false as response', async() => {
|
||||||
let ctx = {req: {accessToken: {userId: 18}}};
|
let ctx = {req: {accessToken: {userId: 18}}};
|
||||||
let response = await app.models.Chat.send(ctx, '@salesPerson', 'I changed something');
|
let response = await models.Chat.send(ctx, '@salesPerson', 'I changed something');
|
||||||
|
|
||||||
expect(response).toEqual(false);
|
expect(response).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const models = require('vn-loopback/server/server').models;
|
const models = require('vn-loopback/server/server').models;
|
||||||
|
|
||||||
describe('Chat sendCheckingPresence()', () => {
|
describe('Chat sendCheckingPresence()', () => {
|
||||||
const today = new Date();
|
const today = Date.vnNew();
|
||||||
today.setHours(6, 0);
|
today.setHours(6, 0);
|
||||||
const chatModel = models.Chat;
|
const chatModel = models.Chat;
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ describe('Chat sendCheckingPresence()', () => {
|
||||||
|
|
||||||
const chat = {
|
const chat = {
|
||||||
checkUserStatus: 1,
|
checkUserStatus: 1,
|
||||||
status: 0,
|
status: 'pending',
|
||||||
attempts: 0
|
attempts: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ describe('Chat sendCheckingPresence()', () => {
|
||||||
|
|
||||||
const chat = {
|
const chat = {
|
||||||
checkUserStatus: 0,
|
checkUserStatus: 0,
|
||||||
status: 0,
|
status: 'pending',
|
||||||
attempts: 0
|
attempts: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethodCtx('getTickets', {
|
||||||
|
description: 'Make a new collection of tickets',
|
||||||
|
accessType: 'WRITE',
|
||||||
|
accepts: [{
|
||||||
|
arg: 'id',
|
||||||
|
type: 'number',
|
||||||
|
description: 'The collection id',
|
||||||
|
required: true,
|
||||||
|
http: {source: 'path'}
|
||||||
|
}, {
|
||||||
|
arg: 'print',
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'True if you want to print'
|
||||||
|
}],
|
||||||
|
returns: {
|
||||||
|
type: ['object'],
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/:id/getTickets`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.getTickets = async(ctx, id, print, options) => {
|
||||||
|
const userId = ctx.req.accessToken.userId;
|
||||||
|
const origin = ctx.req.headers.origin;
|
||||||
|
const $t = ctx.req.__;
|
||||||
|
const myOptions = {};
|
||||||
|
|
||||||
|
if (typeof options == 'object')
|
||||||
|
Object.assign(myOptions, options);
|
||||||
|
|
||||||
|
myOptions.userId = userId;
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
const [tickets] = await Self.rawSql(`CALL vn.collection_getTickets(?)`, [id], myOptions);
|
||||||
|
const sales = await Self.rawSql(`
|
||||||
|
SELECT s.ticketFk,
|
||||||
|
sgd.saleGroupFk,
|
||||||
|
s.id saleFk,
|
||||||
|
s.itemFk,
|
||||||
|
i.longName,
|
||||||
|
i.size,
|
||||||
|
ic.color,
|
||||||
|
o.code origin,
|
||||||
|
ish.packing,
|
||||||
|
ish.grouping,
|
||||||
|
s.isAdded,
|
||||||
|
s.originalQuantity,
|
||||||
|
s.quantity saleQuantity,
|
||||||
|
iss.quantity reservedQuantity,
|
||||||
|
SUM(iss.quantity) OVER (PARTITION BY s.id ORDER BY ish.id) accumulatedQuantity,
|
||||||
|
ROW_NUMBER () OVER (PARTITION BY s.id ORDER BY pickingOrder) currentItemShelving,
|
||||||
|
COUNT(*) OVER (PARTITION BY s.id ORDER BY s.id) totalItemShelving,
|
||||||
|
sh.code,
|
||||||
|
IFNULL(p2.code, p.code) parkingCode,
|
||||||
|
IFNULL(p2.pickingOrder, p.pickingOrder) pickingOrder,
|
||||||
|
iss.id itemShelvingSaleFk,
|
||||||
|
iss.isPicked
|
||||||
|
FROM ticketCollection tc
|
||||||
|
LEFT JOIN collection c ON c.id = tc.collectionFk
|
||||||
|
JOIN ticket t ON t.id = tc.ticketFk
|
||||||
|
JOIN sale s ON s.ticketFk = t.id
|
||||||
|
LEFT JOIN saleGroupDetail sgd ON sgd.saleFk = s.id
|
||||||
|
LEFT JOIN saleGroup sg ON sg.id = sgd.saleGroupFk
|
||||||
|
LEFT JOIN parking p2 ON p2.id = sg.parkingFk
|
||||||
|
JOIN item i ON i.id = s.itemFk
|
||||||
|
LEFT JOIN itemShelvingSale iss ON iss.saleFk = s.id
|
||||||
|
LEFT JOIN itemShelving ish ON ish.id = iss.itemShelvingFk
|
||||||
|
LEFT JOIN shelving sh ON sh.code = ish.shelvingFk
|
||||||
|
LEFT JOIN parking p ON p.id = sh.parkingFk
|
||||||
|
LEFT JOIN itemColor ic ON ic.itemFk = s.itemFk
|
||||||
|
LEFT JOIN origin o ON o.id = i.originFk
|
||||||
|
WHERE tc.collectionFk = ?
|
||||||
|
GROUP BY ish.id, p.code, p2.code
|
||||||
|
ORDER BY pickingOrder;`, [id], myOptions);
|
||||||
|
|
||||||
|
if (print)
|
||||||
|
await Self.rawSql(`CALL vn.collection_printSticker(?, ?)`, [id, null], myOptions);
|
||||||
|
|
||||||
|
const collection = {collectionFk: id, tickets: []};
|
||||||
|
if (tickets && tickets.length) {
|
||||||
|
for (const ticket of tickets) {
|
||||||
|
const ticketId = ticket.ticketFk;
|
||||||
|
|
||||||
|
// SEND ROCKET
|
||||||
|
if (ticket.observaciones != '') {
|
||||||
|
for (observation of ticket.observaciones.split(' ')) {
|
||||||
|
if (['#', '@'].includes(observation.charAt(0))) {
|
||||||
|
promises.push(Self.app.models.Chat.send(ctx, observation,
|
||||||
|
$t('The ticket is in preparation', {
|
||||||
|
ticketId: ticketId,
|
||||||
|
ticketUrl: `${origin}/#!/ticket/${ticketId}/summary`,
|
||||||
|
salesPersonId: ticket.salesPersonFk
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SET COLLECTION
|
||||||
|
if (sales && sales.length) {
|
||||||
|
// GET BARCODES
|
||||||
|
const barcodes = await Self.rawSql(`
|
||||||
|
SELECT s.id saleFk, b.code, c.id
|
||||||
|
FROM vn.sale s
|
||||||
|
LEFT JOIN vn.itemBarcode b ON b.itemFk = s.itemFk
|
||||||
|
LEFT JOIN vn.buy c ON c.itemFk = s.itemFk
|
||||||
|
LEFT JOIN vn.entry e ON e.id = c.entryFk
|
||||||
|
LEFT JOIN vn.travel tr ON tr.id = e.travelFk
|
||||||
|
WHERE s.ticketFk = ?
|
||||||
|
AND tr.landed >= util.VN_CURDATE() - INTERVAL 1 YEAR`,
|
||||||
|
[ticketId], myOptions);
|
||||||
|
|
||||||
|
// BINDINGS
|
||||||
|
ticket.sales = [];
|
||||||
|
for (const sale of sales) {
|
||||||
|
if (sale.ticketFk === ticketId) {
|
||||||
|
sale.Barcodes = [];
|
||||||
|
|
||||||
|
if (barcodes && barcodes.length) {
|
||||||
|
for (const barcode of barcodes) {
|
||||||
|
if (barcode.saleFk === sale.saleFk) {
|
||||||
|
for (const prop in barcode) {
|
||||||
|
if (['id', 'code'].includes(prop) && barcode[prop])
|
||||||
|
sale.Barcodes.push(barcode[prop].toString(), '0' + barcode[prop]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ticket.sales.push(sale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collection.tickets.push(ticket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
};
|
||||||
|
};
|
|
@ -30,11 +30,11 @@ module.exports = Self => {
|
||||||
|
|
||||||
Self.newCollection = async(ctx, collectionFk, sectorFk, vWagons) => {
|
Self.newCollection = async(ctx, collectionFk, sectorFk, vWagons) => {
|
||||||
let query = '';
|
let query = '';
|
||||||
|
const userId = ctx.req.accessToken.userId;
|
||||||
|
|
||||||
if (!collectionFk) {
|
if (!collectionFk) {
|
||||||
const userId = ctx.req.accessToken.userId;
|
|
||||||
query = `CALL vn.collectionTrain_newBeta(?,?,?)`;
|
query = `CALL vn.collectionTrain_newBeta(?,?,?)`;
|
||||||
const [result] = await Self.rawSql(query, [sectorFk, vWagons, userId]);
|
const [result] = await Self.rawSql(query, [sectorFk, vWagons, userId], {userId});
|
||||||
if (result.length == 0)
|
if (result.length == 0)
|
||||||
throw new Error(`No collections for today`);
|
throw new Error(`No collections for today`);
|
||||||
|
|
||||||
|
@ -42,16 +42,16 @@ module.exports = Self => {
|
||||||
}
|
}
|
||||||
|
|
||||||
query = `CALL vn.collectionTicket_get(?)`;
|
query = `CALL vn.collectionTicket_get(?)`;
|
||||||
const [tickets] = await Self.rawSql(query, [collectionFk]);
|
const [tickets] = await Self.rawSql(query, [collectionFk], {userId});
|
||||||
|
|
||||||
query = `CALL vn.collectionSale_get(?)`;
|
query = `CALL vn.collectionSale_get(?)`;
|
||||||
const [sales] = await Self.rawSql(query, [collectionFk]);
|
const [sales] = await Self.rawSql(query, [collectionFk], {userId});
|
||||||
|
|
||||||
query = `CALL vn.collectionPlacement_get(?)`;
|
query = `CALL vn.collectionPlacement_get(?)`;
|
||||||
const [placements] = await Self.rawSql(query, [collectionFk]);
|
const [placements] = await Self.rawSql(query, [collectionFk], {userId});
|
||||||
|
|
||||||
query = `CALL vn.collectionSticker_print(?,?)`;
|
query = `CALL vn.collectionSticker_print(?,?)`;
|
||||||
await Self.rawSql(query, [collectionFk, sectorFk]);
|
await Self.rawSql(query, [collectionFk, sectorFk], {userId});
|
||||||
|
|
||||||
return makeCollection(tickets, sales, placements, collectionFk);
|
return makeCollection(tickets, sales, placements, collectionFk);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethodCtx('previousLabel', {
|
||||||
|
description: 'Returns the previa label pdf',
|
||||||
|
accessType: 'READ',
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'id',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
description: 'The item id',
|
||||||
|
http: {source: 'path'}
|
||||||
|
}],
|
||||||
|
returns: [
|
||||||
|
{
|
||||||
|
arg: 'body',
|
||||||
|
type: 'file',
|
||||||
|
root: true
|
||||||
|
}, {
|
||||||
|
arg: 'Content-Type',
|
||||||
|
type: 'String',
|
||||||
|
http: {target: 'header'}
|
||||||
|
}, {
|
||||||
|
arg: 'Content-Disposition',
|
||||||
|
type: 'String',
|
||||||
|
http: {target: 'header'}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
http: {
|
||||||
|
path: '/:id/previousLabel',
|
||||||
|
verb: 'GET'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.previousLabel = (ctx, id) => Self.printReport(ctx, id, 'previa-label');
|
||||||
|
};
|
|
@ -24,13 +24,31 @@ module.exports = Self => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.setSaleQuantity = async(saleId, quantity) => {
|
Self.setSaleQuantity = async(saleId, quantity, options) => {
|
||||||
const models = Self.app.models;
|
const models = Self.app.models;
|
||||||
|
const myOptions = {};
|
||||||
|
let tx;
|
||||||
|
|
||||||
const sale = await models.Sale.findById(saleId);
|
if (typeof options == 'object')
|
||||||
return await sale.updateAttributes({
|
Object.assign(myOptions, options);
|
||||||
originalQuantity: sale.quantity,
|
|
||||||
quantity: quantity
|
if (!myOptions.transaction) {
|
||||||
});
|
tx = await Self.beginTransaction({});
|
||||||
|
myOptions.transaction = tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sale = await models.Sale.findById(saleId, null, myOptions);
|
||||||
|
const saleUpdated = await sale.updateAttributes({
|
||||||
|
quantity
|
||||||
|
}, myOptions);
|
||||||
|
|
||||||
|
if (tx) await tx.commit();
|
||||||
|
|
||||||
|
return saleUpdated;
|
||||||
|
} catch (e) {
|
||||||
|
if (tx) await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const {models} = require('vn-loopback/server/server');
|
||||||
|
|
||||||
describe('getSectors()', () => {
|
describe('getSectors()', () => {
|
||||||
it('return list of sectors', async() => {
|
it('return list of sectors', async() => {
|
||||||
let response = await app.models.Collection.getSectors();
|
let response = await models.Collection.getSectors();
|
||||||
|
|
||||||
expect(response.length).toBeGreaterThan(0);
|
expect(response.length).toBeGreaterThan(0);
|
||||||
expect(response[0].id).toEqual(1);
|
expect(response[0].id).toEqual(1);
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
const models = require('vn-loopback/server/server').models;
|
||||||
|
|
||||||
|
describe('collection getTickets()', () => {
|
||||||
|
let ctx;
|
||||||
|
beforeAll(async() => {
|
||||||
|
ctx = {
|
||||||
|
req: {
|
||||||
|
accessToken: {userId: 9},
|
||||||
|
headers: {origin: 'http://localhost'}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get tickets, sales and barcodes from collection', async() => {
|
||||||
|
const tx = await models.Collection.beginTransaction({});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
const collectionId = 1;
|
||||||
|
|
||||||
|
const collectionTickets = await models.Collection.getTickets(ctx, collectionId, null, options);
|
||||||
|
|
||||||
|
expect(collectionTickets.collectionFk).toEqual(collectionId);
|
||||||
|
expect(collectionTickets.tickets.length).toEqual(3);
|
||||||
|
expect(collectionTickets.tickets[0].ticketFk).toEqual(1);
|
||||||
|
expect(collectionTickets.tickets[1].ticketFk).toEqual(2);
|
||||||
|
expect(collectionTickets.tickets[2].ticketFk).toEqual(23);
|
||||||
|
expect(collectionTickets.tickets[0].sales[0].ticketFk).toEqual(1);
|
||||||
|
expect(collectionTickets.tickets[0].sales[1].ticketFk).toEqual(1);
|
||||||
|
expect(collectionTickets.tickets[0].sales[2].ticketFk).toEqual(1);
|
||||||
|
expect(collectionTickets.tickets[0].sales[0].Barcodes.length).toBeTruthy();
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,10 +1,10 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const {models} = require('vn-loopback/server/server');
|
||||||
|
|
||||||
describe('newCollection()', () => {
|
describe('newCollection()', () => {
|
||||||
it('should return a new collection', async() => {
|
it('should return a new collection', async() => {
|
||||||
pending('#3400 analizar que hacer con rutas de back collection');
|
pending('#3400 analizar que hacer con rutas de back collection');
|
||||||
let ctx = {req: {accessToken: {userId: 1106}}};
|
let ctx = {req: {accessToken: {userId: 1106}}};
|
||||||
let response = await app.models.Collection.newCollection(ctx, 1, 1, 1);
|
let response = await models.Collection.newCollection(ctx, 1, 1, 1);
|
||||||
|
|
||||||
expect(response.length).toBeGreaterThan(0);
|
expect(response.length).toBeGreaterThan(0);
|
||||||
expect(response[0].ticketFk).toEqual(2);
|
expect(response[0].ticketFk).toEqual(2);
|
||||||
|
|
|
@ -1,16 +1,42 @@
|
||||||
const models = require('vn-loopback/server/server').models;
|
const models = require('vn-loopback/server/server').models;
|
||||||
|
const LoopBackContext = require('loopback-context');
|
||||||
|
|
||||||
describe('setSaleQuantity()', () => {
|
describe('setSaleQuantity()', () => {
|
||||||
|
beforeAll(async() => {
|
||||||
|
const activeCtx = {
|
||||||
|
accessToken: {userId: 9},
|
||||||
|
http: {
|
||||||
|
req: {
|
||||||
|
headers: {origin: 'http://localhost'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||||
|
active: activeCtx
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should change quantity sale', async() => {
|
it('should change quantity sale', async() => {
|
||||||
const saleId = 30;
|
const tx = await models.Ticket.beginTransaction({});
|
||||||
const newQuantity = 10;
|
|
||||||
|
|
||||||
const originalSale = await models.Sale.findById(saleId);
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
await models.Collection.setSaleQuantity(saleId, newQuantity);
|
const saleId = 30;
|
||||||
const updateSale = await models.Sale.findById(saleId);
|
const newQuantity = 10;
|
||||||
|
|
||||||
expect(updateSale.originalQuantity).toEqual(originalSale.quantity);
|
const originalSale = await models.Sale.findById(saleId, null, options);
|
||||||
expect(updateSale.quantity).toEqual(newQuantity);
|
|
||||||
|
await models.Collection.setSaleQuantity(saleId, newQuantity, options);
|
||||||
|
const updateSale = await models.Sale.findById(saleId, null, options);
|
||||||
|
|
||||||
|
expect(updateSale.quantity).not.toEqual(originalSale.quantity);
|
||||||
|
expect(updateSale.quantity).toEqual(newQuantity);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -32,7 +32,7 @@ module.exports = Self => {
|
||||||
where: {code: 'trash'}
|
where: {code: 'trash'}
|
||||||
}, myOptions);
|
}, myOptions);
|
||||||
|
|
||||||
const date = new Date();
|
const date = Date.vnNew();
|
||||||
date.setMonth(date.getMonth() - 4);
|
date.setMonth(date.getMonth() - 4);
|
||||||
|
|
||||||
const dmsToDelete = await models.Dms.find({
|
const dmsToDelete = await models.Dms.find({
|
||||||
|
@ -47,20 +47,22 @@ module.exports = Self => {
|
||||||
for (let dms of dmsToDelete) {
|
for (let dms of dmsToDelete) {
|
||||||
const pathHash = DmsContainer.getHash(dms.id);
|
const pathHash = DmsContainer.getHash(dms.id);
|
||||||
const dmsContainer = await DmsContainer.container(pathHash);
|
const dmsContainer = await DmsContainer.container(pathHash);
|
||||||
const dstFile = path.join(dmsContainer.client.root, pathHash, dms.file);
|
|
||||||
try {
|
try {
|
||||||
|
const dstFile = path.join(dmsContainer.client.root, pathHash, dms.file);
|
||||||
await fs.unlink(dstFile);
|
await fs.unlink(dstFile);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
continue;
|
if (err.code != 'ENOENT' && dms.file)
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await dms.destroy(myOptions);
|
||||||
|
|
||||||
const dstFolder = path.join(dmsContainer.client.root, pathHash);
|
const dstFolder = path.join(dmsContainer.client.root, pathHash);
|
||||||
try {
|
try {
|
||||||
await fs.rmdir(dstFolder);
|
await fs.rmdir(dstFolder);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await dms.destroy(myOptions);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const {models} = require('vn-loopback/server/server');
|
||||||
|
|
||||||
describe('dms downloadFile()', () => {
|
describe('dms downloadFile()', () => {
|
||||||
let dmsId = 1;
|
let dmsId = 1;
|
||||||
|
@ -6,7 +6,7 @@ describe('dms downloadFile()', () => {
|
||||||
it('should return a response for an employee with text content-type', async() => {
|
it('should return a response for an employee with text content-type', async() => {
|
||||||
let workerId = 1107;
|
let workerId = 1107;
|
||||||
let ctx = {req: {accessToken: {userId: workerId}}};
|
let ctx = {req: {accessToken: {userId: workerId}}};
|
||||||
const result = await app.models.Dms.downloadFile(ctx, dmsId);
|
const result = await models.Dms.downloadFile(ctx, dmsId);
|
||||||
|
|
||||||
expect(result[1]).toEqual('text/plain');
|
expect(result[1]).toEqual('text/plain');
|
||||||
});
|
});
|
||||||
|
@ -16,7 +16,7 @@ describe('dms downloadFile()', () => {
|
||||||
let ctx = {req: {accessToken: {userId: clientId}}};
|
let ctx = {req: {accessToken: {userId: clientId}}};
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
await app.models.Dms.downloadFile(ctx, dmsId).catch(e => {
|
await models.Dms.downloadFile(ctx, dmsId).catch(e => {
|
||||||
error = e;
|
error = e;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
expect(error.message).toEqual(`You don't have enough privileges`);
|
expect(error.message).toEqual(`You don't have enough privileges`);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const {models} = require('vn-loopback/server/server');
|
||||||
|
|
||||||
describe('dms removeFile()', () => {
|
describe('dms removeFile()', () => {
|
||||||
let dmsId = 1;
|
let dmsId = 1;
|
||||||
|
@ -8,7 +8,7 @@ describe('dms removeFile()', () => {
|
||||||
let ctx = {req: {accessToken: {userId: clientId}}};
|
let ctx = {req: {accessToken: {userId: clientId}}};
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
await app.models.Dms.removeFile(ctx, dmsId).catch(e => {
|
await models.Dms.removeFile(ctx, dmsId).catch(e => {
|
||||||
error = e;
|
error = e;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
expect(error.message).toEqual(`You don't have enough privileges`);
|
expect(error.message).toEqual(`You don't have enough privileges`);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const {models} = require('vn-loopback/server/server');
|
||||||
|
|
||||||
describe('dms updateFile()', () => {
|
describe('dms updateFile()', () => {
|
||||||
it(`should return an error for a user without enough privileges`, async() => {
|
it(`should return an error for a user without enough privileges`, async() => {
|
||||||
|
@ -11,7 +11,7 @@ describe('dms updateFile()', () => {
|
||||||
let ctx = {req: {accessToken: {userId: clientId}}, args: {dmsTypeId: dmsTypeId}};
|
let ctx = {req: {accessToken: {userId: clientId}}, args: {dmsTypeId: dmsTypeId}};
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
await app.models.Dms.updateFile(ctx, dmsId, warehouseId, companyId, dmsTypeId).catch(e => {
|
await models.Dms.updateFile(ctx, dmsId, warehouseId, companyId, dmsTypeId).catch(e => {
|
||||||
error = e;
|
error = e;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
expect(error.message).toEqual(`You don't have enough privileges`);
|
expect(error.message).toEqual(`You don't have enough privileges`);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const {models} = require('vn-loopback/server/server');
|
||||||
|
|
||||||
describe('dms uploadFile()', () => {
|
describe('dms uploadFile()', () => {
|
||||||
it(`should return an error for a user without enough privileges`, async() => {
|
it(`should return an error for a user without enough privileges`, async() => {
|
||||||
|
@ -7,7 +7,7 @@ describe('dms uploadFile()', () => {
|
||||||
let ctx = {req: {accessToken: {userId: clientId}}, args: {dmsTypeId: ticketDmsTypeId}};
|
let ctx = {req: {accessToken: {userId: clientId}}, args: {dmsTypeId: ticketDmsTypeId}};
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
await app.models.Dms.uploadFile(ctx).catch(e => {
|
await models.Dms.uploadFile(ctx).catch(e => {
|
||||||
error = e;
|
error = e;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
expect(error.message).toEqual(`You don't have enough privileges`);
|
expect(error.message).toEqual(`You don't have enough privileges`);
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
const got = require('got');
|
|
||||||
|
|
||||||
module.exports = Self => {
|
module.exports = Self => {
|
||||||
Self.remoteMethodCtx('checkFile', {
|
Self.remoteMethod('checkFile', {
|
||||||
description: 'Check if exist docuware file',
|
description: 'Check if exist docuware file',
|
||||||
accessType: 'READ',
|
accessType: 'READ',
|
||||||
accepts: [
|
accepts: [
|
||||||
{
|
{
|
||||||
arg: 'id',
|
arg: 'id',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'The id',
|
description: 'The id',
|
||||||
http: {source: 'path'}
|
http: {source: 'path'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -18,14 +16,18 @@ module.exports = Self => {
|
||||||
description: 'The fileCabinet name'
|
description: 'The fileCabinet name'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
arg: 'dialog',
|
arg: 'filter',
|
||||||
type: 'string',
|
type: 'object',
|
||||||
required: true,
|
description: 'The filter'
|
||||||
description: 'The dialog name'
|
},
|
||||||
}
|
{
|
||||||
|
arg: 'signed',
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'If pdf is necessary to be signed'
|
||||||
|
},
|
||||||
],
|
],
|
||||||
returns: {
|
returns: {
|
||||||
type: 'boolean',
|
type: 'object',
|
||||||
root: true
|
root: true
|
||||||
},
|
},
|
||||||
http: {
|
http: {
|
||||||
|
@ -34,58 +36,45 @@ module.exports = Self => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.checkFile = async function(ctx, id, fileCabinet, dialog) {
|
Self.checkFile = async function(id, fileCabinet, filter, signed) {
|
||||||
const myUserId = ctx.req.accessToken.userId;
|
|
||||||
if (!myUserId)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const models = Self.app.models;
|
const models = Self.app.models;
|
||||||
const docuwareConfig = await models.DocuwareConfig.findOne();
|
const action = 'find';
|
||||||
|
|
||||||
const docuwareInfo = await models.Docuware.findOne({
|
const docuwareInfo = await models.Docuware.findOne({
|
||||||
where: {
|
where: {
|
||||||
code: fileCabinet,
|
code: fileCabinet,
|
||||||
dialogName: dialog
|
action: action
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const docuwareUrl = docuwareConfig.url;
|
if (!filter) {
|
||||||
const cookie = docuwareConfig.token;
|
filter = {
|
||||||
const fileCabinetName = docuwareInfo.fileCabinetName;
|
condition: [
|
||||||
const find = docuwareInfo.find;
|
{
|
||||||
const options = {
|
DBName: docuwareInfo.findById,
|
||||||
'headers': {
|
Value: [id]
|
||||||
'Accept': 'application/json',
|
}
|
||||||
'Content-Type': 'application/json',
|
],
|
||||||
'Cookie': cookie
|
sortOrder: [
|
||||||
}
|
{
|
||||||
};
|
Field: 'FILENAME',
|
||||||
const searchFilter = {
|
Direction: 'Desc'
|
||||||
condition: [
|
}
|
||||||
{
|
]
|
||||||
DBName: find,
|
};
|
||||||
Value: [id]
|
}
|
||||||
}
|
if (signed) {
|
||||||
]
|
filter.condition.push({
|
||||||
};
|
DBName: 'ESTADO',
|
||||||
|
Value: ['Firmado']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// get fileCabinetId
|
const [response] = await Self.get(fileCabinet, filter);
|
||||||
const fileCabinetResponse = await got.get(`${docuwareUrl}/FileCabinets`, options);
|
if (!response) return false;
|
||||||
const fileCabinetJson = JSON.parse(fileCabinetResponse.body).FileCabinet;
|
|
||||||
const fileCabinetId = fileCabinetJson.find(dialogs => dialogs.Name === fileCabinetName).Id;
|
|
||||||
|
|
||||||
// get dialog
|
return {id: response['Document ID']};
|
||||||
const dialogResponse = await got.get(`${docuwareUrl}/FileCabinets/${fileCabinetId}/dialogs`, options);
|
|
||||||
const dialogJson = JSON.parse(dialogResponse.body).Dialog;
|
|
||||||
const dialogId = dialogJson.find(dialogs => dialogs.DisplayName === 'find').Id;
|
|
||||||
|
|
||||||
// get docuwareID
|
|
||||||
Object.assign(options, {'body': JSON.stringify(searchFilter)});
|
|
||||||
const response = await got.post(
|
|
||||||
`${docuwareUrl}/FileCabinets/${fileCabinetId}/Query/DialogExpression?dialogId=${dialogId}`, options);
|
|
||||||
JSON.parse(response.body).Items[0].Id;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
/**
|
||||||
|
* Returns basic headers
|
||||||
|
*
|
||||||
|
* @param {string} cookie - The docuware cookie
|
||||||
|
* @return {object} - The headers
|
||||||
|
*/
|
||||||
|
Self.getOptions = async() => {
|
||||||
|
const docuwareConfig = await Self.app.models.DocuwareConfig.findOne();
|
||||||
|
const headers = {
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cookie': docuwareConfig.cookie
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: docuwareConfig.url,
|
||||||
|
headers
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the dialog id
|
||||||
|
*
|
||||||
|
* @param {string} code - The fileCabinet name
|
||||||
|
* @param {string} action - The fileCabinet name
|
||||||
|
* @param {string} fileCabinetId - Optional The fileCabinet name
|
||||||
|
* @return {number} - The fileCabinet id
|
||||||
|
*/
|
||||||
|
Self.getDialog = async(code, action, fileCabinetId) => {
|
||||||
|
if (!process.env.NODE_ENV)
|
||||||
|
return Math.floor(Math.random() + 100);
|
||||||
|
|
||||||
|
const docuwareInfo = await Self.app.models.Docuware.findOne({
|
||||||
|
where: {
|
||||||
|
code,
|
||||||
|
action
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!fileCabinetId) fileCabinetId = await Self.getFileCabinet(code);
|
||||||
|
|
||||||
|
const options = await Self.getOptions();
|
||||||
|
|
||||||
|
const response = await axios.get(`${options.url}/FileCabinets/${fileCabinetId}/dialogs`, options.headers);
|
||||||
|
const dialogs = response.data.Dialog;
|
||||||
|
const dialogId = dialogs.find(dialogs => dialogs.DisplayName === docuwareInfo.dialogName).Id;
|
||||||
|
|
||||||
|
return dialogId;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the fileCabinetId
|
||||||
|
*
|
||||||
|
* @param {string} code - The fileCabinet code
|
||||||
|
* @return {number} - The fileCabinet id
|
||||||
|
*/
|
||||||
|
Self.getFileCabinet = async code => {
|
||||||
|
if (!process.env.NODE_ENV)
|
||||||
|
return Math.floor(Math.random() + 100);
|
||||||
|
|
||||||
|
const options = await Self.getOptions();
|
||||||
|
const docuwareInfo = await Self.app.models.Docuware.findOne({
|
||||||
|
where: {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileCabinetResponse = await axios.get(`${options.url}/FileCabinets`, options.headers);
|
||||||
|
const fileCabinets = fileCabinetResponse.data.FileCabinet;
|
||||||
|
const fileCabinetId = fileCabinets.find(fileCabinet => fileCabinet.Name === docuwareInfo.fileCabinetName).Id;
|
||||||
|
|
||||||
|
return fileCabinetId;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns docuware data
|
||||||
|
*
|
||||||
|
* @param {string} code - The fileCabinet code
|
||||||
|
* @param {object} filter - The filter for docuware
|
||||||
|
* @param {object} parse - The fields parsed
|
||||||
|
* @return {object} - The data
|
||||||
|
*/
|
||||||
|
Self.get = async(code, filter, parse) => {
|
||||||
|
if (!process.env.NODE_ENV) return;
|
||||||
|
|
||||||
|
const options = await Self.getOptions();
|
||||||
|
const fileCabinetId = await Self.getFileCabinet(code);
|
||||||
|
const dialogId = await Self.getDialog(code, 'find', fileCabinetId);
|
||||||
|
|
||||||
|
const data = await axios.post(
|
||||||
|
`${options.url}/FileCabinets/${fileCabinetId}/Query/DialogExpression?dialogId=${dialogId}`,
|
||||||
|
filter,
|
||||||
|
options.headers
|
||||||
|
);
|
||||||
|
return parser(data.data, parse);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns docuware data
|
||||||
|
*
|
||||||
|
* @param {string} code - The fileCabinet code
|
||||||
|
* @param {any} id - The id of docuware
|
||||||
|
* @param {object} parse - The fields parsed
|
||||||
|
* @return {object} - The data
|
||||||
|
*/
|
||||||
|
Self.getById = async(code, id, parse) => {
|
||||||
|
if (!process.env.NODE_ENV) return;
|
||||||
|
|
||||||
|
const docuwareInfo = await Self.app.models.Docuware.findOne({
|
||||||
|
fields: ['findById'],
|
||||||
|
where: {
|
||||||
|
code,
|
||||||
|
action: 'find'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const filter = {
|
||||||
|
condition: [
|
||||||
|
{
|
||||||
|
DBName: docuwareInfo.findById,
|
||||||
|
Value: [id]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
return Self.get(code, filter, parse);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns docuware data filtered
|
||||||
|
*
|
||||||
|
* @param {array} data - The data
|
||||||
|
* @param {object} parse - The fields parsed
|
||||||
|
* @return {object} - The data parsed
|
||||||
|
*/
|
||||||
|
function parser(data, parse) {
|
||||||
|
if (!(data && data.Items)) return data;
|
||||||
|
|
||||||
|
const parsed = [];
|
||||||
|
for (item of data.Items) {
|
||||||
|
const itemParsed = {};
|
||||||
|
item.Fields.map(field => {
|
||||||
|
if (field.ItemElementName.includes('Date')) field.Item = toDate(field.Item);
|
||||||
|
if (!parse) return itemParsed[field.FieldLabel] = field.Item;
|
||||||
|
if (parse[field.FieldLabel])
|
||||||
|
itemParsed[parse[field.FieldLabel]] = field.Item;
|
||||||
|
});
|
||||||
|
parsed.push(itemParsed);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDate(value) {
|
||||||
|
if (!value) return;
|
||||||
|
return new Date(Number(value.substring(6, 19)));
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,78 @@
|
||||||
|
const {Email} = require('vn-print');
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethodCtx('deliveryNoteEmail', {
|
||||||
|
description: 'Sends the delivery note email with an docuware attached PDF',
|
||||||
|
accessType: 'WRITE',
|
||||||
|
accessScopes: ['docuwareDeliveryNoteEmail'],
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'id',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
description: 'The ticket id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'recipientId',
|
||||||
|
type: 'number',
|
||||||
|
description: 'The client id',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'recipient',
|
||||||
|
type: 'string',
|
||||||
|
description: 'The recipient email',
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
returns: [
|
||||||
|
{
|
||||||
|
arg: 'body',
|
||||||
|
type: 'file',
|
||||||
|
root: true
|
||||||
|
}, {
|
||||||
|
arg: 'Content-Type',
|
||||||
|
type: 'String',
|
||||||
|
http: {target: 'header'}
|
||||||
|
}, {
|
||||||
|
arg: 'Content-Disposition',
|
||||||
|
type: 'String',
|
||||||
|
http: {target: 'header'}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
http: {
|
||||||
|
path: '/delivery-note-email',
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.deliveryNoteEmail = async(ctx, id, recipientId, recipient) => {
|
||||||
|
const models = Self.app.models;
|
||||||
|
const args = Object.assign({}, ctx.args);
|
||||||
|
const params = {
|
||||||
|
recipient: args.recipient,
|
||||||
|
lang: ctx.req.getLocale()
|
||||||
|
};
|
||||||
|
|
||||||
|
delete args.ctx;
|
||||||
|
for (const param in args)
|
||||||
|
params[param] = args[param];
|
||||||
|
|
||||||
|
if (!recipient) {
|
||||||
|
client = await models.Client.findById(recipientId, {fields: ['email']});
|
||||||
|
params.recipient = client.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = new Email('delivery-note', params);
|
||||||
|
|
||||||
|
const docuwareFile = await models.Docuware.download(id, 'deliveryNote');
|
||||||
|
|
||||||
|
return email.send({
|
||||||
|
overrideAttachments: true,
|
||||||
|
attachments: [{
|
||||||
|
filename: `${id}.pdf`,
|
||||||
|
content: docuwareFile[0]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,29 +1,27 @@
|
||||||
/* eslint max-len: ["error", { "code": 180 }]*/
|
/* eslint max-len: ["error", { "code": 180 }]*/
|
||||||
const got = require('got');
|
const axios = require('axios');
|
||||||
const UserError = require('vn-loopback/util/user-error');
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
|
|
||||||
module.exports = Self => {
|
module.exports = Self => {
|
||||||
Self.remoteMethodCtx('download', {
|
Self.remoteMethod('download', {
|
||||||
description: 'Download an docuware PDF',
|
description: 'Download an docuware PDF',
|
||||||
accessType: 'READ',
|
accessType: 'READ',
|
||||||
accepts: [
|
accepts: [
|
||||||
{
|
{
|
||||||
arg: 'id',
|
arg: 'id',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'The id',
|
description: 'The ticket id',
|
||||||
http: {source: 'path'}
|
http: {source: 'path'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
arg: 'fileCabinet',
|
arg: 'fileCabinet',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The id',
|
description: 'The file cabinet'
|
||||||
http: {source: 'path'}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
arg: 'dialog',
|
arg: 'filter',
|
||||||
type: 'string',
|
type: 'object',
|
||||||
description: 'The id',
|
description: 'The filter'
|
||||||
http: {source: 'path'}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
returns: [
|
returns: [
|
||||||
|
@ -42,79 +40,27 @@ module.exports = Self => {
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
http: {
|
http: {
|
||||||
path: `/:id/download/:fileCabinet/:dialog`,
|
path: `/:id/download`,
|
||||||
verb: 'GET'
|
verb: 'GET'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.download = async function(ctx, id, fileCabinet, dialog) {
|
Self.download = async function(id, fileCabinet, filter) {
|
||||||
const myUserId = ctx.req.accessToken.userId;
|
|
||||||
if (!myUserId)
|
|
||||||
throw new UserError(`You don't have enough privileges`);
|
|
||||||
|
|
||||||
const models = Self.app.models;
|
const models = Self.app.models;
|
||||||
const docuwareConfig = await models.DocuwareConfig.findOne();
|
|
||||||
const docuwareInfo = await models.Docuware.findOne({
|
|
||||||
where: {
|
|
||||||
code: fileCabinet,
|
|
||||||
dialogName: dialog
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const docuwareUrl = docuwareConfig.url;
|
const docuwareFile = await models.Docuware.checkFile(id, fileCabinet, filter);
|
||||||
const cookie = docuwareConfig.token;
|
if (!docuwareFile) throw new UserError('The DOCUWARE PDF document does not exists');
|
||||||
const fileCabinetName = docuwareInfo.fileCabinetName;
|
|
||||||
const find = docuwareInfo.find;
|
|
||||||
const options = {
|
|
||||||
'headers': {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Cookie': cookie
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const searchFilter = {
|
|
||||||
condition: [
|
|
||||||
{
|
|
||||||
DBName: find,
|
|
||||||
Value: [id]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
const fileCabinetId = await Self.getFileCabinet(fileCabinet);
|
||||||
// get fileCabinetId
|
const options = await Self.getOptions();
|
||||||
const fileCabinetResponse = await got.get(`${docuwareUrl}/FileCabinets`, options);
|
options.headers.responseType = 'stream';
|
||||||
const fileCabinetJson = JSON.parse(fileCabinetResponse.body).FileCabinet;
|
|
||||||
const fileCabinetId = fileCabinetJson.find(dialogs => dialogs.Name === fileCabinetName).Id;
|
|
||||||
|
|
||||||
// get dialog
|
const fileName = `filename="${id}.pdf"`;
|
||||||
const dialogResponse = await got.get(`${docuwareUrl}/FileCabinets/${fileCabinetId}/dialogs`, options);
|
const contentType = 'application/pdf';
|
||||||
const dialogJson = JSON.parse(dialogResponse.body).Dialog;
|
const downloadUri = `${options.url}/FileCabinets/${fileCabinetId}/Documents/${docuwareFile.id}/FileDownload?targetFileType=Auto&keepAnnotations=false`;
|
||||||
const dialogId = dialogJson.find(dialogs => dialogs.DisplayName === 'find').Id;
|
|
||||||
|
|
||||||
// get docuwareID
|
const stream = await axios.get(downloadUri, options.headers);
|
||||||
Object.assign(options, {'body': JSON.stringify(searchFilter)});
|
|
||||||
const response = await got.post(`${docuwareUrl}/FileCabinets/${fileCabinetId}/Query/DialogExpression?dialogId=${dialogId}`, options);
|
|
||||||
const docuwareId = JSON.parse(response.body).Items[0].Id;
|
|
||||||
|
|
||||||
// download & save file
|
return [stream.data, contentType, fileName];
|
||||||
const fileName = `filename="${id}.pdf"`;
|
|
||||||
const contentType = 'application/pdf';
|
|
||||||
const downloadUri = `${docuwareUrl}/FileCabinets/${fileCabinetId}/Documents/${docuwareId}/FileDownload?targetFileType=Auto&keepAnnotations=false`;
|
|
||||||
const downloadOptions = {
|
|
||||||
'headers': {
|
|
||||||
'Cookie': cookie
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stream = got.stream(downloadUri, downloadOptions);
|
|
||||||
|
|
||||||
return [stream, contentType, fileName];
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'ENOENT')
|
|
||||||
throw new UserError('The DOCUWARE PDF document does not exists');
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,64 +1,28 @@
|
||||||
const models = require('vn-loopback/server/server').models;
|
const models = require('vn-loopback/server/server').models;
|
||||||
const got = require('got');
|
|
||||||
|
|
||||||
describe('docuware download()', () => {
|
describe('docuware download()', () => {
|
||||||
const ticketId = 1;
|
const ticketId = 1;
|
||||||
const userId = 9;
|
|
||||||
const ctx = {
|
|
||||||
req: {
|
|
||||||
|
|
||||||
accessToken: {userId: userId},
|
const docuwareModel = models.Docuware;
|
||||||
headers: {origin: 'http://localhost:5000'},
|
const fileCabinetName = 'deliveryNote';
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fileCabinetName = 'deliveryClient';
|
it('should return false if there are no documents', async() => {
|
||||||
const dialogDisplayName = 'find';
|
spyOn(docuwareModel, 'get').and.returnValue((new Promise(resolve => resolve({Items: []}))));
|
||||||
const dialogName = 'findTicket';
|
|
||||||
|
|
||||||
const gotGetResponse = {
|
const result = await models.Docuware.checkFile(ticketId, fileCabinetName, null, true);
|
||||||
body: JSON.stringify(
|
|
||||||
{
|
|
||||||
FileCabinet: [
|
|
||||||
{Id: 12, Name: fileCabinetName}
|
|
||||||
],
|
|
||||||
Dialog: [
|
|
||||||
{Id: 34, DisplayName: dialogDisplayName}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should return exist file in docuware', async() => {
|
|
||||||
const gotPostResponse = {
|
|
||||||
body: JSON.stringify(
|
|
||||||
{
|
|
||||||
Items: [
|
|
||||||
{Id: 56}
|
|
||||||
],
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
spyOn(got, 'get').and.returnValue(new Promise(resolve => resolve(gotGetResponse)));
|
|
||||||
spyOn(got, 'post').and.returnValue(new Promise(resolve => resolve(gotPostResponse)));
|
|
||||||
|
|
||||||
const result = await models.Docuware.checkFile(ctx, ticketId, fileCabinetName, dialogName);
|
|
||||||
|
|
||||||
expect(result).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return not exist file in docuware', async() => {
|
|
||||||
const gotPostResponse = {
|
|
||||||
body: JSON.stringify(
|
|
||||||
{
|
|
||||||
Items: [],
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
spyOn(got, 'get').and.returnValue(new Promise(resolve => resolve(gotGetResponse)));
|
|
||||||
spyOn(got, 'post').and.returnValue(new Promise(resolve => resolve(gotPostResponse)));
|
|
||||||
|
|
||||||
const result = await models.Docuware.checkFile(ctx, ticketId, fileCabinetName, dialogName);
|
|
||||||
|
|
||||||
expect(result).toEqual(false);
|
expect(result).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return the document data', async() => {
|
||||||
|
const docuwareId = 1;
|
||||||
|
const response = [{
|
||||||
|
'Document ID': docuwareId
|
||||||
|
}];
|
||||||
|
spyOn(docuwareModel, 'get').and.returnValue((new Promise(resolve => resolve(response))));
|
||||||
|
|
||||||
|
const result = await models.Docuware.checkFile(ticketId, fileCabinetName, null, true);
|
||||||
|
|
||||||
|
expect(result.id).toEqual(docuwareId);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
const axios = require('axios');
|
||||||
|
const models = require('vn-loopback/server/server').models;
|
||||||
|
|
||||||
|
describe('Docuware core', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
process.env.NODE_ENV = 'testing';
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
delete process.env.NODE_ENV;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getOptions()', () => {
|
||||||
|
it('should return url and headers', async() => {
|
||||||
|
const result = await models.Docuware.getOptions();
|
||||||
|
|
||||||
|
expect(result.url).toBeDefined();
|
||||||
|
expect(result.headers).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDialog()', () => {
|
||||||
|
it('should return dialogId', async() => {
|
||||||
|
const dialogs = {
|
||||||
|
data: {
|
||||||
|
Dialog: [
|
||||||
|
{
|
||||||
|
DisplayName: 'find',
|
||||||
|
Id: 'getDialogTest'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
spyOn(axios, 'get').and.returnValue(new Promise(resolve => resolve(dialogs)));
|
||||||
|
const result = await models.Docuware.getDialog('deliveryNote', 'find', 'randomFileCabinetId');
|
||||||
|
|
||||||
|
expect(result).toEqual('getDialogTest');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFileCabinet()', () => {
|
||||||
|
it('should return fileCabinetId', async() => {
|
||||||
|
const code = 'deliveryNote';
|
||||||
|
const docuwareInfo = await models.Docuware.findOne({
|
||||||
|
where: {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const dialogs = {
|
||||||
|
data: {
|
||||||
|
FileCabinet: [
|
||||||
|
{
|
||||||
|
Name: docuwareInfo.fileCabinetName,
|
||||||
|
Id: 'getFileCabinetTest'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
spyOn(axios, 'get').and.returnValue(new Promise(resolve => resolve(dialogs)));
|
||||||
|
const result = await models.Docuware.getFileCabinet(code);
|
||||||
|
|
||||||
|
expect(result).toEqual('getFileCabinetTest');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get()', () => {
|
||||||
|
it('should return data without parse', async() => {
|
||||||
|
spyOn(models.Docuware, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||||
|
spyOn(models.Docuware, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||||
|
const data = {
|
||||||
|
data: {
|
||||||
|
id: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(data)));
|
||||||
|
const result = await models.Docuware.get('deliveryNote');
|
||||||
|
|
||||||
|
expect(result.id).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return data with parse', async() => {
|
||||||
|
spyOn(models.Docuware, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||||
|
spyOn(models.Docuware, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||||
|
const data = {
|
||||||
|
data: {
|
||||||
|
Items: [{
|
||||||
|
Fields: [
|
||||||
|
{
|
||||||
|
ItemElementName: 'integer',
|
||||||
|
FieldLabel: 'firstRequiredField',
|
||||||
|
Item: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ItemElementName: 'string',
|
||||||
|
FieldLabel: 'secondRequiredField',
|
||||||
|
Item: 'myName'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ItemElementName: 'integer',
|
||||||
|
FieldLabel: 'notRequiredField',
|
||||||
|
Item: 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const parse = {
|
||||||
|
'firstRequiredField': 'id',
|
||||||
|
'secondRequiredField': 'name',
|
||||||
|
};
|
||||||
|
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(data)));
|
||||||
|
const [result] = await models.Docuware.get('deliveryNote', null, parse);
|
||||||
|
|
||||||
|
expect(result.id).toEqual(1);
|
||||||
|
expect(result.name).toEqual('myName');
|
||||||
|
expect(result.notRequiredField).not.toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getById()', () => {
|
||||||
|
it('should return data', async() => {
|
||||||
|
spyOn(models.Docuware, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||||
|
spyOn(models.Docuware, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||||
|
const data = {
|
||||||
|
data: {
|
||||||
|
id: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(data)));
|
||||||
|
const result = await models.Docuware.getById('deliveryNote', 1);
|
||||||
|
|
||||||
|
expect(result.id).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
const models = require('vn-loopback/server/server').models;
|
const models = require('vn-loopback/server/server').models;
|
||||||
const got = require('got');
|
const axios = require('axios');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
|
||||||
describe('docuware download()', () => {
|
describe('docuware download()', () => {
|
||||||
|
@ -13,36 +13,33 @@ describe('docuware download()', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should return the downloaded file name', async() => {
|
const docuwareModel = models.Docuware;
|
||||||
const fileCabinetName = 'deliveryClient';
|
const fileCabinetName = 'deliveryNote';
|
||||||
const dialogDisplayName = 'find';
|
|
||||||
const dialogName = 'findTicket';
|
|
||||||
const gotGetResponse = {
|
|
||||||
body: JSON.stringify(
|
|
||||||
{
|
|
||||||
FileCabinet: [
|
|
||||||
{Id: 12, Name: fileCabinetName}
|
|
||||||
],
|
|
||||||
Dialog: [
|
|
||||||
{Id: 34, DisplayName: dialogDisplayName}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
const gotPostResponse = {
|
beforeAll(() => {
|
||||||
body: JSON.stringify(
|
spyOn(docuwareModel, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||||
{
|
spyOn(docuwareModel, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||||
Items: [
|
});
|
||||||
{Id: 56}
|
|
||||||
],
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
spyOn(got, 'get').and.returnValue(new Promise(resolve => resolve(gotGetResponse)));
|
it('should return error if file not exist', async() => {
|
||||||
spyOn(got, 'post').and.returnValue(new Promise(resolve => resolve(gotPostResponse)));
|
spyOn(docuwareModel, 'checkFile').and.returnValue(false);
|
||||||
spyOn(got, 'stream').and.returnValue(new stream.PassThrough({objectMode: true}));
|
spyOn(axios, 'get').and.returnValue(new stream.PassThrough({objectMode: true}));
|
||||||
|
|
||||||
const result = await models.Docuware.download(ctx, ticketId, fileCabinetName, dialogName);
|
let error;
|
||||||
|
try {
|
||||||
|
await models.Docuware.download(ctx, ticketId, fileCabinetName);
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error).toEqual('The DOCUWARE PDF document does not exists');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the downloaded file if exist file ', async() => {
|
||||||
|
spyOn(docuwareModel, 'checkFile').and.returnValue({});
|
||||||
|
spyOn(axios, 'get').and.returnValue(new stream.PassThrough({objectMode: true}));
|
||||||
|
|
||||||
|
const result = await models.Docuware.download(ticketId, fileCabinetName);
|
||||||
|
|
||||||
expect(result[1]).toEqual('application/pdf');
|
expect(result[1]).toEqual('application/pdf');
|
||||||
expect(result[2]).toEqual(`filename="${ticketId}.pdf"`);
|
expect(result[2]).toEqual(`filename="${ticketId}.pdf"`);
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
const models = require('vn-loopback/server/server').models;
|
||||||
|
|
||||||
|
describe('docuware upload()', () => {
|
||||||
|
const userId = 9;
|
||||||
|
const ticketIds = [10];
|
||||||
|
const ctx = {
|
||||||
|
args: {ticketIds},
|
||||||
|
req: {
|
||||||
|
getLocale: () => {
|
||||||
|
return 'en';
|
||||||
|
},
|
||||||
|
accessToken: {userId: userId},
|
||||||
|
headers: {origin: 'http://localhost:5000'},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const docuwareModel = models.Docuware;
|
||||||
|
const ticketModel = models.Ticket;
|
||||||
|
const fileCabinetName = 'deliveryNote';
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
spyOn(docuwareModel, 'getFileCabinet').and.returnValue(new Promise(resolve => resolve(Math.random())));
|
||||||
|
spyOn(docuwareModel, 'getDialog').and.returnValue(new Promise(resolve => resolve(Math.random())));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should try upload file', async() => {
|
||||||
|
spyOn(ticketModel, 'deliveryNotePdf').and.returnValue(new Promise(resolve => resolve({})));
|
||||||
|
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await models.Docuware.upload(ctx, ticketIds, fileCabinetName);
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error).toEqual('Action not allowed on the test environment');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,151 @@
|
||||||
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethodCtx('upload', {
|
||||||
|
description: 'Upload docuware PDFs',
|
||||||
|
accessType: 'WRITE',
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'ticketIds',
|
||||||
|
type: ['number'],
|
||||||
|
description: 'The ticket ids',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'fileCabinet',
|
||||||
|
type: 'string',
|
||||||
|
description: 'The file cabinet',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
returns: {
|
||||||
|
type: 'object',
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/upload`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.upload = async function(ctx, ticketIds, fileCabinet) {
|
||||||
|
delete ctx.args.ticketIds;
|
||||||
|
const models = Self.app.models;
|
||||||
|
const action = 'store';
|
||||||
|
|
||||||
|
const options = await Self.getOptions();
|
||||||
|
const fileCabinetId = await Self.getFileCabinet(fileCabinet);
|
||||||
|
const dialogId = await Self.getDialog(fileCabinet, action, fileCabinetId);
|
||||||
|
|
||||||
|
const uploaded = [];
|
||||||
|
for (id of ticketIds) {
|
||||||
|
// get delivery note
|
||||||
|
ctx.args.id = id;
|
||||||
|
const deliveryNote = await models.Ticket.deliveryNotePdf(ctx, {
|
||||||
|
id,
|
||||||
|
type: 'deliveryNote'
|
||||||
|
});
|
||||||
|
// get ticket data
|
||||||
|
const ticket = await models.Ticket.findById(id, {
|
||||||
|
include: [{
|
||||||
|
relation: 'client',
|
||||||
|
scope: {
|
||||||
|
fields: ['id', 'name', 'fi']
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
// upload file
|
||||||
|
const templateJson = {
|
||||||
|
'Fields': [
|
||||||
|
{
|
||||||
|
'FieldName': 'N__ALBAR_N',
|
||||||
|
'ItemElementName': 'string',
|
||||||
|
'Item': id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'CIF_PROVEEDOR',
|
||||||
|
'ItemElementName': 'string',
|
||||||
|
'Item': ticket.client().fi,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'CODIGO_PROVEEDOR',
|
||||||
|
'ItemElementName': 'string',
|
||||||
|
'Item': ticket.client().id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'NOMBRE_PROVEEDOR',
|
||||||
|
'ItemElementName': 'string',
|
||||||
|
'Item': ticket.client().name + ' - ' + id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'FECHA_FACTURA',
|
||||||
|
'ItemElementName': 'date',
|
||||||
|
'Item': ticket.shipped,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'TOTAL_FACTURA',
|
||||||
|
'ItemElementName': 'Decimal',
|
||||||
|
'Item': ticket.totalWithVat,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'ESTADO',
|
||||||
|
'ItemElementName': 'string',
|
||||||
|
'Item': 'Pendiente procesar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'FIRMA_',
|
||||||
|
'ItemElementName': 'string',
|
||||||
|
'Item': 'Si',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'FILTRO_TABLET',
|
||||||
|
'ItemElementName': 'string',
|
||||||
|
'Item': 'Tablet1',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV != 'production')
|
||||||
|
throw new UserError('Action not allowed on the test environment');
|
||||||
|
|
||||||
|
// delete old
|
||||||
|
const docuwareFile = await models.Docuware.checkFile(id, fileCabinet, false);
|
||||||
|
if (docuwareFile) {
|
||||||
|
const deleteJson = {
|
||||||
|
'Field': [{'FieldName': 'ESTADO', 'Item': 'Pendiente eliminar', 'ItemElementName': 'String'}]
|
||||||
|
};
|
||||||
|
const deleteUri = `${options.url}/FileCabinets/${fileCabinetId}/Documents/${docuwareFile.id}/Fields`;
|
||||||
|
await axios.put(deleteUri, deleteJson, options.headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadUri = `${options.url}/FileCabinets/${fileCabinetId}/Documents?StoreDialogId=${dialogId}`;
|
||||||
|
const FormData = require('form-data');
|
||||||
|
const data = new FormData();
|
||||||
|
|
||||||
|
data.append('document', JSON.stringify(templateJson), 'schema.json');
|
||||||
|
data.append('file[]', deliveryNote[0], 'file.pdf');
|
||||||
|
const uploadOptions = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
'X-File-ModifiedDate': Date.vnNew(),
|
||||||
|
'Cookie': options.headers.headers.Cookie,
|
||||||
|
...data.getHeaders()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(uploadUri, data, uploadOptions);
|
||||||
|
} catch (err) {
|
||||||
|
const $t = ctx.req.__;
|
||||||
|
const message = $t('Failed to upload delivery note', {id});
|
||||||
|
if (uploaded.length)
|
||||||
|
await models.TicketTracking.setDelivered(ctx, uploaded);
|
||||||
|
throw new UserError(message);
|
||||||
|
}
|
||||||
|
uploaded.push(id);
|
||||||
|
}
|
||||||
|
return models.TicketTracking.setDelivered(ctx, ticketIds);
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,9 +1,9 @@
|
||||||
LOAD DATA LOCAL INFILE ?
|
LOAD DATA LOCAL INFILE ?
|
||||||
INTO TABLE `edi`.`item`
|
INTO TABLE `edi`.`item`
|
||||||
|
CHARACTER SET ascii
|
||||||
FIELDS TERMINATED BY ';'
|
FIELDS TERMINATED BY ';'
|
||||||
LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6, @col7, @col8, @col9, @col10, @col11, @col12)
|
LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6, @col7, @col8, @col9, @col10, @col11, @col12)
|
||||||
SET
|
SET id = @col2,
|
||||||
id = @col2,
|
|
||||||
product_name = @col4,
|
product_name = @col4,
|
||||||
name = @col5,
|
name = @col5,
|
||||||
plant_id = @col7,
|
plant_id = @col7,
|
||||||
|
@ -11,3 +11,4 @@ LOAD DATA LOCAL INFILE ?
|
||||||
entry_date = STR_TO_DATE(@col10, '%Y%m%d'),
|
entry_date = STR_TO_DATE(@col10, '%Y%m%d'),
|
||||||
expiry_date = IFNULL(NULL,STR_TO_DATE(@col11, '%Y%m%d')),
|
expiry_date = IFNULL(NULL,STR_TO_DATE(@col11, '%Y%m%d')),
|
||||||
change_date_time = STR_TO_DATE(@col12, '%Y%m%d%H%i')
|
change_date_time = STR_TO_DATE(@col12, '%Y%m%d%H%i')
|
||||||
|
|
|
@ -16,14 +16,14 @@ module.exports = Self => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.updateData = async() => {
|
Self.updateData = async ctx => {
|
||||||
const models = Self.app.models;
|
const models = Self.app.models;
|
||||||
|
|
||||||
// Get files checksum
|
// Get files checksum
|
||||||
const tx = await Self.beginTransaction({});
|
const tx = await Self.beginTransaction({});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = {transaction: tx};
|
const options = {transaction: tx, userId: ctx.req.accessToken.userId};
|
||||||
const files = await Self.rawSql('SELECT name, checksum, keyValue FROM edi.fileConfig', null, options);
|
const files = await Self.rawSql('SELECT name, checksum, keyValue FROM edi.fileConfig', null, options);
|
||||||
|
|
||||||
const updatableFiles = [];
|
const updatableFiles = [];
|
||||||
|
@ -53,9 +53,9 @@ module.exports = Self => {
|
||||||
const fileNames = updatableFiles.map(file => file.name);
|
const fileNames = updatableFiles.map(file => file.name);
|
||||||
|
|
||||||
const tables = await Self.rawSql(`
|
const tables = await Self.rawSql(`
|
||||||
SELECT fileName, toTable, file
|
SELECT fileName, toTable, file
|
||||||
FROM edi.tableConfig
|
FROM edi.tableConfig
|
||||||
WHERE file IN (?)`, [fileNames], options);
|
WHERE file IN (?)`, [fileNames], options);
|
||||||
|
|
||||||
for (const table of tables) {
|
for (const table of tables) {
|
||||||
const fileName = table.file;
|
const fileName = table.file;
|
||||||
|
@ -85,9 +85,9 @@ module.exports = Self => {
|
||||||
for (const file of updatableFiles) {
|
for (const file of updatableFiles) {
|
||||||
console.log(`Updating file ${file.name} checksum...`);
|
console.log(`Updating file ${file.name} checksum...`);
|
||||||
await Self.rawSql(`
|
await Self.rawSql(`
|
||||||
UPDATE edi.fileConfig
|
UPDATE edi.fileConfig
|
||||||
SET checksum = ?
|
SET checksum = ?
|
||||||
WHERE name = ?`,
|
WHERE name = ?`,
|
||||||
[file.checksum, file.name], options);
|
[file.checksum, file.name], options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +121,8 @@ module.exports = Self => {
|
||||||
host: ftpConfig.host,
|
host: ftpConfig.host,
|
||||||
username: ftpConfig.user,
|
username: ftpConfig.user,
|
||||||
password: ftpConfig.password,
|
password: ftpConfig.password,
|
||||||
procotol: 'ftp'
|
procotol: 'ftp',
|
||||||
|
additionalLftpCommands: 'set ssl:verify-certificate no'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,13 +133,13 @@ module.exports = Self => {
|
||||||
const ftpClient = await getFtpClient();
|
const ftpClient = await getFtpClient();
|
||||||
console.debug(`Checking checksum for file ${file.name}...`);
|
console.debug(`Checking checksum for file ${file.name}...`);
|
||||||
|
|
||||||
ftpClient.cat(`codes/${file.name}.txt`);
|
ftpClient.cat(`codes/${file.name}.TXT`);
|
||||||
|
|
||||||
const response = await new Promise((resolve, reject) => {
|
const response = await new Promise((resolve, reject) => {
|
||||||
ftpClient.exec((err, response) => {
|
ftpClient.exec((err, response) => {
|
||||||
if (err || response.error) {
|
if (err || response.error) {
|
||||||
console.debug(`Error downloading checksum file... ${response.error}`);
|
console.debug(`Error downloading checksum file... ${response.error}`);
|
||||||
return reject(err);
|
return reject(response.error || err);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(response);
|
resolve(response);
|
||||||
|
@ -227,10 +228,10 @@ module.exports = Self => {
|
||||||
|
|
||||||
await Self.rawSql(sqlTemplate, [filePath], options);
|
await Self.rawSql(sqlTemplate, [filePath], options);
|
||||||
await Self.rawSql(`
|
await Self.rawSql(`
|
||||||
UPDATE edi.tableConfig
|
UPDATE edi.tableConfig
|
||||||
SET updated = ?
|
SET updated = ?
|
||||||
WHERE fileName = ?
|
WHERE fileName = ?
|
||||||
`, [new Date(), baseName], options);
|
`, [Date.vnNew(), baseName], options);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Updated table ${toTable}\n`);
|
console.log(`Updated table ${toTable}\n`);
|
||||||
|
|
|
@ -67,17 +67,20 @@ module.exports = Self => {
|
||||||
|
|
||||||
if (!image) return false;
|
if (!image) return false;
|
||||||
|
|
||||||
const hasReadRole = models.ImageCollection.hasReadRole(ctx, collection);
|
const hasReadRole = await models.ImageCollection.hasReadRole(ctx, collection);
|
||||||
if (!hasReadRole)
|
if (!hasReadRole)
|
||||||
throw new UserError(`You don't have enough privileges`);
|
throw new UserError(`You don't have enough privileges`);
|
||||||
|
|
||||||
const container = await models.ImageContainer.getContainer(collection);
|
const container = await models.ImageContainer.getContainer(collection);
|
||||||
const rootPath = container.client.root;
|
const rootPath = container.client.root;
|
||||||
const fileSrc = path.join(rootPath, collection, size);
|
const fileSrc = path.join(rootPath, collection, size);
|
||||||
|
|
||||||
|
const ext = image.name.substring((image.name.length - 4));
|
||||||
|
const fileName = ext !== '.png' ? `${image.name}.png` : image.name;
|
||||||
const file = {
|
const file = {
|
||||||
path: `${fileSrc}/${image.name}.png`,
|
path: `${fileSrc}/${fileName}`,
|
||||||
contentType: 'image/png',
|
contentType: 'image/png',
|
||||||
name: `${image.name}.png`
|
name: image.name
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!fs.existsSync(file.path)) return [];
|
if (!fs.existsSync(file.path)) return [];
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethod('scrub', {
|
||||||
|
description: 'Deletes images without database reference',
|
||||||
|
accessType: 'WRITE',
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'collection',
|
||||||
|
type: 'string',
|
||||||
|
description: 'The collection name',
|
||||||
|
required: true
|
||||||
|
}, {
|
||||||
|
arg: 'remove',
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Delete instead of move images to trash'
|
||||||
|
}, {
|
||||||
|
arg: 'limit',
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Maximum number of images to clean'
|
||||||
|
}, {
|
||||||
|
arg: 'dryRun',
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Simulate actions'
|
||||||
|
}, {
|
||||||
|
arg: 'skipLock',
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Wether to skip exclusive lock'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
returns: {
|
||||||
|
type: 'integer',
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/scrub`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.scrub = async function(collection, remove, limit, dryRun, skipLock) {
|
||||||
|
const $ = Self.app.models;
|
||||||
|
|
||||||
|
const env = process.env.NODE_ENV;
|
||||||
|
dryRun = dryRun || (env && env !== 'production');
|
||||||
|
|
||||||
|
const instance = await $.ImageCollection.findOne({
|
||||||
|
fields: ['id'],
|
||||||
|
where: {name: collection}
|
||||||
|
});
|
||||||
|
if (!instance)
|
||||||
|
throw new UserError('Collection does not exist');
|
||||||
|
|
||||||
|
const container = await $.ImageContainer.container(collection);
|
||||||
|
const rootPath = container.client.root;
|
||||||
|
|
||||||
|
let tx;
|
||||||
|
let opts;
|
||||||
|
const lockName = 'salix.Image.scrub';
|
||||||
|
|
||||||
|
if (!skipLock) {
|
||||||
|
tx = await Self.beginTransaction({timeout: null});
|
||||||
|
opts = {transaction: tx};
|
||||||
|
|
||||||
|
const [row] = await Self.rawSql(
|
||||||
|
`SELECT GET_LOCK(?, 10) hasLock`, [lockName], opts);
|
||||||
|
if (!row.hasLock)
|
||||||
|
throw new UserError('Cannot obtain exclusive lock');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const now = Date.vnNew().toJSON();
|
||||||
|
const scrubDir = path.join(rootPath, '.scrub', now);
|
||||||
|
|
||||||
|
const collectionDir = path.join(rootPath, collection);
|
||||||
|
const sizes = await fs.readdir(collectionDir);
|
||||||
|
let cleanCount = 0;
|
||||||
|
|
||||||
|
mainLoop: for (const size of sizes) {
|
||||||
|
const sizeDir = path.join(collectionDir, size);
|
||||||
|
const scrubSizeDir = path.join(scrubDir, collection, size);
|
||||||
|
const images = await fs.readdir(sizeDir);
|
||||||
|
for (const image of images) {
|
||||||
|
const imageName = path.parse(image).name;
|
||||||
|
const count = await Self.count({
|
||||||
|
collectionFk: collection,
|
||||||
|
name: imageName
|
||||||
|
}, opts);
|
||||||
|
const exists = count > 0;
|
||||||
|
let scrubDirCreated = false;
|
||||||
|
if (!exists) {
|
||||||
|
const srcFile = path.join(sizeDir, image);
|
||||||
|
if (remove !== true) {
|
||||||
|
if (!scrubDirCreated) {
|
||||||
|
if (!dryRun)
|
||||||
|
await fs.mkdir(scrubSizeDir, {recursive: true});
|
||||||
|
scrubDirCreated = true;
|
||||||
|
}
|
||||||
|
const dstFile = path.join(scrubSizeDir, image);
|
||||||
|
if (!dryRun) await fs.rename(srcFile, dstFile);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if (!dryRun) await fs.unlink(srcFile);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanCount++;
|
||||||
|
if (limit && cleanCount == limit)
|
||||||
|
break mainLoop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanCount;
|
||||||
|
} finally {
|
||||||
|
if (!skipLock) {
|
||||||
|
try {
|
||||||
|
await Self.rawSql(`DO RELEASE_LOCK(?)`, [lockName], opts);
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const {models} = require('vn-loopback/server/server');
|
||||||
|
|
||||||
describe('image download()', () => {
|
describe('image download()', () => {
|
||||||
const collection = 'user';
|
const collection = 'user';
|
||||||
|
@ -8,7 +8,7 @@ describe('image download()', () => {
|
||||||
|
|
||||||
it('should return the image content-type of the user', async() => {
|
it('should return the image content-type of the user', async() => {
|
||||||
const userId = 9;
|
const userId = 9;
|
||||||
const image = await app.models.Image.download(ctx, collection, size, userId);
|
const image = await models.Image.download(ctx, collection, size, userId);
|
||||||
const contentType = image[1];
|
const contentType = image[1];
|
||||||
|
|
||||||
expect(contentType).toEqual('image/png');
|
expect(contentType).toEqual('image/png');
|
||||||
|
@ -16,7 +16,7 @@ describe('image download()', () => {
|
||||||
|
|
||||||
it(`should return false if the user doesn't have image`, async() => {
|
it(`should return false if the user doesn't have image`, async() => {
|
||||||
const userId = 1110;
|
const userId = 1110;
|
||||||
const image = await app.models.Image.download(ctx, collection, size, userId);
|
const image = await models.Image.download(ctx, collection, size, userId);
|
||||||
|
|
||||||
expect(image).toBeFalse();
|
expect(image).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const {models} = require('vn-loopback/server/server');
|
||||||
|
|
||||||
describe('image upload()', () => {
|
describe('image upload()', () => {
|
||||||
describe('as buyer', () => {
|
describe('as buyer', () => {
|
||||||
|
@ -16,7 +16,7 @@ describe('image upload()', () => {
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
try {
|
try {
|
||||||
await app.models.Image.upload(ctx);
|
await models.Image.upload(ctx);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err;
|
error = err;
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ describe('image upload()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call to the TempContainer upload method for the collection "catalog"', async() => {
|
it('should call to the TempContainer upload method for the collection "catalog"', async() => {
|
||||||
const containerModel = app.models.TempContainer;
|
const containerModel = models.TempContainer;
|
||||||
spyOn(containerModel, 'upload');
|
spyOn(containerModel, 'upload');
|
||||||
|
|
||||||
const ctx = {req: {accessToken: {userId: buyerId}},
|
const ctx = {req: {accessToken: {userId: buyerId}},
|
||||||
|
@ -36,7 +36,7 @@ describe('image upload()', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await app.models.Image.upload(ctx);
|
await models.Image.upload(ctx);
|
||||||
} catch (err) { }
|
} catch (err) { }
|
||||||
|
|
||||||
expect(containerModel.upload).toHaveBeenCalled();
|
expect(containerModel.upload).toHaveBeenCalled();
|
||||||
|
@ -49,7 +49,7 @@ describe('image upload()', () => {
|
||||||
const itemId = 4;
|
const itemId = 4;
|
||||||
|
|
||||||
it('should be able to call to the TempContainer upload method for the collection "user"', async() => {
|
it('should be able to call to the TempContainer upload method for the collection "user"', async() => {
|
||||||
const containerModel = app.models.TempContainer;
|
const containerModel = models.TempContainer;
|
||||||
spyOn(containerModel, 'upload');
|
spyOn(containerModel, 'upload');
|
||||||
|
|
||||||
const ctx = {req: {accessToken: {userId: marketingId}},
|
const ctx = {req: {accessToken: {userId: marketingId}},
|
||||||
|
@ -60,14 +60,14 @@ describe('image upload()', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await app.models.Image.upload(ctx);
|
await models.Image.upload(ctx);
|
||||||
} catch (err) { }
|
} catch (err) { }
|
||||||
|
|
||||||
expect(containerModel.upload).toHaveBeenCalled();
|
expect(containerModel.upload).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to call to the TempContainer upload method for the collection "catalog"', async() => {
|
it('should be able to call to the TempContainer upload method for the collection "catalog"', async() => {
|
||||||
const containerModel = app.models.TempContainer;
|
const containerModel = models.TempContainer;
|
||||||
spyOn(containerModel, 'upload');
|
spyOn(containerModel, 'upload');
|
||||||
|
|
||||||
const ctx = {req: {accessToken: {userId: marketingId}},
|
const ctx = {req: {accessToken: {userId: marketingId}},
|
||||||
|
@ -78,7 +78,7 @@ describe('image upload()', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await app.models.Image.upload(ctx);
|
await models.Image.upload(ctx);
|
||||||
} catch (err) { }
|
} catch (err) { }
|
||||||
|
|
||||||
expect(containerModel.upload).toHaveBeenCalled();
|
expect(containerModel.upload).toHaveBeenCalled();
|
||||||
|
@ -91,7 +91,7 @@ describe('image upload()', () => {
|
||||||
const itemId = 4;
|
const itemId = 4;
|
||||||
|
|
||||||
it('should upload a file for the collection "user" and call to the TempContainer upload method', async() => {
|
it('should upload a file for the collection "user" and call to the TempContainer upload method', async() => {
|
||||||
const containerModel = app.models.TempContainer;
|
const containerModel = models.TempContainer;
|
||||||
spyOn(containerModel, 'upload');
|
spyOn(containerModel, 'upload');
|
||||||
|
|
||||||
const ctx = {req: {accessToken: {userId: hhrrId}},
|
const ctx = {req: {accessToken: {userId: hhrrId}},
|
||||||
|
@ -102,7 +102,7 @@ describe('image upload()', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await app.models.Image.upload(ctx);
|
await models.Image.upload(ctx);
|
||||||
} catch (err) { }
|
} catch (err) { }
|
||||||
|
|
||||||
expect(containerModel.upload).toHaveBeenCalled();
|
expect(containerModel.upload).toHaveBeenCalled();
|
||||||
|
@ -118,7 +118,7 @@ describe('image upload()', () => {
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
try {
|
try {
|
||||||
await app.models.Image.upload(ctx);
|
await models.Image.upload(ctx);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err;
|
error = err;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const UserError = require('vn-loopback/util/user-error');
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs/promises');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
module.exports = Self => {
|
module.exports = Self => {
|
||||||
|
@ -12,19 +12,13 @@ module.exports = Self => {
|
||||||
type: 'Number',
|
type: 'Number',
|
||||||
description: 'The entity id',
|
description: 'The entity id',
|
||||||
required: true
|
required: true
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
arg: 'collection',
|
arg: 'collection',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The collection name',
|
description: 'The collection name',
|
||||||
required: true
|
required: true
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
arg: 'fileName',
|
|
||||||
type: 'string',
|
|
||||||
description: 'The file name',
|
|
||||||
required: true
|
|
||||||
}],
|
|
||||||
returns: {
|
returns: {
|
||||||
type: 'Object',
|
type: 'Object',
|
||||||
root: true
|
root: true
|
||||||
|
@ -41,7 +35,7 @@ module.exports = Self => {
|
||||||
const fileOptions = {};
|
const fileOptions = {};
|
||||||
const args = ctx.args;
|
const args = ctx.args;
|
||||||
|
|
||||||
let srcFile;
|
let tempFilePath;
|
||||||
try {
|
try {
|
||||||
const hasWriteRole = await models.ImageCollection.hasWriteRole(ctx, args.collection);
|
const hasWriteRole = await models.ImageCollection.hasWriteRole(ctx, args.collection);
|
||||||
if (!hasWriteRole)
|
if (!hasWriteRole)
|
||||||
|
@ -56,15 +50,22 @@ module.exports = Self => {
|
||||||
const [uploadedFile] = Object.values(uploaded.files).map(file => {
|
const [uploadedFile] = Object.values(uploaded.files).map(file => {
|
||||||
return file[0];
|
return file[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name);
|
const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name);
|
||||||
srcFile = path.join(file.client.root, file.container, file.name);
|
tempFilePath = path.join(file.client.root, file.container, file.name);
|
||||||
|
|
||||||
await models.Image.registerImage(args.collection, srcFile, args.fileName, args.id);
|
const fileName = `${args.id}.png`;
|
||||||
} catch (e) {
|
|
||||||
if (fs.existsSync(srcFile))
|
|
||||||
await fs.unlink(srcFile);
|
|
||||||
|
|
||||||
throw e;
|
await models.Image.resize({
|
||||||
|
collectionName: args.collection,
|
||||||
|
srcFile: tempFilePath,
|
||||||
|
fileName: fileName,
|
||||||
|
entityId: args.id
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await fs.unlink(tempFilePath);
|
||||||
|
} catch (error) { }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethod('clean', {
|
||||||
|
description: 'clean notifications from queue',
|
||||||
|
accessType: 'WRITE',
|
||||||
|
returns: {
|
||||||
|
type: 'object',
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/clean`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.clean = async options => {
|
||||||
|
const models = Self.app.models;
|
||||||
|
const status = ['sent', 'error'];
|
||||||
|
|
||||||
|
const myOptions = {};
|
||||||
|
let tx;
|
||||||
|
|
||||||
|
if (typeof options == 'object')
|
||||||
|
Object.assign(myOptions, options);
|
||||||
|
|
||||||
|
if (!myOptions.transaction) {
|
||||||
|
tx = await Self.beginTransaction({});
|
||||||
|
myOptions.transaction = tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = await models.NotificationConfig.findOne({}, myOptions);
|
||||||
|
|
||||||
|
if (!config.cleanDays) return;
|
||||||
|
|
||||||
|
const cleanDate = Date.vnNew();
|
||||||
|
cleanDate.setDate(cleanDate.getDate() - config.cleanDays);
|
||||||
|
|
||||||
|
await models.NotificationQueue.destroyAll({
|
||||||
|
where: {status: {inq: status}},
|
||||||
|
created: {lt: cleanDate}
|
||||||
|
}, myOptions);
|
||||||
|
} catch (e) {
|
||||||
|
if (tx) await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx) await tx.commit();
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,83 @@
|
||||||
|
const {Email} = require('vn-print');
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethod('send', {
|
||||||
|
description: 'Send notifications from queue',
|
||||||
|
accessType: 'WRITE',
|
||||||
|
returns: {
|
||||||
|
type: 'object',
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/send`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.send = async options => {
|
||||||
|
const models = Self.app.models;
|
||||||
|
const findStatus = 'pending';
|
||||||
|
|
||||||
|
const myOptions = {};
|
||||||
|
if (typeof options == 'object')
|
||||||
|
Object.assign(myOptions, options);
|
||||||
|
|
||||||
|
const notificationQueue = await models.NotificationQueue.find({
|
||||||
|
where: {status: findStatus},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
relation: 'notification',
|
||||||
|
scope: {
|
||||||
|
include: {
|
||||||
|
relation: 'subscription',
|
||||||
|
scope: {
|
||||||
|
include: {
|
||||||
|
relation: 'user',
|
||||||
|
scope: {
|
||||||
|
fields: ['name', 'lang'],
|
||||||
|
include: {
|
||||||
|
relation: 'emailUser'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, myOptions);
|
||||||
|
|
||||||
|
const statusSent = 'sent';
|
||||||
|
const statusError = 'error';
|
||||||
|
|
||||||
|
for (const queue of notificationQueue) {
|
||||||
|
const queueName = queue.notification().name;
|
||||||
|
const queueParams = JSON.parse(queue.params);
|
||||||
|
|
||||||
|
for (const notificationUser of queue.notification().subscription()) {
|
||||||
|
try {
|
||||||
|
const sendParams = {
|
||||||
|
recipient: notificationUser.user().emailUser().email,
|
||||||
|
lang: notificationUser.user().lang
|
||||||
|
};
|
||||||
|
|
||||||
|
if (notificationUser.userFk == queue.authorFk) {
|
||||||
|
await queue.updateAttribute('status', statusSent);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newParams = Object.assign({}, queueParams, sendParams);
|
||||||
|
const email = new Email(queueName, newParams);
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV != 'test')
|
||||||
|
await email.send();
|
||||||
|
|
||||||
|
await queue.updateAttribute('status', statusSent);
|
||||||
|
} catch (error) {
|
||||||
|
await queue.updateAttribute('status', statusError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,42 @@
|
||||||
|
const models = require('vn-loopback/server/server').models;
|
||||||
|
|
||||||
|
describe('Notification Clean()', () => {
|
||||||
|
it('should delete old rows with error', async() => {
|
||||||
|
const userId = 9;
|
||||||
|
const status = 'error';
|
||||||
|
const tx = await models.NotificationQueue.beginTransaction({});
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
const notification = await models.Notification.findOne({}, options);
|
||||||
|
const notificationConfig = await models.NotificationConfig.findOne({});
|
||||||
|
|
||||||
|
const cleanDate = Date.vnNew();
|
||||||
|
cleanDate.setDate(cleanDate.getDate() - (notificationConfig.cleanDays + 1));
|
||||||
|
|
||||||
|
let before;
|
||||||
|
let after;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const notificationDelete = await models.NotificationQueue.create({
|
||||||
|
notificationFk: notification.name,
|
||||||
|
authorFk: userId,
|
||||||
|
status: status,
|
||||||
|
created: cleanDate
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
before = await models.NotificationQueue.findById(notificationDelete.id, null, options);
|
||||||
|
|
||||||
|
await models.Notification.clean(options);
|
||||||
|
|
||||||
|
after = await models.NotificationQueue.findById(notificationDelete.id, null, options);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(before.notificationFk).toEqual(notification.name);
|
||||||
|
expect(after).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,33 @@
|
||||||
|
const models = require('vn-loopback/server/server').models;
|
||||||
|
|
||||||
|
describe('Notification Send()', () => {
|
||||||
|
it('should send notification', async() => {
|
||||||
|
const statusPending = 'pending';
|
||||||
|
const tx = await models.NotificationQueue.beginTransaction({});
|
||||||
|
const options = {transaction: tx};
|
||||||
|
const filter = {
|
||||||
|
where: {
|
||||||
|
status: statusPending
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let before;
|
||||||
|
let after;
|
||||||
|
|
||||||
|
try {
|
||||||
|
before = await models.NotificationQueue.find(filter, options);
|
||||||
|
|
||||||
|
await models.Notification.send(options);
|
||||||
|
|
||||||
|
after = await models.NotificationQueue.find(filter, options);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(before.length).toEqual(3);
|
||||||
|
expect(after.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,12 +1,13 @@
|
||||||
const jsdom = require('jsdom');
|
const jsdom = require('jsdom');
|
||||||
const mysql = require('mysql');
|
const mysql = require('mysql');
|
||||||
|
const FormData = require('form-data');
|
||||||
|
|
||||||
module.exports = Self => {
|
module.exports = Self => {
|
||||||
Self.remoteMethodCtx('closeTicket', {
|
Self.remoteMethodCtx('closeTicket', {
|
||||||
description: 'Close tickets without response from the user',
|
description: 'Close tickets without response from the user',
|
||||||
accessType: 'READ',
|
accessType: 'READ',
|
||||||
returns: {
|
returns: {
|
||||||
type: 'Object',
|
type: 'object',
|
||||||
root: true
|
root: true
|
||||||
},
|
},
|
||||||
http: {
|
http: {
|
||||||
|
@ -24,52 +25,59 @@ module.exports = Self => {
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const con = mysql.createConnection({
|
const con = mysql.createConnection({
|
||||||
host: `${config.hostDb}`,
|
host: config.hostDb,
|
||||||
user: `${config.userDb}`,
|
user: config.userDb,
|
||||||
password: `${config.passwordDb}`,
|
password: config.passwordDb,
|
||||||
port: `${config.portDb}`
|
port: config.portDb
|
||||||
});
|
});
|
||||||
|
|
||||||
const sql = `SELECT ot.ticket_id, ot.number
|
const sql = `SELECT ot.ticket_id, ot.number
|
||||||
FROM osticket.ost_ticket ot
|
FROM osticket.ost_ticket ot
|
||||||
JOIN osticket.ost_ticket_status ots ON ots.id = ot.status_id
|
JOIN osticket.ost_ticket_status ots ON ots.id = ot.status_id
|
||||||
JOIN osticket.ost_thread ot2 ON ot2.object_id = ot.ticket_id AND ot2.object_type = 'T'
|
JOIN osticket.ost_thread ot2 ON ot2.object_id = ot.ticket_id AND ot2.object_type = 'T'
|
||||||
JOIN (
|
JOIN (
|
||||||
SELECT ote.thread_id, MAX(ote.created) created, MAX(ote.updated) updated
|
SELECT sub2.thread_id, sub2.type, sub2.updated, sub2.created
|
||||||
FROM osticket.ost_thread_entry ote
|
FROM (
|
||||||
WHERE ote.staff_id != 0 AND ote.type = 'R'
|
SELECT ote.thread_id, ote.created, ote.updated, ote.type
|
||||||
GROUP BY ote.thread_id
|
FROM osticket.ost_thread_entry ote
|
||||||
) sub ON sub.thread_id = ot2.id
|
WHERE ote.staff_id
|
||||||
WHERE ot.isanswered = 1
|
ORDER BY ote.id DESC
|
||||||
AND ots.state = '${config.oldStatus}'
|
LIMIT 10000000000000000000) sub2
|
||||||
AND IF(sub.updated > sub.created, sub.updated, sub.created) < DATE_SUB(CURDATE(), INTERVAL ${config.day} DAY)`;
|
GROUP BY sub2.thread_id
|
||||||
|
) sub ON sub.thread_id = ot2.id
|
||||||
|
WHERE ot.isanswered
|
||||||
|
AND ots.id IN (?)
|
||||||
|
AND sub.type = 'R'
|
||||||
|
AND IF(sub.updated > sub.created, sub.updated, sub.created) < DATE_SUB(CURDATE(), INTERVAL ? DAY);`;
|
||||||
|
|
||||||
|
const ticketsId = [];
|
||||||
|
const statusIdToClose = config.oldStatus.split(',');
|
||||||
|
|
||||||
let ticketsId = [];
|
|
||||||
con.connect(err => {
|
con.connect(err => {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
con.query(sql, (err, results) => {
|
con.query(sql, [statusIdToClose, config.day],
|
||||||
if (err) throw err;
|
(err, results) => {
|
||||||
for (const result of results)
|
if (err) throw err;
|
||||||
ticketsId.push(result.ticket_id);
|
for (const result of results)
|
||||||
});
|
ticketsId.push(result.ticket_id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
await getRequestToken();
|
||||||
|
|
||||||
await requestToken();
|
async function getRequestToken() {
|
||||||
|
|
||||||
async function requestToken() {
|
|
||||||
const response = await fetch(ostUri);
|
const response = await fetch(ostUri);
|
||||||
|
|
||||||
const result = response.headers.get('set-cookie');
|
const result = response.headers.get('set-cookie');
|
||||||
const [firtHeader] = result.split(' ');
|
const [firtHeader] = result.split(' ');
|
||||||
const firtCookie = firtHeader.substring(0, firtHeader.length - 1);
|
const cookie = firtHeader.substring(0, firtHeader.length - 1);
|
||||||
const body = await response.text();
|
const body = await response.text();
|
||||||
const dom = new jsdom.JSDOM(body);
|
const dom = new jsdom.JSDOM(body);
|
||||||
const token = dom.window.document.querySelector('[name="__CSRFToken__"]').value;
|
const token = dom.window.document.querySelector('[name="__CSRFToken__"]').value;
|
||||||
|
|
||||||
await login(token, firtCookie);
|
await login(token, cookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function login(token, firtCookie) {
|
async function login(token, cookie) {
|
||||||
const data = {
|
const data = {
|
||||||
__CSRFToken__: token,
|
__CSRFToken__: token,
|
||||||
do: 'scplogin',
|
do: 'scplogin',
|
||||||
|
@ -82,37 +90,66 @@ module.exports = Self => {
|
||||||
body: new URLSearchParams(data),
|
body: new URLSearchParams(data),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||||
'Cookie': firtCookie
|
'Cookie': cookie
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await fetch(ostUri, params);
|
||||||
|
|
||||||
|
await close(token, cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function close(token, cookie) {
|
||||||
|
for (const ticketId of ticketsId) {
|
||||||
|
try {
|
||||||
|
const lock = await getLockCode(token, cookie, ticketId);
|
||||||
|
if (!lock.code) {
|
||||||
|
let error = `Can't get lock code`;
|
||||||
|
if (lock.msg) error += `: ${lock.msg}`;
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
let form = new FormData();
|
||||||
|
form.append('__CSRFToken__', token);
|
||||||
|
form.append('id', ticketId);
|
||||||
|
form.append('a', config.responseType);
|
||||||
|
form.append('lockCode', lock.code);
|
||||||
|
form.append('from_email_id', config.fromEmailId);
|
||||||
|
form.append('reply-to', config.replyTo);
|
||||||
|
form.append('cannedResp', 0);
|
||||||
|
form.append('response', config.comment);
|
||||||
|
form.append('signature', 'none');
|
||||||
|
form.append('reply_status_id', config.newStatusId);
|
||||||
|
|
||||||
|
const ostUri = `${config.host}/tickets.php?id=${ticketId}`;
|
||||||
|
const params = {
|
||||||
|
method: 'POST',
|
||||||
|
body: form,
|
||||||
|
headers: {
|
||||||
|
'Cookie': cookie
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await fetch(ostUri, params);
|
||||||
|
} catch (e) {
|
||||||
|
const err = new Error(`${ticketId} Ticket close failed: ${e.message}`);
|
||||||
|
err.stack += e.stack;
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLockCode(token, cookie, ticketId) {
|
||||||
|
const ostUri = `${config.host}/ajax.php/lock/ticket/${ticketId}`;
|
||||||
|
const params = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': token,
|
||||||
|
'Cookie': cookie
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const response = await fetch(ostUri, params);
|
const response = await fetch(ostUri, params);
|
||||||
const result = response.headers.get('set-cookie');
|
const body = await response.text();
|
||||||
const [firtHeader] = result.split(' ');
|
const json = JSON.parse(body);
|
||||||
const secondCookie = firtHeader.substring(0, firtHeader.length - 1);
|
|
||||||
|
|
||||||
await close(token, secondCookie);
|
return json;
|
||||||
}
|
|
||||||
|
|
||||||
async function close(token, secondCookie) {
|
|
||||||
for (const ticketId of ticketsId) {
|
|
||||||
const ostUri = `${config.host}/ajax.php/tickets/${ticketId}/status`;
|
|
||||||
const data = {
|
|
||||||
status_id: config.newStatusId,
|
|
||||||
comments: config.comment,
|
|
||||||
undefined: config.action
|
|
||||||
};
|
|
||||||
const params = {
|
|
||||||
method: 'POST',
|
|
||||||
body: new URLSearchParams(data),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
||||||
'X-CSRFToken': token,
|
|
||||||
'Cookie': secondCookie
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return fetch(ostUri, params);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
const smtp = require('vn-print/core/smtp');
|
||||||
|
const config = require('vn-print/core/config');
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethodCtx('sendToSupport', {
|
||||||
|
description: 'Send mail to support',
|
||||||
|
accessType: 'WRITE',
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'reason',
|
||||||
|
type: 'string',
|
||||||
|
description: 'The reason'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'additionalData',
|
||||||
|
type: 'object',
|
||||||
|
required: true,
|
||||||
|
description: 'The additional data'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
returns: {
|
||||||
|
type: 'object',
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/send-to-support`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.sendToSupport = async(ctx, reason, additionalData) => {
|
||||||
|
const emailUser =
|
||||||
|
await Self.app.models.EmailUser.findById(ctx.req.accessToken.userId, {fields: ['email']});
|
||||||
|
|
||||||
|
let html = `<strong>Motivo</strong>:<br/>${reason}<br/>`;
|
||||||
|
html += `<strong>Usuario</strong>:<br/>${ctx.req.accessToken.userId} ${emailUser.email}<br/>`;
|
||||||
|
|
||||||
|
for (const data in additionalData)
|
||||||
|
html += `<strong>${data}</strong>:<br/>${tryParse(additionalData[data])}<br/>`;
|
||||||
|
|
||||||
|
const subjectReason = JSON.parse(additionalData?.httpRequest)?.data?.error;
|
||||||
|
smtp.send({
|
||||||
|
to: `${config.app.reportEmail}, ${emailUser.email}`,
|
||||||
|
subject:
|
||||||
|
'[Support-Salix] ' +
|
||||||
|
additionalData?.frontPath + ' ' +
|
||||||
|
subjectReason?.name + ':' +
|
||||||
|
subjectReason?.message,
|
||||||
|
html
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function tryParse(value) {
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
value = JSON.parse(value);
|
||||||
|
} catch {}
|
||||||
|
return JSON.stringify(value, null, ' ').split('\n').join('<br>');
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const {models} = require('vn-loopback/server/server');
|
||||||
const LoopBackContext = require('loopback-context');
|
const LoopBackContext = require('loopback-context');
|
||||||
|
|
||||||
describe('getStarredModules()', () => {
|
describe('getStarredModules()', () => {
|
||||||
|
@ -19,13 +19,13 @@ describe('getStarredModules()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should return the starred modules for a given user`, async() => {
|
it(`should return the starred modules for a given user`, async() => {
|
||||||
const newStarred = await app.models.StarredModule.create({workerFk: 9, moduleFk: 'Clients', position: 1});
|
const newStarred = await models.StarredModule.create({workerFk: 9, moduleFk: 'customer', position: 1});
|
||||||
const starredModules = await app.models.StarredModule.getStarredModules(ctx);
|
const starredModules = await models.StarredModule.getStarredModules(ctx);
|
||||||
|
|
||||||
expect(starredModules.length).toEqual(1);
|
expect(starredModules.length).toEqual(1);
|
||||||
expect(starredModules[0].moduleFk).toEqual('Clients');
|
expect(starredModules[0].moduleFk).toEqual('customer');
|
||||||
|
|
||||||
// restores
|
// restores
|
||||||
await app.models.StarredModule.destroyById(newStarred.id);
|
await models.StarredModule.destroyById(newStarred.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const {models} = require('vn-loopback/server/server');
|
||||||
const LoopBackContext = require('loopback-context');
|
const LoopBackContext = require('loopback-context');
|
||||||
|
|
||||||
describe('setPosition()', () => {
|
describe('setPosition()', () => {
|
||||||
|
@ -21,35 +21,35 @@ describe('setPosition()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should increase the orders module position by replacing it with clients and vice versa', async() => {
|
it('should increase the orders module position by replacing it with clients and vice versa', async() => {
|
||||||
const tx = await app.models.StarredModule.beginTransaction({});
|
const tx = await models.StarredModule.beginTransaction({});
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
where: {
|
where: {
|
||||||
workerFk: ctx.req.accessToken.userId,
|
workerFk: ctx.req.accessToken.userId,
|
||||||
moduleFk: 'Orders'
|
moduleFk: 'order'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = {transaction: tx};
|
const options = {transaction: tx};
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'order', options);
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'customer', options);
|
||||||
|
|
||||||
let orders = await app.models.StarredModule.findOne(filter, options);
|
let orders = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Clients';
|
filter.where.moduleFk = 'customer';
|
||||||
let clients = await app.models.StarredModule.findOne(filter, options);
|
let clients = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
expect(orders.position).toEqual(1);
|
expect(orders.position).toEqual(1);
|
||||||
expect(clients.position).toEqual(2);
|
expect(clients.position).toEqual(2);
|
||||||
|
|
||||||
await app.models.StarredModule.setPosition(ctx, 'Clients', 'left', options);
|
await models.StarredModule.setPosition(ctx, 'customer', 'left', options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Clients';
|
filter.where.moduleFk = 'customer';
|
||||||
clients = await app.models.StarredModule.findOne(filter, options);
|
clients = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Orders';
|
filter.where.moduleFk = 'order';
|
||||||
orders = await app.models.StarredModule.findOne(filter, options);
|
orders = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
expect(clients.position).toEqual(1);
|
expect(clients.position).toEqual(1);
|
||||||
expect(orders.position).toEqual(2);
|
expect(orders.position).toEqual(2);
|
||||||
|
@ -62,35 +62,35 @@ describe('setPosition()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should decrease the orders module position by replacing it with clients and vice versa', async() => {
|
it('should decrease the orders module position by replacing it with clients and vice versa', async() => {
|
||||||
const tx = await app.models.StarredModule.beginTransaction({});
|
const tx = await models.StarredModule.beginTransaction({});
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
where: {
|
where: {
|
||||||
workerFk: ctx.req.accessToken.userId,
|
workerFk: ctx.req.accessToken.userId,
|
||||||
moduleFk: 'Orders'
|
moduleFk: 'order'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = {transaction: tx};
|
const options = {transaction: tx};
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'order', options);
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'customer', options);
|
||||||
|
|
||||||
let orders = await app.models.StarredModule.findOne(filter, options);
|
let orders = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Clients';
|
filter.where.moduleFk = 'customer';
|
||||||
let clients = await app.models.StarredModule.findOne(filter, options);
|
let clients = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
expect(orders.position).toEqual(1);
|
expect(orders.position).toEqual(1);
|
||||||
expect(clients.position).toEqual(2);
|
expect(clients.position).toEqual(2);
|
||||||
|
|
||||||
await app.models.StarredModule.setPosition(ctx, 'Orders', 'right', options);
|
await models.StarredModule.setPosition(ctx, 'order', 'right', options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Orders';
|
filter.where.moduleFk = 'order';
|
||||||
orders = await app.models.StarredModule.findOne(filter, options);
|
orders = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Clients';
|
filter.where.moduleFk = 'customer';
|
||||||
clients = await app.models.StarredModule.findOne(filter, options);
|
clients = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
expect(orders.position).toEqual(2);
|
expect(orders.position).toEqual(2);
|
||||||
expect(clients.position).toEqual(1);
|
expect(clients.position).toEqual(1);
|
||||||
|
@ -103,41 +103,41 @@ describe('setPosition()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should switch two modules after adding and deleting several modules', async() => {
|
it('should switch two modules after adding and deleting several modules', async() => {
|
||||||
const tx = await app.models.StarredModule.beginTransaction({});
|
const tx = await models.StarredModule.beginTransaction({});
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
where: {
|
where: {
|
||||||
workerFk: ctx.req.accessToken.userId,
|
workerFk: ctx.req.accessToken.userId,
|
||||||
moduleFk: 'Items'
|
moduleFk: 'item'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = {transaction: tx};
|
const options = {transaction: tx};
|
||||||
|
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'customer', options);
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'order', options);
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'customer', options);
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'order', options);
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Items', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'item', options);
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Claims', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'claim', options);
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'customer', options);
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'order', options);
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Zones', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'zone', options);
|
||||||
|
|
||||||
const items = await app.models.StarredModule.findOne(filter, options);
|
const items = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Claims';
|
filter.where.moduleFk = 'claim';
|
||||||
const claims = await app.models.StarredModule.findOne(filter, options);
|
const claims = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Clients';
|
filter.where.moduleFk = 'customer';
|
||||||
let clients = await app.models.StarredModule.findOne(filter, options);
|
let clients = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Orders';
|
filter.where.moduleFk = 'order';
|
||||||
let orders = await app.models.StarredModule.findOne(filter, options);
|
let orders = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Zones';
|
filter.where.moduleFk = 'zone';
|
||||||
const zones = await app.models.StarredModule.findOne(filter, options);
|
const zones = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
expect(items.position).toEqual(1);
|
expect(items.position).toEqual(1);
|
||||||
expect(claims.position).toEqual(2);
|
expect(claims.position).toEqual(2);
|
||||||
|
@ -145,13 +145,13 @@ describe('setPosition()', () => {
|
||||||
expect(orders.position).toEqual(4);
|
expect(orders.position).toEqual(4);
|
||||||
expect(zones.position).toEqual(5);
|
expect(zones.position).toEqual(5);
|
||||||
|
|
||||||
await app.models.StarredModule.setPosition(ctx, 'Clients', 'right', options);
|
await models.StarredModule.setPosition(ctx, 'customer', 'right', options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Orders';
|
filter.where.moduleFk = 'order';
|
||||||
orders = await app.models.StarredModule.findOne(filter, options);
|
orders = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Clients';
|
filter.where.moduleFk = 'customer';
|
||||||
clients = await app.models.StarredModule.findOne(filter, options);
|
clients = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
expect(orders.position).toEqual(3);
|
expect(orders.position).toEqual(3);
|
||||||
expect(clients.position).toEqual(4);
|
expect(clients.position).toEqual(4);
|
||||||
|
@ -164,37 +164,37 @@ describe('setPosition()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should switch two modules after adding and deleting a module between them', async() => {
|
it('should switch two modules after adding and deleting a module between them', async() => {
|
||||||
const tx = await app.models.StarredModule.beginTransaction({});
|
const tx = await models.StarredModule.beginTransaction({});
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
where: {
|
where: {
|
||||||
workerFk: ctx.req.accessToken.userId,
|
workerFk: ctx.req.accessToken.userId,
|
||||||
moduleFk: 'Items'
|
moduleFk: 'item'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = {transaction: tx};
|
const options = {transaction: tx};
|
||||||
|
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Items', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'item', options);
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'customer', options);
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Claims', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'claim', options);
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'order', options);
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Zones', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'zone', options);
|
||||||
|
|
||||||
const items = await app.models.StarredModule.findOne(filter, options);
|
const items = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Clients';
|
filter.where.moduleFk = 'customer';
|
||||||
let clients = await app.models.StarredModule.findOne(filter, options);
|
let clients = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Claims';
|
filter.where.moduleFk = 'claim';
|
||||||
const claims = await app.models.StarredModule.findOne(filter, options);
|
const claims = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Orders';
|
filter.where.moduleFk = 'order';
|
||||||
let orders = await app.models.StarredModule.findOne(filter, options);
|
let orders = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Zones';
|
filter.where.moduleFk = 'zone';
|
||||||
const zones = await app.models.StarredModule.findOne(filter, options);
|
const zones = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
expect(items.position).toEqual(1);
|
expect(items.position).toEqual(1);
|
||||||
expect(clients.position).toEqual(2);
|
expect(clients.position).toEqual(2);
|
||||||
|
@ -202,14 +202,14 @@ describe('setPosition()', () => {
|
||||||
expect(orders.position).toEqual(4);
|
expect(orders.position).toEqual(4);
|
||||||
expect(zones.position).toEqual(5);
|
expect(zones.position).toEqual(5);
|
||||||
|
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Claims', options);
|
await models.StarredModule.toggleStarredModule(ctx, 'claim', options);
|
||||||
await app.models.StarredModule.setPosition(ctx, 'Clients', 'right', options);
|
await models.StarredModule.setPosition(ctx, 'customer', 'right', options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Clients';
|
filter.where.moduleFk = 'customer';
|
||||||
clients = await app.models.StarredModule.findOne(filter, options);
|
clients = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
filter.where.moduleFk = 'Orders';
|
filter.where.moduleFk = 'order';
|
||||||
orders = await app.models.StarredModule.findOne(filter, options);
|
orders = await models.StarredModule.findOne(filter, options);
|
||||||
|
|
||||||
expect(orders.position).toEqual(2);
|
expect(orders.position).toEqual(2);
|
||||||
expect(clients.position).toEqual(4);
|
expect(clients.position).toEqual(4);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const {models} = require('vn-loopback/server/server');
|
||||||
const LoopBackContext = require('loopback-context');
|
const LoopBackContext = require('loopback-context');
|
||||||
|
|
||||||
describe('toggleStarredModule()', () => {
|
describe('toggleStarredModule()', () => {
|
||||||
|
@ -21,16 +21,16 @@ describe('toggleStarredModule()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a new starred module and then remove it by calling the method again with same args', async() => {
|
it('should create a new starred module and then remove it by calling the method again with same args', async() => {
|
||||||
const starredModule = await app.models.StarredModule.toggleStarredModule(ctx, 'Orders');
|
const starredModule = await models.StarredModule.toggleStarredModule(ctx, 'order');
|
||||||
let starredModules = await app.models.StarredModule.getStarredModules(ctx);
|
let starredModules = await models.StarredModule.getStarredModules(ctx);
|
||||||
|
|
||||||
expect(starredModules.length).toEqual(1);
|
expect(starredModules.length).toEqual(1);
|
||||||
expect(starredModule.moduleFk).toEqual('Orders');
|
expect(starredModule.moduleFk).toEqual('order');
|
||||||
expect(starredModule.workerFk).toEqual(activeCtx.accessToken.userId);
|
expect(starredModule.workerFk).toEqual(activeCtx.accessToken.userId);
|
||||||
expect(starredModule.position).toEqual(starredModules.length);
|
expect(starredModule.position).toEqual(starredModules.length);
|
||||||
|
|
||||||
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders');
|
await models.StarredModule.toggleStarredModule(ctx, 'order');
|
||||||
starredModules = await app.models.StarredModule.getStarredModules(ctx);
|
starredModules = await models.StarredModule.getStarredModules(ctx);
|
||||||
|
|
||||||
expect(starredModules.length).toEqual(0);
|
expect(starredModules.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
const axios = require('axios');
|
||||||
|
const {DOMParser} = require('xmldom');
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethod('internationalExpedition', {
|
||||||
|
description: 'Create an expedition and return a label',
|
||||||
|
accessType: 'WRITE',
|
||||||
|
accepts: [{
|
||||||
|
arg: 'expeditionFk',
|
||||||
|
type: 'number',
|
||||||
|
required: true
|
||||||
|
}],
|
||||||
|
returns: {
|
||||||
|
type: ['object'],
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/internationalExpedition`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.internationalExpedition = async expeditionFk => {
|
||||||
|
const models = Self.app.models;
|
||||||
|
|
||||||
|
const viaexpressConfig = await models.ViaexpressConfig.findOne({
|
||||||
|
fields: ['url']
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderedXml = await models.ViaexpressConfig.renderer(expeditionFk);
|
||||||
|
const response = await axios.post(`${viaexpressConfig.url}ServicioVxClientes.asmx`, renderedXml, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/soap+xml; charset=utf-8'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const xmlString = response.data;
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
|
||||||
|
const referenciaVxElement = xmlDoc.getElementsByTagName('ReferenciaVx')[0];
|
||||||
|
const referenciaVx = referenciaVxElement.textContent;
|
||||||
|
|
||||||
|
return referenciaVx;
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,126 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const ejs = require('ejs');
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethod('renderer', {
|
||||||
|
description: 'Renders the data from an XML',
|
||||||
|
accessType: 'READ',
|
||||||
|
accepts: [{
|
||||||
|
arg: 'expeditionFk',
|
||||||
|
type: 'number',
|
||||||
|
required: true
|
||||||
|
}],
|
||||||
|
returns: {
|
||||||
|
type: ['object'],
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/renderer`,
|
||||||
|
verb: 'GET'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.renderer = async expeditionFk => {
|
||||||
|
const models = Self.app.models;
|
||||||
|
|
||||||
|
const viaexpressConfig = await models.ViaexpressConfig.findOne({
|
||||||
|
fields: ['client', 'user', 'password', 'defaultWeight', 'deliveryType']
|
||||||
|
});
|
||||||
|
|
||||||
|
const expedition = await models.Expedition.findOne({
|
||||||
|
fields: ['id', 'ticketFk'],
|
||||||
|
where: {id: expeditionFk},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
relation: 'ticket',
|
||||||
|
scope: {
|
||||||
|
fields: ['shipped', 'addressFk', 'clientFk', 'companyFk'],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
relation: 'client',
|
||||||
|
scope: {
|
||||||
|
fields: ['mobile', 'phone', 'email']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relation: 'address',
|
||||||
|
scope: {
|
||||||
|
fields: [
|
||||||
|
'nickname',
|
||||||
|
'street',
|
||||||
|
'postalCode',
|
||||||
|
'city',
|
||||||
|
'mobile',
|
||||||
|
'phone',
|
||||||
|
'provinceFk'
|
||||||
|
],
|
||||||
|
include: {
|
||||||
|
relation: 'province',
|
||||||
|
scope: {
|
||||||
|
fields: ['name', 'countryFk'],
|
||||||
|
include: {
|
||||||
|
relation: 'country',
|
||||||
|
scope: {
|
||||||
|
fields: ['code'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relation: 'company',
|
||||||
|
scope: {
|
||||||
|
fields: ['clientFk'],
|
||||||
|
include: {
|
||||||
|
relation: 'client',
|
||||||
|
scope: {
|
||||||
|
fields: ['socialName', 'mobile', 'phone', 'email', 'defaultAddressFk'],
|
||||||
|
include: {
|
||||||
|
relation: 'defaultAddress',
|
||||||
|
scope: {
|
||||||
|
fields: [
|
||||||
|
'street',
|
||||||
|
'postalCode',
|
||||||
|
'city',
|
||||||
|
'mobile',
|
||||||
|
'phone',
|
||||||
|
'provinceFk'
|
||||||
|
],
|
||||||
|
include: {
|
||||||
|
relation: 'province',
|
||||||
|
scope: {
|
||||||
|
fields: ['name']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const ticket = expedition.ticket();
|
||||||
|
const sender = ticket.company().client();
|
||||||
|
const shipped = ticket.shipped.toISOString();
|
||||||
|
const data = {
|
||||||
|
viaexpressConfig,
|
||||||
|
sender,
|
||||||
|
senderAddress: sender.defaultAddress(),
|
||||||
|
client: ticket.client(),
|
||||||
|
address: ticket.address(),
|
||||||
|
shipped
|
||||||
|
};
|
||||||
|
|
||||||
|
const template = fs.readFileSync(__dirname + '/template.ejs', 'utf-8');
|
||||||
|
const renderedXml = ejs.render(template, data);
|
||||||
|
return renderedXml;
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
|
||||||
|
<soap12:Body>
|
||||||
|
<PutExpedicionInternacional xmlns="http://82.223.6.71:82">
|
||||||
|
<ObjetoEnvio>
|
||||||
|
<Peso><%= viaexpressConfig.defaultWeight %></Peso>
|
||||||
|
<Bultos>1</Bultos>
|
||||||
|
<Reembolso>0</Reembolso>
|
||||||
|
<Fecha><%= shipped %></Fecha>
|
||||||
|
<ConRetorno>0</ConRetorno>
|
||||||
|
<Tipo><%= viaexpressConfig.deliveryType %></Tipo>
|
||||||
|
<Debidos>0</Debidos>
|
||||||
|
<Asegurado>0</Asegurado>
|
||||||
|
<Imprimir>0</Imprimir>
|
||||||
|
<ConDevolucionAlbaran>0</ConDevolucionAlbaran>
|
||||||
|
<Intradia>0</Intradia>
|
||||||
|
<Observaciones></Observaciones>
|
||||||
|
<AlbaranRemitente></AlbaranRemitente>
|
||||||
|
<Modo>0</Modo>
|
||||||
|
<TextoAgencia></TextoAgencia>
|
||||||
|
<Terminal></Terminal>
|
||||||
|
<ObjetoRemitente>
|
||||||
|
<RazonSocial><%= sender.socialName %></RazonSocial>
|
||||||
|
<Domicilio><%= senderAddress.street %></Domicilio>
|
||||||
|
<Cpostal><%= senderAddress.postalCode %></Cpostal>
|
||||||
|
<Poblacion><%= senderAddress.city %></Poblacion>
|
||||||
|
<Provincia><%= senderAddress.province().name %></Provincia>
|
||||||
|
<Contacto></Contacto>
|
||||||
|
<Telefono><%= senderAddress.mobile || senderAddress.phone || sender.mobile || sender.phone %></Telefono>
|
||||||
|
<Email><%= sender.email %></Email>
|
||||||
|
</ObjetoRemitente>
|
||||||
|
<ObjetoDestinatario>
|
||||||
|
<RazonSocial><%= address.nickname %></RazonSocial>
|
||||||
|
<Domicilio><%= address.street %></Domicilio>
|
||||||
|
<Cpostal><%= address.postalCode %></Cpostal>
|
||||||
|
<Poblacion><%= address.city %></Poblacion>
|
||||||
|
<Municipio></Municipio>
|
||||||
|
<Provincia><%= address.province().name %></Provincia>
|
||||||
|
<Contacto></Contacto>
|
||||||
|
<Telefono><%= address.mobile || address.phone || client.mobile || client.phone %></Telefono>
|
||||||
|
<Email><%= client.email %></Email>
|
||||||
|
<Pais><%= address.province().country().code %></Pais>
|
||||||
|
</ObjetoDestinatario>
|
||||||
|
<ObjetoLogin>
|
||||||
|
<IdCliente><%= viaexpressConfig.client %></IdCliente>
|
||||||
|
<Usuario><%= viaexpressConfig.user %></Usuario>
|
||||||
|
<Password><%= viaexpressConfig.password %></Password>
|
||||||
|
</ObjetoLogin>
|
||||||
|
</ObjetoEnvio>
|
||||||
|
</PutExpedicionInternacional>
|
||||||
|
</soap12:Body>
|
||||||
|
</soap12:Envelope>
|
|
@ -22,14 +22,8 @@ module.exports = Self => {
|
||||||
let userId = ctx.req.accessToken.userId;
|
let userId = ctx.req.accessToken.userId;
|
||||||
let models = Self.app.models;
|
let models = Self.app.models;
|
||||||
|
|
||||||
let user = await models.Account.findById(userId, {
|
let user = await Self.findById(userId, {
|
||||||
fields: ['id', 'name', 'nickname', 'email', 'lang'],
|
fields: ['id', 'name', 'nickname', 'email', 'lang']
|
||||||
include: {
|
|
||||||
relation: 'userConfig',
|
|
||||||
scope: {
|
|
||||||
fields: ['darkMode']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let roles = await models.RoleMapping.find({
|
let roles = await models.RoleMapping.find({
|
|
@ -0,0 +1,83 @@
|
||||||
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethod('privileges', {
|
||||||
|
description: 'Change role and hasGrant if user has privileges',
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'ctx',
|
||||||
|
type: 'Object',
|
||||||
|
http: {source: 'context'}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'id',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
description: 'The user id',
|
||||||
|
http: {source: 'path'}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'roleFk',
|
||||||
|
type: 'number',
|
||||||
|
description: 'The new role for user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'hasGrant',
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Whether to has grant'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
http: {
|
||||||
|
path: `/:id/privileges`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.privileges = async function(ctx, id, roleFk, hasGrant, options) {
|
||||||
|
if (!(hasGrant != null || roleFk)) return;
|
||||||
|
|
||||||
|
const models = Self.app.models;
|
||||||
|
const userId = ctx.req.accessToken.userId;
|
||||||
|
|
||||||
|
const myOptions = {};
|
||||||
|
|
||||||
|
if (typeof options == 'object')
|
||||||
|
Object.assign(myOptions, options);
|
||||||
|
|
||||||
|
const user = await Self.findById(userId, {fields: ['hasGrant']}, myOptions);
|
||||||
|
|
||||||
|
const userToUpdate = await Self.findById(id, {
|
||||||
|
fields: ['id', 'name', 'hasGrant', 'roleFk', 'password', 'email'],
|
||||||
|
include: {
|
||||||
|
relation: 'role',
|
||||||
|
scope: {
|
||||||
|
fields: ['name']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, myOptions);
|
||||||
|
|
||||||
|
if (!user.hasGrant)
|
||||||
|
throw new UserError(`You don't have grant privilege`);
|
||||||
|
|
||||||
|
const hasRoleFromUser = await Self.hasRole(userId, userToUpdate.role().name, myOptions);
|
||||||
|
|
||||||
|
if (!hasRoleFromUser)
|
||||||
|
throw new UserError(`You don't own the role and you can't assign it to another user`);
|
||||||
|
|
||||||
|
if (hasGrant != null)
|
||||||
|
userToUpdate.hasGrant = hasGrant;
|
||||||
|
|
||||||
|
if (roleFk) {
|
||||||
|
const role = await models.Role.findById(roleFk, {fields: ['name']}, myOptions);
|
||||||
|
const hasRole = await Self.hasRole(userId, role.name, myOptions);
|
||||||
|
|
||||||
|
if (!hasRole)
|
||||||
|
throw new UserError(`You don't own the role and you can't assign it to another user`);
|
||||||
|
|
||||||
|
userToUpdate.roleFk = roleFk;
|
||||||
|
}
|
||||||
|
|
||||||
|
await userToUpdate.save(userToUpdate);
|
||||||
|
await models.Account.sync(userToUpdate.name);
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,45 @@
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethod('recoverPassword', {
|
||||||
|
description: 'Send email to the user',
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'user',
|
||||||
|
type: 'string',
|
||||||
|
description: 'The user name or email',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'app',
|
||||||
|
type: 'string',
|
||||||
|
description: 'The directory for mail'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
http: {
|
||||||
|
path: `/recoverPassword`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.recoverPassword = async function(user, app) {
|
||||||
|
const models = Self.app.models;
|
||||||
|
|
||||||
|
const usesEmail = user.indexOf('@') !== -1;
|
||||||
|
if (!usesEmail) {
|
||||||
|
const account = await models.VnUser.findOne({
|
||||||
|
fields: ['email'],
|
||||||
|
where: {name: user}
|
||||||
|
});
|
||||||
|
if (!account) return;
|
||||||
|
user = account.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Self.resetPassword({email: user, emailTemplate: 'recover-password', app});
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'EMAIL_NOT_FOUND')
|
||||||
|
return;
|
||||||
|
else
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,38 @@
|
||||||
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethodCtx('renewToken', {
|
||||||
|
description: 'Checks if the token has more than renewPeriod seconds to live and if so, renews it',
|
||||||
|
accessType: 'WRITE',
|
||||||
|
accepts: [],
|
||||||
|
returns: {
|
||||||
|
type: 'Object',
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/renewToken`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.renewToken = async function(ctx) {
|
||||||
|
const models = Self.app.models;
|
||||||
|
const token = ctx.req.accessToken;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const differenceMilliseconds = now - token.created;
|
||||||
|
const differenceSeconds = Math.floor(differenceMilliseconds / 1000);
|
||||||
|
|
||||||
|
const fields = ['renewPeriod', 'courtesyTime'];
|
||||||
|
const accessTokenConfig = await models.AccessTokenConfig.findOne({fields});
|
||||||
|
|
||||||
|
if (differenceSeconds < accessTokenConfig.renewPeriod - accessTokenConfig.courtesyTime)
|
||||||
|
throw new UserError(`The renew period has not been exceeded`, 'periodNotExceeded');
|
||||||
|
|
||||||
|
await Self.logout(token.id);
|
||||||
|
const user = await Self.findById(token.userId);
|
||||||
|
const accessToken = await user.createAccessToken();
|
||||||
|
|
||||||
|
return {id: accessToken.id, ttl: accessToken.ttl};
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,96 @@
|
||||||
|
const ForbiddenError = require('vn-loopback/util/forbiddenError');
|
||||||
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethodCtx('signIn', {
|
||||||
|
description: 'Login a user with username/email and password',
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'user',
|
||||||
|
type: 'String',
|
||||||
|
description: 'The user name or email',
|
||||||
|
required: true
|
||||||
|
}, {
|
||||||
|
arg: 'password',
|
||||||
|
type: 'String',
|
||||||
|
description: 'The password'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
returns: {
|
||||||
|
type: 'object',
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/sign-in`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.signIn = async function(ctx, user, password, options) {
|
||||||
|
const myOptions = {};
|
||||||
|
if (typeof options == 'object')
|
||||||
|
Object.assign(myOptions, options);
|
||||||
|
|
||||||
|
const where = Self.userUses(user);
|
||||||
|
const vnUser = await Self.findOne({
|
||||||
|
fields: ['id', 'name', 'password', 'active', 'email', 'passExpired', 'twoFactor'],
|
||||||
|
where
|
||||||
|
}, myOptions);
|
||||||
|
|
||||||
|
const validCredentials = vnUser
|
||||||
|
&& await vnUser.hasPassword(password);
|
||||||
|
|
||||||
|
if (validCredentials) {
|
||||||
|
if (!vnUser.active)
|
||||||
|
throw new UserError('User disabled');
|
||||||
|
await Self.sendTwoFactor(ctx, vnUser, myOptions);
|
||||||
|
await Self.passExpired(vnUser, myOptions);
|
||||||
|
|
||||||
|
if (vnUser.twoFactor)
|
||||||
|
throw new ForbiddenError(null, 'REQUIRES_2FA');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Self.validateLogin(user, password);
|
||||||
|
};
|
||||||
|
|
||||||
|
Self.passExpired = async vnUser => {
|
||||||
|
const today = Date.vnNew();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
if (vnUser.passExpired && vnUser.passExpired.getTime() <= today.getTime()) {
|
||||||
|
const err = new UserError('Pass expired', 'passExpired');
|
||||||
|
err.details = {userId: vnUser.id, twoFactor: vnUser.twoFactor ? true : false};
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Self.sendTwoFactor = async(ctx, vnUser, myOptions) => {
|
||||||
|
if (vnUser.twoFactor === 'email') {
|
||||||
|
const $ = Self.app.models;
|
||||||
|
|
||||||
|
const code = String(Math.floor(Math.random() * 999999));
|
||||||
|
const maxTTL = ((60 * 1000) * 5); // 5 min
|
||||||
|
await $.AuthCode.upsertWithWhere({userFk: vnUser.id}, {
|
||||||
|
userFk: vnUser.id,
|
||||||
|
code: code,
|
||||||
|
expires: Date.vnNow() + maxTTL
|
||||||
|
}, myOptions);
|
||||||
|
|
||||||
|
const headers = ctx.req.headers;
|
||||||
|
const platform = headers['sec-ch-ua-platform']?.replace(/['"=]+/g, '');
|
||||||
|
const browser = headers['sec-ch-ua']?.replace(/['"=]+/g, '');
|
||||||
|
const params = {
|
||||||
|
args: {
|
||||||
|
recipientId: vnUser.id,
|
||||||
|
recipient: vnUser.email,
|
||||||
|
code: code,
|
||||||
|
ip: ctx.req?.connection?.remoteAddress,
|
||||||
|
device: platform && browser ? platform + ', ' + browser : headers['user-agent'],
|
||||||
|
},
|
||||||
|
req: {getLocale: ctx.req.getLocale},
|
||||||
|
};
|
||||||
|
|
||||||
|
await Self.sendTemplate(params, 'auth-code', true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,20 +1,22 @@
|
||||||
const models = require('vn-loopback/server/server').models;
|
const models = require('vn-loopback/server/server').models;
|
||||||
|
|
||||||
describe('account privileges()', () => {
|
describe('VnUser privileges()', () => {
|
||||||
const employeeId = 1;
|
const employeeId = 1;
|
||||||
const developerId = 9;
|
const developerId = 9;
|
||||||
const sysadminId = 66;
|
const sysadminId = 66;
|
||||||
const bruceWayneId = 1101;
|
const itBossId = 104;
|
||||||
|
const rootId = 100;
|
||||||
|
const clarkKent = 1103;
|
||||||
|
|
||||||
it('should throw an error when user not has privileges', async() => {
|
it('should throw an error when user not has privileges', async() => {
|
||||||
const ctx = {req: {accessToken: {userId: developerId}}};
|
const ctx = {req: {accessToken: {userId: developerId}}};
|
||||||
const tx = await models.Account.beginTransaction({});
|
const tx = await models.VnUser.beginTransaction({});
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
try {
|
try {
|
||||||
const options = {transaction: tx};
|
const options = {transaction: tx};
|
||||||
|
|
||||||
await models.Account.privileges(ctx, employeeId, null, true, options);
|
await models.VnUser.privileges(ctx, employeeId, null, true, options);
|
||||||
|
|
||||||
await tx.rollback();
|
await tx.rollback();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -22,23 +24,18 @@ describe('account privileges()', () => {
|
||||||
await tx.rollback();
|
await tx.rollback();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(error.message).toContain(`You don't have enough privileges`);
|
expect(error.message).toContain(`You don't have grant privilege`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error when user has privileges but not has the role', async() => {
|
it('should throw an error when user has privileges but not has the role', async() => {
|
||||||
const ctx = {req: {accessToken: {userId: sysadminId}}};
|
const ctx = {req: {accessToken: {userId: sysadminId}}};
|
||||||
const tx = await models.Account.beginTransaction({});
|
const tx = await models.VnUser.beginTransaction({});
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
try {
|
try {
|
||||||
const options = {transaction: tx};
|
const options = {transaction: tx};
|
||||||
|
|
||||||
const root = await models.Role.findOne({
|
await models.VnUser.privileges(ctx, employeeId, rootId, null, options);
|
||||||
where: {
|
|
||||||
name: 'root'
|
|
||||||
}
|
|
||||||
}, options);
|
|
||||||
await models.Account.privileges(ctx, employeeId, root.id, null, options);
|
|
||||||
|
|
||||||
await tx.rollback();
|
await tx.rollback();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -46,12 +43,31 @@ describe('account privileges()', () => {
|
||||||
await tx.rollback();
|
await tx.rollback();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(error.message).toContain(`You don't have enough privileges`);
|
expect(error.message).toContain(`You don't own the role and you can't assign it to another user`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when user has privileges but not has the role from user', async() => {
|
||||||
|
const ctx = {req: {accessToken: {userId: sysadminId}}};
|
||||||
|
const tx = await models.VnUser.beginTransaction({});
|
||||||
|
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
await models.VnUser.privileges(ctx, itBossId, developerId, null, options);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
await tx.rollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error.message).toContain(`You don't own the role and you can't assign it to another user`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change role', async() => {
|
it('should change role', async() => {
|
||||||
const ctx = {req: {accessToken: {userId: sysadminId}}};
|
const ctx = {req: {accessToken: {userId: sysadminId}}};
|
||||||
const tx = await models.Account.beginTransaction({});
|
const tx = await models.VnUser.beginTransaction({});
|
||||||
|
|
||||||
const options = {transaction: tx};
|
const options = {transaction: tx};
|
||||||
const agency = await models.Role.findOne({
|
const agency = await models.Role.findOne({
|
||||||
|
@ -63,8 +79,8 @@ describe('account privileges()', () => {
|
||||||
let error;
|
let error;
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
await models.Account.privileges(ctx, bruceWayneId, agency.id, null, options);
|
await models.VnUser.privileges(ctx, clarkKent, agency.id, null, options);
|
||||||
result = await models.Account.findById(bruceWayneId, null, options);
|
result = await models.VnUser.findById(clarkKent, null, options);
|
||||||
|
|
||||||
await tx.rollback();
|
await tx.rollback();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -78,14 +94,14 @@ describe('account privileges()', () => {
|
||||||
|
|
||||||
it('should change hasGrant', async() => {
|
it('should change hasGrant', async() => {
|
||||||
const ctx = {req: {accessToken: {userId: sysadminId}}};
|
const ctx = {req: {accessToken: {userId: sysadminId}}};
|
||||||
const tx = await models.Account.beginTransaction({});
|
const tx = await models.VnUser.beginTransaction({});
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
const options = {transaction: tx};
|
const options = {transaction: tx};
|
||||||
await models.Account.privileges(ctx, bruceWayneId, null, true, options);
|
await models.VnUser.privileges(ctx, clarkKent, null, true, options);
|
||||||
result = await models.Account.findById(bruceWayneId, null, options);
|
result = await models.VnUser.findById(clarkKent, null, options);
|
||||||
|
|
||||||
await tx.rollback();
|
await tx.rollback();
|
||||||
} catch (e) {
|
} catch (e) {
|
|
@ -0,0 +1,101 @@
|
||||||
|
const {models} = require('vn-loopback/server/server');
|
||||||
|
|
||||||
|
describe('VnUser Sign-in()', () => {
|
||||||
|
const employeeId = 1;
|
||||||
|
const unauthCtx = {
|
||||||
|
req: {
|
||||||
|
headers: {},
|
||||||
|
connection: {
|
||||||
|
remoteAddress: '127.0.0.1'
|
||||||
|
},
|
||||||
|
getLocale: () => 'en'
|
||||||
|
},
|
||||||
|
args: {}
|
||||||
|
};
|
||||||
|
const {VnUser, AccessToken} = models;
|
||||||
|
describe('when credentials are correct', () => {
|
||||||
|
it('should return the token', async() => {
|
||||||
|
let login = await VnUser.signIn(unauthCtx, 'salesAssistant', 'nightmare');
|
||||||
|
let accessToken = await AccessToken.findById(login.token);
|
||||||
|
let ctx = {req: {accessToken: accessToken}};
|
||||||
|
|
||||||
|
expect(login.token).toBeDefined();
|
||||||
|
|
||||||
|
await VnUser.logout(ctx.req.accessToken.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the token if the user doesnt exist but the client does', async() => {
|
||||||
|
let login = await VnUser.signIn(unauthCtx, 'PetterParker', 'nightmare');
|
||||||
|
let accessToken = await AccessToken.findById(login.token);
|
||||||
|
let ctx = {req: {accessToken: accessToken}};
|
||||||
|
|
||||||
|
expect(login.token).toBeDefined();
|
||||||
|
|
||||||
|
await VnUser.logout(ctx.req.accessToken.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when credentials are incorrect', () => {
|
||||||
|
it('should throw a 401 error', async() => {
|
||||||
|
let error;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await VnUser.signIn(unauthCtx, 'IDontExist', 'TotallyWrongPassword');
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.statusCode).toBe(401);
|
||||||
|
expect(error.code).toBe('LOGIN_FAILED');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when two-factor auth is required', () => {
|
||||||
|
it('should throw a 403 error', async() => {
|
||||||
|
const employee = await VnUser.findById(employeeId);
|
||||||
|
const tx = await VnUser.beginTransaction({});
|
||||||
|
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
await employee.updateAttribute('twoFactor', 'email', options);
|
||||||
|
|
||||||
|
await VnUser.signIn(unauthCtx, 'employee', 'nightmare', options);
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.statusCode).toBe(403);
|
||||||
|
expect(error.code).toBe('REQUIRES_2FA');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when passExpired', () => {
|
||||||
|
it('should throw a passExpired error', async() => {
|
||||||
|
const tx = await VnUser.beginTransaction({});
|
||||||
|
const employee = await VnUser.findById(employeeId);
|
||||||
|
const yesterday = Date.vnNew();
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
await employee.updateAttribute('passExpired', yesterday, options);
|
||||||
|
|
||||||
|
await VnUser.signIn(unauthCtx, 'employee', 'nightmare', options);
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.statusCode).toBe(400);
|
||||||
|
expect(error.message).toBe('Pass expired');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,52 @@
|
||||||
|
const {models} = require('vn-loopback/server/server');
|
||||||
|
|
||||||
|
describe('VnUser validate-auth()', () => {
|
||||||
|
describe('validateAuth', () => {
|
||||||
|
it('should signin if data is correct', async() => {
|
||||||
|
await models.AuthCode.create({
|
||||||
|
userFk: 9,
|
||||||
|
code: '555555',
|
||||||
|
expires: Date.vnNow() + (60 * 1000)
|
||||||
|
});
|
||||||
|
const token = await models.VnUser.validateAuth('developer', 'nightmare', '555555');
|
||||||
|
|
||||||
|
expect(token.token).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateCode', () => {
|
||||||
|
it('should throw an error for a non existent code', async() => {
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await models.VnUser.validateCode('developer', '123456');
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.statusCode).toBe(400);
|
||||||
|
expect(error.message).toEqual('Invalid or expired verification code');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when a code doesn`t match the login username', async() => {
|
||||||
|
let error;
|
||||||
|
let authCode;
|
||||||
|
try {
|
||||||
|
authCode = await models.AuthCode.create({
|
||||||
|
userFk: 1,
|
||||||
|
code: '555555',
|
||||||
|
expires: Date.vnNow() + (60 * 1000)
|
||||||
|
});
|
||||||
|
|
||||||
|
await models.VnUser.validateCode('developer', '555555');
|
||||||
|
} catch (e) {
|
||||||
|
authCode && await authCode.destroy();
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.statusCode).toBe(400);
|
||||||
|
expect(error.message).toEqual('Authentication failed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,66 @@
|
||||||
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethod('validateAuth', {
|
||||||
|
description: 'Login a user with username/email and password',
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'user',
|
||||||
|
type: 'String',
|
||||||
|
description: 'The user name or email',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'password',
|
||||||
|
type: 'String',
|
||||||
|
description: 'The password'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'code',
|
||||||
|
type: 'String',
|
||||||
|
description: 'The auth code'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
returns: {
|
||||||
|
type: 'object',
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/validate-auth`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.validateAuth = async(username, password, code, options) => {
|
||||||
|
const myOptions = {};
|
||||||
|
if (typeof options == 'object')
|
||||||
|
Object.assign(myOptions, options);
|
||||||
|
|
||||||
|
const token = Self.validateLogin(username, password);
|
||||||
|
await Self.validateCode(username, code, myOptions);
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
Self.validateCode = async(username, code, myOptions) => {
|
||||||
|
const {AuthCode} = Self.app.models;
|
||||||
|
|
||||||
|
const authCode = await AuthCode.findOne({
|
||||||
|
where: {
|
||||||
|
code: code
|
||||||
|
}
|
||||||
|
}, myOptions);
|
||||||
|
|
||||||
|
const expired = authCode && Date.vnNow() > authCode.expires;
|
||||||
|
if (!authCode || expired)
|
||||||
|
throw new UserError('Invalid or expired verification code');
|
||||||
|
|
||||||
|
const user = await Self.findById(authCode.userFk, {
|
||||||
|
fields: ['name', 'twoFactor']
|
||||||
|
}, myOptions);
|
||||||
|
|
||||||
|
if (user.name !== username)
|
||||||
|
throw new UserError('Authentication failed');
|
||||||
|
|
||||||
|
await authCode.destroy(myOptions);
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,12 +1,23 @@
|
||||||
{
|
{
|
||||||
"Account": {
|
"AccessTokenConfig": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn",
|
||||||
|
"options": {
|
||||||
|
"mysql": {
|
||||||
|
"table": "salix.accessTokenConfig"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"AccountingType": {
|
"AccountingType": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
},
|
},
|
||||||
|
"AuthCode": {
|
||||||
|
"dataSource": "vn"
|
||||||
|
},
|
||||||
"Bank": {
|
"Bank": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
|
},
|
||||||
|
"Buyer": {
|
||||||
|
"dataSource": "vn"
|
||||||
},
|
},
|
||||||
"Campaign": {
|
"Campaign": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
|
@ -77,6 +88,21 @@
|
||||||
"Module": {
|
"Module": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
},
|
},
|
||||||
|
"Notification": {
|
||||||
|
"dataSource": "vn"
|
||||||
|
},
|
||||||
|
"NotificationAcl": {
|
||||||
|
"dataSource": "vn"
|
||||||
|
},
|
||||||
|
"NotificationConfig": {
|
||||||
|
"dataSource": "vn"
|
||||||
|
},
|
||||||
|
"NotificationQueue": {
|
||||||
|
"dataSource": "vn"
|
||||||
|
},
|
||||||
|
"NotificationSubscription": {
|
||||||
|
"dataSource": "vn"
|
||||||
|
},
|
||||||
"Province": {
|
"Province": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
},
|
},
|
||||||
|
@ -101,16 +127,19 @@
|
||||||
"Town": {
|
"Town": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
},
|
},
|
||||||
|
"Url": {
|
||||||
|
"dataSource": "vn"
|
||||||
|
},
|
||||||
"UserConfig": {
|
"UserConfig": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
},
|
},
|
||||||
"UserConfigView": {
|
"UserConfigView": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
},
|
},
|
||||||
"UserLog": {
|
"Warehouse": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
},
|
},
|
||||||
"Warehouse": {
|
"VnUser": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
},
|
},
|
||||||
"OsTicket": {
|
"OsTicket": {
|
||||||
|
@ -124,6 +153,9 @@
|
||||||
},
|
},
|
||||||
"PrintConfig": {
|
"PrintConfig": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
|
},
|
||||||
|
"ViaexpressConfig": {
|
||||||
|
"dataSource": "vn"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "AccessTokenConfig",
|
||||||
|
"base": "VnModel",
|
||||||
|
"options": {
|
||||||
|
"mysql": {
|
||||||
|
"table": "accessTokenConfig"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number",
|
||||||
|
"id": true,
|
||||||
|
"description": "Identifier"
|
||||||
|
},
|
||||||
|
"renewPeriod": {
|
||||||
|
"type": "number",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"courtesyTime": {
|
||||||
|
"type": "number",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"renewInterval": {
|
||||||
|
"type": "number",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"acls": [{
|
||||||
|
"accessType": "READ",
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "ALLOW"
|
||||||
|
}]
|
||||||
|
}
|
|
@ -1,90 +0,0 @@
|
||||||
const md5 = require('md5');
|
|
||||||
|
|
||||||
module.exports = Self => {
|
|
||||||
require('../methods/account/login')(Self);
|
|
||||||
require('../methods/account/logout')(Self);
|
|
||||||
require('../methods/account/acl')(Self);
|
|
||||||
require('../methods/account/change-password')(Self);
|
|
||||||
require('../methods/account/set-password')(Self);
|
|
||||||
require('../methods/account/validate-token')(Self);
|
|
||||||
require('../methods/account/privileges')(Self);
|
|
||||||
|
|
||||||
// Validations
|
|
||||||
|
|
||||||
Self.validatesFormatOf('email', {
|
|
||||||
message: 'Invalid email',
|
|
||||||
allowNull: true,
|
|
||||||
allowBlank: true,
|
|
||||||
with: /^[\w|.|-]+@[\w|-]+(\.[\w|-]+)*(,[\w|.|-]+@[\w|-]+(\.[\w|-]+)*)*$/
|
|
||||||
});
|
|
||||||
|
|
||||||
Self.validatesUniquenessOf('name', {
|
|
||||||
message: `A client with that Web User name already exists`
|
|
||||||
});
|
|
||||||
|
|
||||||
Self.observe('before save', async function(ctx) {
|
|
||||||
if (ctx.currentInstance && ctx.currentInstance.id && ctx.data && ctx.data.password)
|
|
||||||
ctx.data.password = md5(ctx.data.password);
|
|
||||||
});
|
|
||||||
|
|
||||||
Self.remoteMethod('getCurrentUserData', {
|
|
||||||
description: 'Gets the current user data',
|
|
||||||
accepts: [
|
|
||||||
{
|
|
||||||
arg: 'ctx',
|
|
||||||
type: 'Object',
|
|
||||||
http: {source: 'context'}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
returns: {
|
|
||||||
type: 'Object',
|
|
||||||
root: true
|
|
||||||
},
|
|
||||||
http: {
|
|
||||||
verb: 'GET',
|
|
||||||
path: '/getCurrentUserData'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Self.getCurrentUserData = async function(ctx) {
|
|
||||||
let userId = ctx.req.accessToken.userId;
|
|
||||||
return await Self.findById(userId, {
|
|
||||||
fields: ['id', 'name', 'nickname']
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if user has a role.
|
|
||||||
*
|
|
||||||
* @param {Integer} userId The user id
|
|
||||||
* @param {String} name The role name
|
|
||||||
* @param {Object} options Options
|
|
||||||
* @return {Boolean} %true if user has the role, %false otherwise
|
|
||||||
*/
|
|
||||||
Self.hasRole = async function(userId, name, options) {
|
|
||||||
let roles = await Self.getRoles(userId, options);
|
|
||||||
return roles.some(role => role == name);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all user roles.
|
|
||||||
*
|
|
||||||
* @param {Integer} userId The user id
|
|
||||||
* @param {Object} options Options
|
|
||||||
* @return {Object} User role list
|
|
||||||
*/
|
|
||||||
Self.getRoles = async(userId, options) => {
|
|
||||||
let result = await Self.rawSql(
|
|
||||||
`SELECT r.name
|
|
||||||
FROM account.user u
|
|
||||||
JOIN account.roleRole rr ON rr.role = u.role
|
|
||||||
JOIN account.role r ON r.id = rr.inheritsFrom
|
|
||||||
WHERE u.id = ?`, [userId], options);
|
|
||||||
|
|
||||||
let roles = [];
|
|
||||||
for (role of result)
|
|
||||||
roles.push(role.name);
|
|
||||||
|
|
||||||
return roles;
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "AuthCode",
|
||||||
|
"base": "VnModel",
|
||||||
|
"options": {
|
||||||
|
"mysql": {
|
||||||
|
"table": "salix.authCode"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"userFk": {
|
||||||
|
"type": "number",
|
||||||
|
"required": true,
|
||||||
|
"id": true
|
||||||
|
},
|
||||||
|
"code": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"expires": {
|
||||||
|
"type": "number",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"user": {
|
||||||
|
"type": "belongsTo",
|
||||||
|
"model": "Account",
|
||||||
|
"foreignKey": "userFk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "Buyer",
|
||||||
|
"base": "VnModel",
|
||||||
|
"options": {
|
||||||
|
"mysql": {
|
||||||
|
"table": "buyer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"userFk": {
|
||||||
|
"type": "number",
|
||||||
|
"required": true,
|
||||||
|
"id": true
|
||||||
|
},
|
||||||
|
"nickname": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"acls": [
|
||||||
|
{
|
||||||
|
"accessType": "READ",
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "employee",
|
||||||
|
"permission": "ALLOW"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -4,4 +4,23 @@ module.exports = Self => {
|
||||||
require('../methods/chat/sendCheckingPresence')(Self);
|
require('../methods/chat/sendCheckingPresence')(Self);
|
||||||
require('../methods/chat/notifyIssues')(Self);
|
require('../methods/chat/notifyIssues')(Self);
|
||||||
require('../methods/chat/sendQueued')(Self);
|
require('../methods/chat/sendQueued')(Self);
|
||||||
|
|
||||||
|
Self.observe('before save', async function(ctx) {
|
||||||
|
if (!ctx.isNewInstance) return;
|
||||||
|
|
||||||
|
let {message} = ctx.instance;
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
const parts = message.match(/(?<=\[)[a-zA-Z0-9_\-+!@#$%^&*()={};':"\\|,.<>/?\s]*(?=])/g);
|
||||||
|
if (!parts) return;
|
||||||
|
|
||||||
|
const replacedParts = parts.map(part => {
|
||||||
|
return part.replace(/[!$%^&*()={};':"\\,.<>/?]/g, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [index, part] of parts.entries())
|
||||||
|
message = message.replace(part, replacedParts[index]);
|
||||||
|
|
||||||
|
ctx.instance.message = message;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,4 +3,6 @@ module.exports = Self => {
|
||||||
require('../methods/collection/newCollection')(Self);
|
require('../methods/collection/newCollection')(Self);
|
||||||
require('../methods/collection/getSectors')(Self);
|
require('../methods/collection/getSectors')(Self);
|
||||||
require('../methods/collection/setSaleQuantity')(Self);
|
require('../methods/collection/setSaleQuantity')(Self);
|
||||||
|
require('../methods/collection/previousLabel')(Self);
|
||||||
|
require('../methods/collection/getTickets')(Self);
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,12 +18,21 @@
|
||||||
},
|
},
|
||||||
"expired": {
|
"expired": {
|
||||||
"type": "date"
|
"type": "date"
|
||||||
|
},
|
||||||
|
"supplierAccountFk": {
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"scope": {
|
"scope": {
|
||||||
"where" :{
|
"where" :{
|
||||||
"expired": null
|
"expired": null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"client": {
|
||||||
|
"type": "belongsTo",
|
||||||
|
"model": "Client",
|
||||||
|
"foreignKey": "clientFk"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
},
|
},
|
||||||
"isUeeMember": {
|
"isUeeMember": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isSocialNameUnique": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"relations": {
|
"relations": {
|
||||||
|
@ -39,4 +42,4 @@
|
||||||
"permission": "ALLOW"
|
"permission": "ALLOW"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,17 +9,29 @@
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"id": true,
|
"id": true,
|
||||||
"type": "number",
|
"type": "number"
|
||||||
"forceId": false
|
|
||||||
},
|
},
|
||||||
"date": {
|
"created": {
|
||||||
"type": "date"
|
"type": "date"
|
||||||
},
|
},
|
||||||
"m3":{
|
"longitude":{
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"warehouseFk":{
|
"latitude":{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"dated":{
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
"ticketFk":{
|
||||||
"type": "number"
|
"type": "number"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"relations": {
|
||||||
|
"ticket": {
|
||||||
|
"type": "belongsTo",
|
||||||
|
"model": "Ticket",
|
||||||
|
"foreignKey": "ticketFk"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,8 +54,8 @@ module.exports = Self => {
|
||||||
const writeRole = dmsType.writeRole() && dmsType.writeRole().name;
|
const writeRole = dmsType.writeRole() && dmsType.writeRole().name;
|
||||||
const requiredRole = readRole || writeRole;
|
const requiredRole = readRole || writeRole;
|
||||||
|
|
||||||
const hasRequiredRole = await models.Account.hasRole(myUserId, requiredRole, options);
|
const hasRequiredRole = await models.VnUser.hasRole(myUserId, requiredRole, options);
|
||||||
const isRoot = await models.Account.hasRole(myUserId, 'root', options);
|
const isRoot = await models.VnUser.hasRole(myUserId, 'root', options);
|
||||||
|
|
||||||
if (isRoot || hasRequiredRole)
|
if (isRoot || hasRequiredRole)
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -44,4 +44,4 @@
|
||||||
"principalId": "$everyone",
|
"principalId": "$everyone",
|
||||||
"permission": "ALLOW"
|
"permission": "ALLOW"
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
"url": {
|
"url": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"token": {
|
"cookie": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -29,4 +29,4 @@
|
||||||
"permission": "ALLOW"
|
"permission": "ALLOW"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
module.exports = Self => {
|
module.exports = Self => {
|
||||||
require('../methods/docuware/download')(Self);
|
require('../methods/docuware/download')(Self);
|
||||||
|
require('../methods/docuware/upload')(Self);
|
||||||
require('../methods/docuware/checkFile')(Self);
|
require('../methods/docuware/checkFile')(Self);
|
||||||
|
require('../methods/docuware/deliveryNoteEmail')(Self);
|
||||||
|
require('../methods/docuware/core')(Self);
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,20 +19,21 @@
|
||||||
"fileCabinetName": {
|
"fileCabinetName": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"dialogName": {
|
"dialogName": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"find": {
|
"findById": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"acls": [
|
"relations": {
|
||||||
{
|
"dmsType": {
|
||||||
"property": "*",
|
"type": "belongsTo",
|
||||||
"accessType": "*",
|
"model": "DmsType",
|
||||||
"principalType": "ROLE",
|
"foreignKey": "dmsTypeFk"
|
||||||
"principalId": "$everyone",
|
}
|
||||||
"permission": "ALLOW"
|
}
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
"relations": {
|
"relations": {
|
||||||
"user": {
|
"user": {
|
||||||
"type": "belongsTo",
|
"type": "belongsTo",
|
||||||
"model": "Account",
|
"model": "VnUser",
|
||||||
"foreignKey": "userFk"
|
"foreignKey": "userFk"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,10 +9,11 @@ module.exports = Self => {
|
||||||
* @return {boolean} True for user with read privileges
|
* @return {boolean} True for user with read privileges
|
||||||
*/
|
*/
|
||||||
Self.hasReadRole = async(ctx, name, options) => {
|
Self.hasReadRole = async(ctx, name, options) => {
|
||||||
const collection = await Self.findOne({where: {name}}, {
|
const collection = await Self.findOne({
|
||||||
include: {
|
include: {
|
||||||
relation: 'readRole'
|
relation: 'readRole'
|
||||||
}
|
},
|
||||||
|
where: {name}
|
||||||
}, options);
|
}, options);
|
||||||
|
|
||||||
return await hasRole(ctx, collection, options);
|
return await hasRole(ctx, collection, options);
|
||||||
|
@ -53,8 +54,8 @@ module.exports = Self => {
|
||||||
const writeRole = collection.writeRole() && collection.writeRole().name;
|
const writeRole = collection.writeRole() && collection.writeRole().name;
|
||||||
const requiredRole = readRole || writeRole;
|
const requiredRole = readRole || writeRole;
|
||||||
|
|
||||||
const hasRequiredRole = await models.Account.hasRole(myUserId, requiredRole, options);
|
const hasRequiredRole = await models.VnUser.hasRole(myUserId, requiredRole, options);
|
||||||
const isRoot = await models.Account.hasRole(myUserId, 'root', options);
|
const isRoot = await models.VnUser.hasRole(myUserId, 'root', options);
|
||||||
|
|
||||||
if (isRoot || hasRequiredRole)
|
if (isRoot || hasRequiredRole)
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -1,161 +1,112 @@
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const sharp = require('sharp');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const readChunk = require('read-chunk');
|
const gm = require('gm');
|
||||||
const imageType = require('image-type');
|
|
||||||
const bmp = require('bmp-js');
|
|
||||||
|
|
||||||
module.exports = Self => {
|
module.exports = Self => {
|
||||||
require('../methods/image/download')(Self);
|
require('../methods/image/download')(Self);
|
||||||
require('../methods/image/upload')(Self);
|
require('../methods/image/upload')(Self);
|
||||||
|
require('../methods/image/scrub')(Self);
|
||||||
|
|
||||||
// Function extracted from jimp package (utils)
|
Self.resize = async function({collectionName, srcFile, fileName, entityId}) {
|
||||||
function scan(image, x, y, w, h, f) {
|
|
||||||
// round input
|
|
||||||
x = Math.round(x);
|
|
||||||
y = Math.round(y);
|
|
||||||
w = Math.round(w);
|
|
||||||
h = Math.round(h);
|
|
||||||
|
|
||||||
for (let _y = y; _y < y + h; _y++) {
|
|
||||||
for (let _x = x; _x < x + w; _x++) {
|
|
||||||
const idx = (image.bitmap.width * _y + _x) << 2;
|
|
||||||
f.call(image, _x, _y, idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function extracted from jimp package (type-bmp)
|
|
||||||
function fromAGBR(bitmap) {
|
|
||||||
return scan({bitmap}, 0, 0, bitmap.width, bitmap.height, function(
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
index
|
|
||||||
) {
|
|
||||||
const alpha = this.bitmap.data[index + 0];
|
|
||||||
const blue = this.bitmap.data[index + 1];
|
|
||||||
const green = this.bitmap.data[index + 2];
|
|
||||||
const red = this.bitmap.data[index + 3];
|
|
||||||
|
|
||||||
this.bitmap.data[index + 0] = red;
|
|
||||||
this.bitmap.data[index + 1] = green;
|
|
||||||
this.bitmap.data[index + 2] = blue;
|
|
||||||
this.bitmap.data[index + 3] = bitmap.is_with_alpha ? alpha : 0xff;
|
|
||||||
}).bitmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
Self.registerImage = async(collectionName, srcFilePath, fileName, entityId) => {
|
|
||||||
const models = Self.app.models;
|
const models = Self.app.models;
|
||||||
const tx = await Self.beginTransaction({});
|
|
||||||
const myOptions = {transaction: tx};
|
|
||||||
|
|
||||||
try {
|
const collection = await models.ImageCollection.findOne(
|
||||||
const collection = await models.ImageCollection.findOne({
|
{
|
||||||
fields: [
|
fields: [
|
||||||
'id',
|
'id',
|
||||||
'name',
|
|
||||||
'maxWidth',
|
'maxWidth',
|
||||||
'maxHeight',
|
'maxHeight',
|
||||||
'model',
|
'model',
|
||||||
'property'
|
'property',
|
||||||
],
|
],
|
||||||
where: {name: collectionName},
|
where: {name: collectionName},
|
||||||
include: {
|
include: {
|
||||||
relation: 'sizes',
|
relation: 'sizes',
|
||||||
scope: {
|
scope: {
|
||||||
fields: ['width', 'height', 'crop']
|
fields: ['width', 'height', 'crop'],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}, myOptions);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const data = {
|
// Insert image row
|
||||||
name: fileName,
|
const imageName = path.parse(fileName).name;
|
||||||
|
await models.Image.upsertWithWhere(
|
||||||
|
{
|
||||||
|
name: imageName,
|
||||||
collectionFk: collectionName
|
collectionFk: collectionName
|
||||||
};
|
},
|
||||||
const newImage = await Self.upsertWithWhere(data, {
|
{
|
||||||
name: fileName,
|
name: imageName,
|
||||||
collectionFk: collectionName,
|
collectionFk: collectionName,
|
||||||
updated: (new Date).getTime()
|
updated: Date.vnNow() / 1000,
|
||||||
}, myOptions);
|
|
||||||
|
|
||||||
// Resizes and saves the image
|
|
||||||
const container = await models.ImageContainer.container(collectionName);
|
|
||||||
const rootPath = container.client.root;
|
|
||||||
const collectionDir = path.join(rootPath, collectionName);
|
|
||||||
const file = `${fileName}.png`;
|
|
||||||
const dstDir = path.join(collectionDir, 'full');
|
|
||||||
const dstFile = path.join(dstDir, file);
|
|
||||||
|
|
||||||
const buffer = readChunk.sync(srcFilePath, 0, 12);
|
|
||||||
const type = imageType(buffer);
|
|
||||||
|
|
||||||
let sharpOptions;
|
|
||||||
let imgSrc = srcFilePath;
|
|
||||||
if (type.mime == 'image/bmp') {
|
|
||||||
const bmpBuffer = fs.readFileSync(srcFilePath);
|
|
||||||
const bmpData = fromAGBR(bmp.decode(bmpBuffer));
|
|
||||||
imgSrc = bmpData.data;
|
|
||||||
sharpOptions = {
|
|
||||||
raw: {
|
|
||||||
width: bmpData.width,
|
|
||||||
height: bmpData.height,
|
|
||||||
channels: 4
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const resizeOpts = {
|
// Update entity image file name
|
||||||
withoutEnlargement: true,
|
const model = models[collection.model];
|
||||||
fit: 'inside'
|
if (!model) throw new Error('No matching model found');
|
||||||
};
|
|
||||||
|
|
||||||
await fs.mkdir(dstDir, {recursive: true});
|
const entity = await model.findById(entityId);
|
||||||
await sharp(imgSrc, sharpOptions)
|
if (entity) {
|
||||||
.resize(collection.maxWidth, collection.maxHeight, resizeOpts)
|
await entity.updateAttribute(
|
||||||
.png()
|
collection.property,
|
||||||
.toFile(dstFile);
|
imageName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const sizes = collection.sizes();
|
// Resize
|
||||||
for (let size of sizes) {
|
const container = await models.ImageContainer.container(
|
||||||
const dstDir = path.join(collectionDir, `${size.width}x${size.height}`);
|
collectionName
|
||||||
const dstFile = path.join(dstDir, file);
|
);
|
||||||
const resizeOpts = {
|
const rootPath = container.client.root;
|
||||||
withoutEnlargement: true,
|
const collectionDir = path.join(rootPath, collectionName);
|
||||||
fit: size.crop ? 'cover' : 'inside'
|
|
||||||
};
|
|
||||||
|
|
||||||
await fs.mkdir(dstDir, {recursive: true});
|
// To max size
|
||||||
await sharp(imgSrc, sharpOptions)
|
const {maxWidth, maxHeight} = collection;
|
||||||
.resize(size.width, size.height, resizeOpts)
|
const fullSizePath = path.join(collectionDir, 'full');
|
||||||
.png()
|
const toFullSizePath = `${fullSizePath}/${fileName}`;
|
||||||
.toFile(dstFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
const model = models[collection.model];
|
await fs.mkdir(fullSizePath, {recursive: true});
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
gm(srcFile)
|
||||||
|
.resize(maxWidth, maxHeight, '>')
|
||||||
|
.setFormat('png')
|
||||||
|
.quality(100)
|
||||||
|
.write(toFullSizePath, function(err) {
|
||||||
|
if (err) reject(err);
|
||||||
|
if (!err) resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (!model)
|
// To collection sizes
|
||||||
throw new Error('Matching model not found');
|
for (const size of collection.sizes()) {
|
||||||
|
const {width, height} = size;
|
||||||
|
|
||||||
const item = await model.findById(entityId, null, myOptions);
|
const sizePath = path.join(collectionDir, `${width}x${height}`);
|
||||||
if (item) {
|
const toSizePath = `${sizePath}/${fileName}`;
|
||||||
await item.updateAttribute(
|
|
||||||
collection.property,
|
|
||||||
fileName,
|
|
||||||
myOptions
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fs.existsSync(srcFilePath))
|
await fs.mkdir(sizePath, {recursive: true});
|
||||||
await fs.unlink(srcFilePath);
|
await new Promise((resolve, reject) => {
|
||||||
|
const gmInstance = gm(srcFile);
|
||||||
|
|
||||||
await tx.commit();
|
if (size.crop) {
|
||||||
|
gmInstance
|
||||||
|
.resize(width, height, '^')
|
||||||
|
.gravity('Center')
|
||||||
|
.crop(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
return newImage;
|
if (!size.crop) gmInstance.resize(width, height, '>');
|
||||||
} catch (e) {
|
|
||||||
await tx.rollback();
|
gmInstance
|
||||||
throw e;
|
.setFormat('png')
|
||||||
|
.quality(100)
|
||||||
|
.write(toSizePath, function(err) {
|
||||||
|
if (err) reject(err);
|
||||||
|
if (!err) resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = Self => {
|
||||||
|
require('../methods/notification/send')(Self);
|
||||||
|
require('../methods/notification/clean')(Self);
|
||||||
|
};
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "Notification",
|
||||||
|
"base": "VnModel",
|
||||||
|
"options": {
|
||||||
|
"mysql": {
|
||||||
|
"table": "util.notification"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number",
|
||||||
|
"id": true,
|
||||||
|
"description": "Identifier"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"subscription": {
|
||||||
|
"type": "hasMany",
|
||||||
|
"model": "NotificationSubscription",
|
||||||
|
"foreignKey": "notificationFk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "NotificationAcl",
|
||||||
|
"base": "VnModel",
|
||||||
|
"options": {
|
||||||
|
"mysql": {
|
||||||
|
"table": "util.notificationAcl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties":{
|
||||||
|
"notificationFk": {
|
||||||
|
"id": true,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"roleFk":{
|
||||||
|
"id": true,
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"notification": {
|
||||||
|
"type": "belongsTo",
|
||||||
|
"model": "Notification",
|
||||||
|
"foreignKey": "notificationFk"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "belongsTo",
|
||||||
|
"model": "Role",
|
||||||
|
"foreignKey": "roleFk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "NotificationConfig",
|
||||||
|
"base": "VnModel",
|
||||||
|
"options": {
|
||||||
|
"mysql": {
|
||||||
|
"table": "util.notificationConfig"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number",
|
||||||
|
"id": true,
|
||||||
|
"description": "Identifier"
|
||||||
|
},
|
||||||
|
"cleanDays": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"name": "NotificationQueue",
|
||||||
|
"base": "VnModel",
|
||||||
|
"options": {
|
||||||
|
"mysql": {
|
||||||
|
"table": "util.notificationQueue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number",
|
||||||
|
"id": true,
|
||||||
|
"description": "Identifier"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created": {
|
||||||
|
"type": "date"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"notification": {
|
||||||
|
"type": "belongsTo",
|
||||||
|
"model": "Notification",
|
||||||
|
"foreignKey": "notificationFk",
|
||||||
|
"primaryKey": "name"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"type": "belongsTo",
|
||||||
|
"model": "VnUser",
|
||||||
|
"foreignKey": "authorFk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue