First Commit
This commit is contained in:
commit
f75db0e0d1
131 changed files with 12286 additions and 0 deletions
37
.env
Normal file
37
.env
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=postgres
|
||||
DB_NAME=paqueteria
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=paqueteria-secret-key-2026
|
||||
JWT_EXPIRATION=15d
|
||||
|
||||
# API Key
|
||||
API_KEY=3C03C2A5-F739-4353-9B7E-038A2113285A
|
||||
|
||||
# OCR Provider (tesseract | ocrspace | google-vision | mimo)
|
||||
OCR_PROVIDER=mimo
|
||||
|
||||
# OCR.space (solo si OCR_PROVIDER=ocrspace)
|
||||
OCRSPACE_API_KEY=
|
||||
|
||||
# mimo key
|
||||
MIMO_API_KEY=tp-sb1of6tkbio6jlw5gbzpajnv0qz0c915p34bdgqi8nfl7jvo
|
||||
|
||||
# Google Vision (solo si OCR_PROVIDER=google-vision)
|
||||
GOOGLE_VISION_API_KEY=AIzaSyB60ZqP92SKetK7IN2BY54EejJcxy8LEmk
|
||||
|
||||
# Worker
|
||||
WORKER_KEY=885E2E87-6B8C-4CBC-93D8-D07CC5A13158
|
||||
|
||||
# App
|
||||
PORT=3000
|
||||
|
||||
# Logo Empresa
|
||||
EMPRESA_LOGO_MAX_SIZE=200
|
||||
EMPRESA_LOGO_WIDTH=400
|
||||
EMPRESA_LOGO_HEIGHT=400
|
||||
EMPRESA_LOGO_QUALITY=80
|
||||
38
.env.example
Normal file
38
.env.example
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=postgres
|
||||
DB_NAME=paqueteria
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your-secret-key
|
||||
JWT_EXPIRATION=7d
|
||||
|
||||
# API Key
|
||||
API_KEY=your-api-key
|
||||
|
||||
# OCR Provider (tesseract | ocrspace | google-vision | mimo)
|
||||
# tesseract: Gratis, local, precisa bien para texto impreso
|
||||
# ocrspace: 25,000 req/mes gratis, mejor para manuscrito
|
||||
# google-vision: Alta precisión, $1.50/1000 imágenes (1000 gratis/mes)
|
||||
# mimo: Multimodal con mimo-v2.5, mejor para manuscrito, comprime >400KB automáticamente
|
||||
OCR_PROVIDER=tesseract
|
||||
|
||||
# OCR.space (solo si OCR_PROVIDER=ocrspace)
|
||||
# Obtener key gratis en: https://ocr.space/ocrapi/freekey
|
||||
OCRSPACE_API_KEY=
|
||||
|
||||
# Google Vision (solo si OCR_PROVIDER=google-vision)
|
||||
# Obtener key en: https://console.cloud.google.com/apis/credentials
|
||||
GOOGLE_VISION_API_KEY=
|
||||
|
||||
# MiMo (solo si OCR_PROVIDER=mimo)
|
||||
# Modelo multimodal mimo-v2.5 para OCR de etiquetas manuscritas
|
||||
MIMO_API_KEY=
|
||||
|
||||
# Worker (clave para autenticar peticiones del worker)
|
||||
WORKER_KEY=your-worker-key
|
||||
|
||||
# App
|
||||
PORT=3000
|
||||
86
.gitignore
vendored
Normal file
86
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
node_modules# Dependencies
|
||||
node_modules/
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
.next/
|
||||
out/
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
|
||||
# Environment
|
||||
# .env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
|
||||
# Data and storage
|
||||
data/
|
||||
media/
|
||||
!src/**/media/
|
||||
uploads/
|
||||
.wwebjs_auth/
|
||||
.wwebjs_cache/
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Docker
|
||||
.docker/
|
||||
|
||||
# Misc
|
||||
*.bak
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Git worktrees
|
||||
.worktrees/
|
||||
|
||||
# AI Agents
|
||||
.playwright-mcp/
|
||||
.remember/
|
||||
.agent/
|
||||
.claude/
|
||||
.zcode/
|
||||
.zcode-worktrees/
|
||||
docs/plans/
|
||||
docs/superpowers/
|
||||
CLAUDE.md
|
||||
_docs/
|
||||
.superpowers/
|
||||
# TypeScript incremental build cache (tsc writes this at repo root under the TS6 build config)
|
||||
*.tsbuildinfo
|
||||
|
||||
node_modules
|
||||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
ignore-scripts=false
|
||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
114
README.md
Normal file
114
README.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Project setup
|
||||
|
||||
```bash
|
||||
$ pnpm install
|
||||
```
|
||||
|
||||
## Compile and run the project
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ pnpm run start
|
||||
|
||||
# watch mode
|
||||
$ pnpm run start:dev
|
||||
|
||||
# production mode
|
||||
$ pnpm run start:prod
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ pnpm run test
|
||||
|
||||
# e2e tests
|
||||
$ pnpm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ pnpm run test:cov
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||
|
||||
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||
|
||||
```bash
|
||||
$ pnpm install -g @nestjs/mau
|
||||
$ mau deploy
|
||||
```
|
||||
|
||||
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||
|
||||
## Observability
|
||||
|
||||
In production applications, observability is essential for understanding how your system behaves, detecting issues early, and maintaining reliable performance.
|
||||
|
||||
[NestJS Observe](https://observe.nestjs.com) automatically instruments your NestJS application, giving you deep visibility into your system with minimal setup:
|
||||
|
||||
- **Distributed tracing:** Follow requests across services and understand how they flow through your system.
|
||||
- **Waterfall analysis:** Visualize request execution and identify slow operations, bottlenecks, and unexpected delays.
|
||||
- **Performance analysis:** Analyze application performance in real time and quickly pinpoint areas that need optimization.
|
||||
- **Metrics:** Track key application and infrastructure metrics to understand system health and performance trends.
|
||||
- **Logging:** Centralize and correlate logs with traces and other telemetry to make debugging easier.
|
||||
- **Error tracking:** Detect errors quickly and investigate their root causes with the surrounding context.
|
||||
- **SLA monitoring:** Track service-level objectives and identify when your application is approaching or exceeding defined thresholds.
|
||||
- **Alarms and alerts:** Set up alerts for critical errors, performance degradation, SLA violations, and other anomalies so your team can react quickly.
|
||||
|
||||
## Resources
|
||||
|
||||
Check out a few resources that may come in handy when working with NestJS:
|
||||
|
||||
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||
- Auto-instrument your application with [NestJS Observer](https://observer.nestjs.com). Distributed tracing, metrics, and logging made easy. Error tracking and performance monitoring for your NestJS applications.
|
||||
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||
BIN
assets/templates/qr-template.png
Normal file
BIN
assets/templates/qr-template.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 622 KiB |
11
http/auth.http
Normal file
11
http/auth.http
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
@baseUrl = http://localhost:3000/v1
|
||||
@contentType = application/json
|
||||
|
||||
### Login
|
||||
POST {{baseUrl}}/auth/login
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"usuario": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
4
http/environment.http
Normal file
4
http/environment.http
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
### Variables de entorno
|
||||
@baseUrl = http://localhost:3000/v1
|
||||
@contentType = application/json
|
||||
@apiKey = 3C03C2A5-F739-4353-9B7E-038A2113285A
|
||||
38
http/usuario.http
Normal file
38
http/usuario.http
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
@baseUrl = http://localhost:3000/v1
|
||||
@contentType = application/json
|
||||
@apiKey = 3C03C2A5-F739-4353-9B7E-038A2113285A
|
||||
|
||||
### Listar usuarios
|
||||
GET {{baseUrl}}/usuario
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
### Obtener usuario por ID
|
||||
GET {{baseUrl}}/usuario/1
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
### Registrar usuario (requiere API Key)
|
||||
POST {{baseUrl}}/usuario
|
||||
Content-Type: {{contentType}}
|
||||
x-api-key: {{apiKey}}
|
||||
|
||||
{
|
||||
"nombre": "Juan Pérez",
|
||||
"usuario": "juan",
|
||||
"password": "password123",
|
||||
"pregunta": "¿Cuál es tu color favorito?",
|
||||
"respuesta": "azul",
|
||||
"rol": "RECEPCION"
|
||||
}
|
||||
|
||||
### Actualizar usuario
|
||||
PUT {{baseUrl}}/usuario/1
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"nombre": "Juan Pérez Actualizado"
|
||||
}
|
||||
|
||||
### Eliminar usuario
|
||||
DELETE {{baseUrl}}/usuario/2
|
||||
Authorization: Bearer {{token}}
|
||||
8
nest-cli.json
Normal file
8
nest-cli.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
10
oxlint.json
Normal file
10
oxlint.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/crates/oxc_linter/src/rules.rs",
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-floating-promises": "warn"
|
||||
},
|
||||
"env": {
|
||||
"node": true
|
||||
}
|
||||
}
|
||||
78
package.json
Normal file
78
package.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"name": "app-paqueteria-nest",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"deploy": "nest deploy",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "oxlint src/ test/",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:cov": "vitest run --coverage",
|
||||
"test:debug": "vitest --inspect-brk --no-file-parallelism",
|
||||
"test:e2e": "vitest run --config ./vitest.config.e2e.ts",
|
||||
"setup:db": "pnpm build && node dist/database/seeds/run.js",
|
||||
"db:create": "npx typeorm-extension db:create --dataSource=typeorm.config.ts",
|
||||
"db:drop": "npx typeorm-extension db:drop --dataSource=typeorm.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^12.0.1",
|
||||
"@nestjs/config": "^12.0.0",
|
||||
"@nestjs/core": "^12.0.1",
|
||||
"@nestjs/jwt": "^12.0.1",
|
||||
"@nestjs/passport": "^12.0.0",
|
||||
"@nestjs/platform-express": "^12.0.1",
|
||||
"@nestjs/serve-static": "^12.0.0",
|
||||
"@nestjs/swagger": "^12.0.1",
|
||||
"@nestjs/typeorm": "^12.0.1",
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.15.1",
|
||||
"dotenv": "^17.4.2",
|
||||
"multer": "^2.3.0",
|
||||
"openai": "^4.0.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.23.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.33.0",
|
||||
"tesseract.js": "^5.1.0",
|
||||
"typeorm": "^1.1.0",
|
||||
"typeorm-extension": "^4.1.0",
|
||||
"uuid": "^14.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^12.0.0",
|
||||
"@nestjs/mau": "^0.2.6",
|
||||
"@nestjs/schematics": "^12.0.0",
|
||||
"@nestjs/testing": "^12.0.1",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/multer": "^2.2.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/pg": "^8.23.1",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/supertest": "^7.0.0",
|
||||
"@vitest/coverage-v8": "^4.1.2",
|
||||
"oxlint": "^1.58.0",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"tsx": "^4.23.13",
|
||||
"typescript": "^6.0.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.1.2"
|
||||
}
|
||||
}
|
||||
5398
pnpm-lock.yaml
generated
Normal file
5398
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
6
pnpm-workspace.yaml
Normal file
6
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
allowBuilds:
|
||||
'@scarf/scarf': true
|
||||
bcrypt: true
|
||||
esbuild: true
|
||||
sharp: set this to true or false
|
||||
tesseract.js: set this to true or false
|
||||
BIN
spa.traineddata
Normal file
BIN
spa.traineddata
Normal file
Binary file not shown.
62
src/app.module.ts
Normal file
62
src/app.module.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { AuthModule } from './modules/auth/auth.module.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
import { EmpresaModule } from './modules/empresa/empresa.module.js';
|
||||
import { SucursalModule } from './modules/sucursal/sucursal.module.js';
|
||||
import { CategoriaModule } from './modules/categoria/categoria.module.js';
|
||||
import { ZonaModule } from './modules/zona/zona.module.js';
|
||||
import { FeriadoModule } from './modules/feriado/feriado.module.js';
|
||||
import { PaqueteModule } from './modules/paquete/paquete.module.js';
|
||||
import { MensajeModule } from './modules/mensaje/mensaje.module.js';
|
||||
import { UploadModule } from './modules/upload/upload.module.js';
|
||||
import { GuardsModule } from './shared/guards.module.js';
|
||||
import { EmprendimientoModule } from './modules/emprendimiento/emprendimiento.module.js';
|
||||
import { UsuarioModule } from './modules/usuario/usuario.module.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
type: 'postgres',
|
||||
host: config.get('DB_HOST'),
|
||||
port: config.get('DB_PORT'),
|
||||
username: config.get('DB_USERNAME'),
|
||||
password: config.get('DB_PASSWORD'),
|
||||
database: config.get('DB_NAME'),
|
||||
|
||||
autoLoadEntities: true,
|
||||
synchronize: true,
|
||||
dropSchema: false,
|
||||
}),
|
||||
}),
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(__dirname, '..', 'uploads'),
|
||||
serveRoot: '/uploads',
|
||||
}),
|
||||
AuthModule,
|
||||
EmpresaModule,
|
||||
SucursalModule,
|
||||
CategoriaModule,
|
||||
ZonaModule,
|
||||
FeriadoModule,
|
||||
PaqueteModule,
|
||||
MensajeModule,
|
||||
UploadModule,
|
||||
GuardsModule,
|
||||
EmprendimientoModule,
|
||||
UsuarioModule
|
||||
],
|
||||
})
|
||||
export class AppModule { }
|
||||
38
src/database/seeds/auto-seed.ts
Normal file
38
src/database/seeds/auto-seed.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { INestApplication } from '@nestjs/common';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { Empresa } from '../../modules/empresa/entities/empresa.entity.js';
|
||||
import { Usuario } from '../../modules/usuario/entities/usuario.entity.js';
|
||||
|
||||
export async function runSeed(app: INestApplication): Promise<void> {
|
||||
const empresaRepo = app.get<Repository<Empresa>>(getRepositoryToken(Empresa));
|
||||
const usuarioRepo = app.get<Repository<Usuario>>(getRepositoryToken(Usuario));
|
||||
|
||||
const existeEmpresa = await empresaRepo.findOne({ where: {} });
|
||||
if (existeEmpresa) return;
|
||||
|
||||
console.log('DB vacía — ejecutando seed...');
|
||||
|
||||
/*
|
||||
await empresaRepo.save({
|
||||
nombre: 'La Clave',
|
||||
diasIncluidos: 7,
|
||||
costoBase: 5,
|
||||
costoDiaAdicional: 2,
|
||||
diasParaAlmacen: 30,
|
||||
});
|
||||
*/
|
||||
|
||||
const passwordHash = await bcrypt.hash('admin123', 10);
|
||||
await usuarioRepo.save({
|
||||
nombre: 'Administrador',
|
||||
usuario: 'admin@admin.com',
|
||||
password: passwordHash,
|
||||
pregunta: '¿Cómo se llama tu mamá?',
|
||||
respuesta: 'Juanita',
|
||||
rol: 'ADMIN',
|
||||
});
|
||||
|
||||
console.log('Seed completado: Empresa "La Clave" + usuario admin creados.');
|
||||
}
|
||||
25
src/database/seeds/empresa.seeder.ts
Normal file
25
src/database/seeds/empresa.seeder.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { DataSource } from 'typeorm';
|
||||
import { Seeder } from 'typeorm-extension';
|
||||
import { Empresa } from '../../modules/empresa/entities/empresa.entity.js';
|
||||
|
||||
export class EmpresaSeeder implements Seeder {
|
||||
async run(dataSource: DataSource): Promise<void> {
|
||||
const repo = dataSource.getRepository(Empresa);
|
||||
|
||||
const existe = await repo.findOne({ where: {} });
|
||||
if (existe) {
|
||||
console.log('Empresa ya existe, omitiendo seed.');
|
||||
return;
|
||||
}
|
||||
|
||||
await repo.save({
|
||||
nombre: 'La Clave',
|
||||
diasIncluidos: 7,
|
||||
costoBase: 5,
|
||||
costoDiaAdicional: 2,
|
||||
diasParaAlmacen: 30,
|
||||
});
|
||||
|
||||
console.log('Empresa "La Clave" creada.');
|
||||
}
|
||||
}
|
||||
13
src/database/seeds/main.seeder.ts
Normal file
13
src/database/seeds/main.seeder.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { DataSource } from 'typeorm';
|
||||
import { Seeder } from 'typeorm-extension';
|
||||
import { EmpresaSeeder } from './empresa.seeder.js';
|
||||
import { UsuarioSeeder } from './usuario.seeder.js';
|
||||
|
||||
export default class MainSeeder implements Seeder {
|
||||
async run(dataSource: DataSource): Promise<void> {
|
||||
//await new EmpresaSeeder().run(dataSource);
|
||||
await new UsuarioSeeder().run(dataSource);
|
||||
|
||||
console.log('Seed completado.');
|
||||
}
|
||||
}
|
||||
36
src/database/seeds/run.ts
Normal file
36
src/database/seeds/run.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import 'reflect-metadata';
|
||||
import 'dotenv/config';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { EmpresaSeeder } from './empresa.seeder.js';
|
||||
import { UsuarioSeeder } from './usuario.seeder.js';
|
||||
import { Empresa } from '../../modules/empresa/entities/empresa.entity.js';
|
||||
import { Usuario } from '../../modules/usuario/entities/usuario.entity.js';
|
||||
|
||||
const dataSource = new DataSource({
|
||||
type: 'postgres',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: Number(process.env.DB_PORT) || 5432,
|
||||
username: process.env.DB_USERNAME || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'postgres',
|
||||
database: process.env.DB_NAME || 'paqueteria',
|
||||
entities: [Empresa, Usuario],
|
||||
});
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await dataSource.initialize();
|
||||
console.log('DB conectada.');
|
||||
|
||||
await new EmpresaSeeder().run(dataSource);
|
||||
await new UsuarioSeeder().run(dataSource);
|
||||
|
||||
console.log('Seed completado.');
|
||||
await dataSource.destroy();
|
||||
} catch (error) {
|
||||
console.error('Error en seed:', error);
|
||||
await dataSource.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
29
src/database/seeds/usuario.seeder.ts
Normal file
29
src/database/seeds/usuario.seeder.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { DataSource } from 'typeorm';
|
||||
import { Seeder } from 'typeorm-extension';
|
||||
import { Usuario } from '../../modules/usuario/entities/usuario.entity.js';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
export class UsuarioSeeder implements Seeder {
|
||||
async run(dataSource: DataSource): Promise<void> {
|
||||
const repo = dataSource.getRepository(Usuario);
|
||||
|
||||
const existe = await repo.findOne({ where: { usuario: 'admin' } });
|
||||
if (existe) {
|
||||
console.log('Usuario admin ya existe, omitiendo seed.');
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash('admin123', 10);
|
||||
|
||||
await repo.save({
|
||||
nombre: 'Administrador',
|
||||
usuario: 'admin@admin.com',
|
||||
password: passwordHash,
|
||||
pregunta: '¿Cómo se llama tu mamá?',
|
||||
respuesta: 'Juanita',
|
||||
rol: 'ADMIN',
|
||||
});
|
||||
|
||||
console.log('Usuario admin creado.');
|
||||
}
|
||||
}
|
||||
11
src/environments/environment.ts
Executable file
11
src/environments/environment.ts
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
export const environment = {
|
||||
production: false,
|
||||
tipoTramite: [
|
||||
{ value: 'nombre_tramite', data: 'Nombre del Trámite' },
|
||||
{ value: 'descripcion', data: 'Descripción' }
|
||||
],
|
||||
fases: [
|
||||
{ value: 'nombre', data: 'Nombre de la Fase' },
|
||||
{ value: 'descripcion', data: 'Descripción' }
|
||||
]
|
||||
};
|
||||
44
src/main.ts
Normal file
44
src/main.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { AppModule } from './app.module.js';
|
||||
import { ApiKeyGuard } from './shared/guards/api-key.guard.js';
|
||||
import { ensureDatabase } from './shared/services/database.service.js';
|
||||
import { runSeed } from './database/seeds/auto-seed.js';
|
||||
|
||||
async function bootstrap() {
|
||||
await ensureDatabase();
|
||||
|
||||
const app = await NestFactory.create(AppModule);
|
||||
await runSeed(app);
|
||||
|
||||
app.setGlobalPrefix('v1');
|
||||
|
||||
app.enableCors();
|
||||
|
||||
app.useGlobalGuards(app.get(ApiKeyGuard));
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('API Paquetería')
|
||||
.setDescription('Sistema de Gestión de Paqueterías')
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('v1/docs', app, document);
|
||||
|
||||
const port = process.env.PORT ?? 3000;
|
||||
await app.listen(port, '0.0.0.0');
|
||||
console.log(`Server running on http://localhost:${port}`);
|
||||
console.log(`Swagger docs: http://localhost:${port}/v1/docs`);
|
||||
}
|
||||
await bootstrap();
|
||||
36
src/modules/auth/auth.controller.ts
Normal file
36
src/modules/auth/auth.controller.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { Controller, Post, Body, HttpStatus } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { LoginDto } from './dto/login.dto.js';
|
||||
import { Public } from '../../shared/interfaces/auth.interface.js';
|
||||
import { ApiResult, defaultApiResult } from '../../shared/interfaces/api.result.js';
|
||||
import { routeAuth } from './auth.router.js';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('login')
|
||||
@Public()
|
||||
async login(@Body() dto: LoginDto): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeAuth.login.title,
|
||||
routeAuth.login.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.authService.login(dto);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Inicio de sesión exitoso';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [result];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
}
|
||||
28
src/modules/auth/auth.module.ts
Normal file
28
src/modules/auth/auth.module.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { AuthController } from './auth.controller.js';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { JwtStrategy } from '../../shared/strategies/jwt.strategy.js';
|
||||
import { Usuario } from '../usuario/entities/usuario.entity.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Usuario]),
|
||||
PassportModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
secret: config.get('JWT_SECRET'),
|
||||
signOptions: { expiresIn: config.get('JWT_EXPIRATION', '7d') },
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
exports: [AuthService, JwtStrategy, JwtModule],
|
||||
})
|
||||
export class AuthModule {}
|
||||
6
src/modules/auth/auth.router.ts
Normal file
6
src/modules/auth/auth.router.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export const routeAuth = {
|
||||
login: {
|
||||
route: '[POST].../v1/auth/login',
|
||||
title: 'Login',
|
||||
},
|
||||
};
|
||||
97
src/modules/auth/auth.service.ts
Normal file
97
src/modules/auth/auth.service.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { Injectable, UnauthorizedException, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { Usuario } from '../usuario/entities/usuario.entity.js';
|
||||
import { LoginDto } from './dto/login.dto.js';
|
||||
|
||||
interface LoginAttempt {
|
||||
count: number;
|
||||
blockedUntil: Date | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private loginAttempts = new Map<string, LoginAttempt>();
|
||||
private readonly MAX_ATTEMPTS = 5;
|
||||
private readonly BLOCK_DURATION_MS = 60 * 60 * 1000; // 1 hora
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Usuario)
|
||||
private usuarioRepo: Repository<Usuario>,
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async login(dto: LoginDto) {
|
||||
const attempts = this.loginAttempts.get(dto.usuario);
|
||||
|
||||
if (attempts?.blockedUntil && attempts.blockedUntil > new Date()) {
|
||||
const remainingMs = attempts.blockedUntil.getTime() - Date.now();
|
||||
const remainingMin = Math.ceil(remainingMs / 60000);
|
||||
throw new HttpException(
|
||||
`Cuenta bloqueada. Intenta de nuevo en ${remainingMin} minuto(s).`,
|
||||
HttpStatus.TOO_MANY_REQUESTS,
|
||||
);
|
||||
}
|
||||
|
||||
const usuario = await this.usuarioRepo.findOne({
|
||||
where: { usuario: dto.usuario },
|
||||
});
|
||||
|
||||
if (!usuario) {
|
||||
this.trackFailedAttempt(dto.usuario);
|
||||
throw new UnauthorizedException('Credenciales inválidas');
|
||||
}
|
||||
|
||||
const passwordValid = await bcrypt.compare(dto.password, usuario.password);
|
||||
if (!passwordValid) {
|
||||
this.trackFailedAttempt(dto.usuario);
|
||||
throw new UnauthorizedException('Credenciales inválidas');
|
||||
}
|
||||
|
||||
this.loginAttempts.delete(dto.usuario);
|
||||
|
||||
const payload = {
|
||||
sub: usuario.idUsuario,
|
||||
usuario: usuario.usuario,
|
||||
rol: usuario.rol,
|
||||
};
|
||||
|
||||
return {
|
||||
token: this.jwtService.sign(payload),
|
||||
usuario: {
|
||||
idUsuario: usuario.idUsuario,
|
||||
nombre: usuario.nombre,
|
||||
usuario: usuario.usuario,
|
||||
rol: usuario.rol,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private trackFailedAttempt(username: string) {
|
||||
const current = this.loginAttempts.get(username);
|
||||
|
||||
if (!current) {
|
||||
this.loginAttempts.set(username, {
|
||||
count: 1,
|
||||
blockedUntil: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newCount = current.count + 1;
|
||||
|
||||
if (newCount >= this.MAX_ATTEMPTS) {
|
||||
this.loginAttempts.set(username, {
|
||||
count: newCount,
|
||||
blockedUntil: new Date(Date.now() + this.BLOCK_DURATION_MS),
|
||||
});
|
||||
} else {
|
||||
this.loginAttempts.set(username, {
|
||||
count: newCount,
|
||||
blockedUntil: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/modules/auth/dto/login.dto.ts
Normal file
14
src/modules/auth/dto/login.dto.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { IsString, IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty({ example: 'admin' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
usuario: string;
|
||||
|
||||
@ApiProperty({ example: 'password123' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
}
|
||||
132
src/modules/categoria/categoria.controller.ts
Normal file
132
src/modules/categoria/categoria.controller.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import { Controller, Get, Post, Put, Delete, Body, Param, HttpStatus } from '@nestjs/common';
|
||||
import { CategoriaService } from './categoria.service.js';
|
||||
import { CategoriaDto } from './dto/categoria.dto.js';
|
||||
import { UpdateCategoriaDto } from './dto/update-categoria.dto.js';
|
||||
import { ApiResult, defaultApiResult } from '../../shared/interfaces/api.result.js';
|
||||
import { routeCategoria } from './categoria.router.js';
|
||||
|
||||
@Controller('categoria')
|
||||
export class CategoriaController {
|
||||
constructor(private readonly categoriaService: CategoriaService) {}
|
||||
|
||||
@Get()
|
||||
async findAll(): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeCategoria.find.title,
|
||||
routeCategoria.find.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.categoriaService.findAll();
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = `${result.length} Categoria(s) encontrado(s).`;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.length;
|
||||
apiResult.data = result;
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Get(':idCategoria')
|
||||
async findOne(@Param('idCategoria') idCategoria: string): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeCategoria.one.title,
|
||||
routeCategoria.one.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.categoriaService.findOne(idCategoria);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Existe un Categoria.';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [result];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Body() categoriaDto: CategoriaDto): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeCategoria.create.title,
|
||||
routeCategoria.create.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.categoriaService.create(categoriaDto);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Categoria se ha agregado correctamente.';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [result];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Put(':idCategoria')
|
||||
async update(
|
||||
@Param('idCategoria') idCategoria: string,
|
||||
@Body() updateCategoriaDto: UpdateCategoriaDto,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeCategoria.update.title,
|
||||
routeCategoria.update.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.categoriaService.update(idCategoria, updateCategoriaDto);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Se ha actualizado correctamente.';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [result];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Delete(':idCategoria')
|
||||
async remove(@Param('idCategoria') idCategoria: string): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeCategoria.remove.title,
|
||||
routeCategoria.remove.route,
|
||||
);
|
||||
|
||||
try {
|
||||
await this.categoriaService.remove(idCategoria);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Se ha eliminado correctamente.';
|
||||
apiResult.boolean = true;
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
}
|
||||
13
src/modules/categoria/categoria.module.ts
Normal file
13
src/modules/categoria/categoria.module.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CategoriaController } from './categoria.controller.js';
|
||||
import { CategoriaService } from './categoria.service.js';
|
||||
import { Categoria } from './entities/categoria.entity.js';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Categoria])],
|
||||
controllers: [CategoriaController],
|
||||
providers: [CategoriaService],
|
||||
exports: [CategoriaService],
|
||||
})
|
||||
export class CategoriaModule {}
|
||||
26
src/modules/categoria/categoria.router.ts
Normal file
26
src/modules/categoria/categoria.router.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export const routeCategoria = {
|
||||
multiple: {
|
||||
route: '[POST].../v1/categoria/multiple',
|
||||
title: 'Create Multiple Categoria',
|
||||
},
|
||||
create: {
|
||||
route: '[POST].../v1/categoria',
|
||||
title: 'Create Categoria',
|
||||
},
|
||||
one: {
|
||||
route: '[GET].../v1/categoria/:idCategoria',
|
||||
title: 'Find One Categoria',
|
||||
},
|
||||
find: {
|
||||
route: '[POST].../v1/categoria/find',
|
||||
title: 'Find Categoria WhereCondition[], Attribute & Direccion',
|
||||
},
|
||||
update: {
|
||||
route: '[PATCH].../v1/categoria/:idCategoria',
|
||||
title: 'Update Categoria',
|
||||
},
|
||||
remove: {
|
||||
route: '[DELETE].../v1/categoria/:idCategoria',
|
||||
title: 'Remove Categoria',
|
||||
},
|
||||
};
|
||||
55
src/modules/categoria/categoria.service.ts
Normal file
55
src/modules/categoria/categoria.service.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Categoria } from './entities/categoria.entity.js';
|
||||
import { CategoriaDto } from './dto/categoria.dto.js';
|
||||
import { UpdateCategoriaDto } from './dto/update-categoria.dto.js';
|
||||
|
||||
@Injectable()
|
||||
export class CategoriaService {
|
||||
constructor(
|
||||
@InjectRepository(Categoria)
|
||||
private repo: Repository<Categoria>,
|
||||
) {}
|
||||
|
||||
async findAll() {
|
||||
return this.repo.find();
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const categoria = await this.repo.findOne({ where: { idCategoria: id } });
|
||||
if (!categoria) throw new NotFoundException('Categoría no encontrada');
|
||||
return categoria;
|
||||
}
|
||||
|
||||
async create(dto: CategoriaDto) {
|
||||
const exists = await this.repo.findOne({ where: { nombre: dto.nombre } });
|
||||
if (exists) throw new ConflictException('Ya existe una categoría con ese nombre');
|
||||
|
||||
const categoria = this.repo.create(dto);
|
||||
const saved = await this.repo.save(categoria);
|
||||
return saved;
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateCategoriaDto) {
|
||||
const categoria = await this.repo.findOne({ where: { idCategoria: id } });
|
||||
if (!categoria) throw new NotFoundException('Categoría no encontrada');
|
||||
|
||||
if (dto.nombre) {
|
||||
const exists = await this.repo.findOne({ where: { nombre: dto.nombre } });
|
||||
if (exists && exists.idCategoria !== id) {
|
||||
throw new ConflictException('Ya existe otra categoría con ese nombre');
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(categoria, dto);
|
||||
const saved = await this.repo.save(categoria);
|
||||
return saved;
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const categoria = await this.repo.findOne({ where: { idCategoria: id } });
|
||||
if (!categoria) throw new NotFoundException('Categoría no encontrada');
|
||||
await this.repo.remove(categoria);
|
||||
}
|
||||
}
|
||||
14
src/modules/categoria/dto/categoria.dto.ts
Normal file
14
src/modules/categoria/dto/categoria.dto.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { IsString, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CategoriaDto {
|
||||
@ApiProperty({ example: 'uuid-id' })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
idCategoria: string;
|
||||
|
||||
@ApiProperty({ example: 'Electrónica' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
nombre: string;
|
||||
}
|
||||
4
src/modules/categoria/dto/update-categoria.dto.ts
Normal file
4
src/modules/categoria/dto/update-categoria.dto.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CategoriaDto } from './categoria.dto.js';
|
||||
|
||||
export class UpdateCategoriaDto extends PartialType(CategoriaDto) {}
|
||||
10
src/modules/categoria/entities/categoria.entity.ts
Normal file
10
src/modules/categoria/entities/categoria.entity.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Categoria {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
idCategoria: string;
|
||||
|
||||
@Column()
|
||||
nombre: string;
|
||||
}
|
||||
28
src/modules/emprendimiento/dto/emprendimiento.dto.ts
Normal file
28
src/modules/emprendimiento/dto/emprendimiento.dto.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { IsString, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class EmprendimientoDto {
|
||||
@ApiProperty({ example: 'uuid-id' })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
idEmprendimiento?: string;
|
||||
|
||||
@ApiProperty({ example: 'Mi Emprendimiento' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
emprendimiento?: string;
|
||||
|
||||
@ApiProperty({ example: 'Juan Perez' })
|
||||
@IsString()
|
||||
propietario?: string;
|
||||
|
||||
@ApiProperty({ example: '591' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
codigoPais: string; // Solo dígitos, sin +
|
||||
|
||||
@ApiProperty({ example: '70123456' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
whatsapp: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import { PartialType } from '@nestjs/swagger';
|
||||
import { EmprendimientoDto } from './emprendimiento.dto.js';
|
||||
|
||||
export class UpdateEmprendimientoDto extends PartialType(EmprendimientoDto) {}
|
||||
176
src/modules/emprendimiento/emprendimiento.controller.ts
Normal file
176
src/modules/emprendimiento/emprendimiento.controller.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import { Controller, Get, Post, Body, Patch, Param, Delete, HttpStatus } from '@nestjs/common';
|
||||
import { EmprendimientoService } from './emprendimiento.service.js';
|
||||
import { EmprendimientoDto } from './dto/emprendimiento.dto.js';
|
||||
import { UpdateEmprendimientoDto } from './dto/update-emprendimiento.dto.js';
|
||||
import { ApiResult, defaultApiResult } from '../../shared/interfaces/api.result.js';
|
||||
import { FindDto } from '../../shared/class/find.dto.js';
|
||||
import { routeEmprendimiento } from './emprendimiento.router.js';
|
||||
|
||||
@Controller('emprendimiento')
|
||||
export class EmprendimientoController {
|
||||
constructor(private readonly emprendimientoService: EmprendimientoService) { }
|
||||
|
||||
@Post()
|
||||
async create(@Body() emprendimientoDto: EmprendimientoDto): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeEmprendimiento.create.title,
|
||||
routeEmprendimiento.create.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.emprendimientoService.create(emprendimientoDto);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = [result.object];
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Get(':idEmprendimiento')
|
||||
async findOne(@Param('idEmprendimiento') idEmprendimiento: string): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeEmprendimiento.one.title,
|
||||
routeEmprendimiento.one.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.emprendimientoService.findOne(idEmprendimiento);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = [result.object];
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Post('find')
|
||||
async find(@Body() findByDto: FindDto): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeEmprendimiento.find.title,
|
||||
routeEmprendimiento.find.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const {
|
||||
orderByDirection,
|
||||
orderByAttribute,
|
||||
whereConditions,
|
||||
limit,
|
||||
offset,
|
||||
} = findByDto;
|
||||
|
||||
const result = await this.emprendimientoService.find(
|
||||
whereConditions,
|
||||
orderByAttribute,
|
||||
orderByDirection,
|
||||
limit,
|
||||
offset,
|
||||
);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = result.data;
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Patch(':idEmprendimiento')
|
||||
async update(
|
||||
@Param('idEmprendimiento') idEmprendimiento: number,
|
||||
@Body() updateEmprendimientoDto: UpdateEmprendimientoDto,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeEmprendimiento.update.title,
|
||||
routeEmprendimiento.update.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.emprendimientoService.update(
|
||||
idEmprendimiento,
|
||||
updateEmprendimientoDto,
|
||||
);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = [result.object];
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Delete(':idEmprendimiento')
|
||||
async remove(@Param('idEmprendimiento') idEmprendimiento: number): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeEmprendimiento.remove.title,
|
||||
routeEmprendimiento.remove.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.emprendimientoService.remove(idEmprendimiento);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
}
|
||||
13
src/modules/emprendimiento/emprendimiento.module.ts
Normal file
13
src/modules/emprendimiento/emprendimiento.module.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { EmprendimientoService } from './emprendimiento.service.js';
|
||||
import { EmprendimientoController } from './emprendimiento.controller.js';
|
||||
import { Emprendimiento } from './entities/emprendimiento.entity.js';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Emprendimiento])],
|
||||
controllers: [EmprendimientoController],
|
||||
providers: [EmprendimientoService],
|
||||
exports: [EmprendimientoService],
|
||||
})
|
||||
export class EmprendimientoModule {}
|
||||
26
src/modules/emprendimiento/emprendimiento.router.ts
Normal file
26
src/modules/emprendimiento/emprendimiento.router.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export const routeEmprendimiento = {
|
||||
multiple: {
|
||||
route: '[POST].../v1/emprendimiento/multiple',
|
||||
title: 'Create Multiple Emprendimiento',
|
||||
},
|
||||
create: {
|
||||
route: '[POST].../v1/emprendimiento',
|
||||
title: 'Create Emprendimiento',
|
||||
},
|
||||
one: {
|
||||
route: '[GET].../v1/emprendimiento/:idEmprendimiento',
|
||||
title: 'Find One Emprendimiento',
|
||||
},
|
||||
find: {
|
||||
route: '[POST].../v1/emprendimiento/find',
|
||||
title: 'Find Emprendimiento WhereCondition[], Attribute & Direccion',
|
||||
},
|
||||
update: {
|
||||
route: '[PATCH].../v1/emprendimiento/:idEmprendimiento',
|
||||
title: 'Update Emprendimiento',
|
||||
},
|
||||
remove: {
|
||||
route: '[DELETE].../v1/emprendimiento/:idEmprendimiento',
|
||||
title: 'Remove Emprendimiento',
|
||||
},
|
||||
};
|
||||
135
src/modules/emprendimiento/emprendimiento.service.ts
Normal file
135
src/modules/emprendimiento/emprendimiento.service.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { EmprendimientoDto } from './dto/emprendimiento.dto.js';
|
||||
import { UpdateEmprendimientoDto } from './dto/update-emprendimiento.dto.js';
|
||||
import { defaultServiceResult, ServiceResult } from '../../shared/interfaces/service.result.js';
|
||||
import { WhereCondition } from '../../shared/type/query-builder.types.js';
|
||||
import { Emprendimiento } from './entities/emprendimiento.entity.js';
|
||||
|
||||
@Injectable()
|
||||
export class EmprendimientoService {
|
||||
constructor(
|
||||
@InjectRepository(Emprendimiento)
|
||||
private emprendimientoRepository: Repository<Emprendimiento>,
|
||||
) { }
|
||||
|
||||
async create(emprendimientoDto: EmprendimientoDto): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
const result = this.emprendimientoRepository.create(emprendimientoDto);
|
||||
await this.emprendimientoRepository.save(result);
|
||||
|
||||
serviceResult.boolean = true;
|
||||
serviceResult.message = 'Emprendimiento se ha agregado correctamente.';
|
||||
serviceResult.number = 1;
|
||||
serviceResult.object = result;
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async findOne(idEmprendimiento: string): Promise<ServiceResult> {
|
||||
let serviceResult = {
|
||||
boolean: false,
|
||||
message: '',
|
||||
number: 0,
|
||||
object: null,
|
||||
data: null,
|
||||
} as ServiceResult;
|
||||
|
||||
const result = await this.emprendimientoRepository.findOneBy({ idEmprendimiento });
|
||||
|
||||
if (result) {
|
||||
serviceResult.boolean = true;
|
||||
serviceResult.message = 'Existe un Emprendimiento.';
|
||||
serviceResult.number = 1;
|
||||
serviceResult.object = result;
|
||||
} else {
|
||||
serviceResult.message = 'No existe Emprendimiento.';
|
||||
}
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async find(
|
||||
whereConditions: WhereCondition[],
|
||||
orderByAttribute: string,
|
||||
orderByDirection: 'ASC' | 'DESC',
|
||||
limit: number,
|
||||
offset: number,
|
||||
): Promise<ServiceResult> {
|
||||
const serviceResult = defaultServiceResult();
|
||||
|
||||
try {
|
||||
let query = this.emprendimientoRepository.createQueryBuilder('emprendimiento');
|
||||
|
||||
whereConditions.forEach((condition, index) => {
|
||||
const { attribute, value, operator = '=' } = condition;
|
||||
const paramKey = `param${index}`;
|
||||
const param = {
|
||||
[paramKey]: operator === 'like' ? `%${value}%` : value,
|
||||
};
|
||||
const whereMethod = index === 0 ? 'where' : 'andWhere';
|
||||
|
||||
query = query[whereMethod](
|
||||
`emprendimiento.${attribute} ${operator} :${paramKey}`,
|
||||
param,
|
||||
);
|
||||
});
|
||||
|
||||
query = query.orderBy(`emprendimiento.${orderByAttribute}`, orderByDirection);
|
||||
|
||||
const totalRegistros = await query.getCount();
|
||||
const skip = (offset - 1) * limit;
|
||||
|
||||
query = query.skip(skip).take(limit);
|
||||
|
||||
const result = await query.getMany();
|
||||
const count = result.length;
|
||||
|
||||
serviceResult.boolean = count > 0;
|
||||
serviceResult.message = `${count} Emprendimiento(s) encontrado(s).`;
|
||||
serviceResult.number = totalRegistros;
|
||||
serviceResult.data = result;
|
||||
} catch (error: any) {
|
||||
serviceResult.boolean = false;
|
||||
serviceResult.message = `Error en el servicio: ${error.message}`;
|
||||
}
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async update(
|
||||
idEmprendimiento: number,
|
||||
updateEmprendimientoDto: UpdateEmprendimientoDto,
|
||||
): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
const result = await this.emprendimientoRepository.update(
|
||||
idEmprendimiento,
|
||||
updateEmprendimientoDto,
|
||||
);
|
||||
|
||||
serviceResult.boolean = result.affected === 1 ? true : false;
|
||||
serviceResult.message = 'Se ha actualizado correctamente.';
|
||||
serviceResult.number = result.affected || 0;
|
||||
serviceResult.object = result;
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async remove(idEmprendimiento: number): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
const result = await this.emprendimientoRepository.delete(idEmprendimiento);
|
||||
|
||||
serviceResult.boolean = result.affected === 1 ? true : false;
|
||||
serviceResult.message =
|
||||
result.affected === 1
|
||||
? 'Se ha eliminado correctamente.'
|
||||
: 'No se ha encontrado el Emprendimiento.';
|
||||
serviceResult.number = result.affected || 0;
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
}
|
||||
19
src/modules/emprendimiento/entities/emprendimiento.entity.ts
Normal file
19
src/modules/emprendimiento/entities/emprendimiento.entity.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Emprendimiento {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
idEmprendimiento: string;
|
||||
|
||||
@Column()
|
||||
emprendimiento: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
propietario: string;
|
||||
|
||||
@Column()
|
||||
codigoPais: string;
|
||||
|
||||
@Column()
|
||||
whatsapp: string;
|
||||
}
|
||||
34
src/modules/empresa/dto/empresa.dto.ts
Normal file
34
src/modules/empresa/dto/empresa.dto.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString, IsNumber } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class EmpresaDto {
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 'La Clave' })
|
||||
@IsString()
|
||||
nombre: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 5 })
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
diasIncluidos: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 5.00 })
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
costoBase: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 2.00 })
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
costoDiaAdicional: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 15 })
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
diasParaAlmacen: number;
|
||||
}
|
||||
4
src/modules/empresa/dto/update-empresa.dto.ts
Normal file
4
src/modules/empresa/dto/update-empresa.dto.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { PartialType } from '@nestjs/swagger';
|
||||
import { EmpresaDto } from './empresa.dto.js';
|
||||
|
||||
export class UpdateEmpresaDto extends PartialType(EmpresaDto) { }
|
||||
107
src/modules/empresa/empresa.controller.ts
Normal file
107
src/modules/empresa/empresa.controller.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { Controller, Get, Post, Patch, Delete, Body, Param, UseInterceptors, UploadedFile, HttpStatus } from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { EmpresaService } from './empresa.service.js';
|
||||
import { EmpresaDto } from './dto/empresa.dto.js';
|
||||
import { UpdateEmpresaDto } from './dto/update-empresa.dto.js';
|
||||
import { ApiResult, defaultApiResult } from '../../shared/interfaces/api.result.js';
|
||||
import { routeEmpresa } from './empresa.router.js';
|
||||
|
||||
@Controller('empresa')
|
||||
export class EmpresaController {
|
||||
constructor(private readonly service: EmpresaService) {}
|
||||
|
||||
@Get()
|
||||
async findOne(): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeEmpresa.one.title,
|
||||
routeEmpresa.one.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.service.findOne();
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = [result.object];
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status;
|
||||
apiResult.message = error;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseInterceptors(FileInterceptor('logo'))
|
||||
async create(
|
||||
@Body() dto: EmpresaDto,
|
||||
@UploadedFile() logo?: Express.Multer.File,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeEmpresa.create.title,
|
||||
routeEmpresa.create.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.service.create(dto, logo);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = [result.object];
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status;
|
||||
apiResult.message = error;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Patch()
|
||||
@UseInterceptors(FileInterceptor('logo'))
|
||||
async update(
|
||||
@Body() dto: UpdateEmpresaDto,
|
||||
@UploadedFile() logo?: Express.Multer.File,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeEmpresa.update.title,
|
||||
routeEmpresa.update.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.service.update(dto, logo);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = [result.object];
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status;
|
||||
apiResult.message = error;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
}
|
||||
18
src/modules/empresa/empresa.module.ts
Normal file
18
src/modules/empresa/empresa.module.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MulterModule } from '@nestjs/platform-express';
|
||||
import multer from 'multer';
|
||||
import { EmpresaController } from './empresa.controller.js';
|
||||
import { EmpresaService } from './empresa.service.js';
|
||||
import { Empresa } from './entities/empresa.entity.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Empresa]),
|
||||
MulterModule.register({ storage: multer.memoryStorage() }),
|
||||
],
|
||||
controllers: [EmpresaController],
|
||||
providers: [EmpresaService],
|
||||
exports: [EmpresaService],
|
||||
})
|
||||
export class EmpresaModule {}
|
||||
26
src/modules/empresa/empresa.router.ts
Normal file
26
src/modules/empresa/empresa.router.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export const routeEmpresa = {
|
||||
multiple: {
|
||||
route: '[POST].../v1/empresa/multiple',
|
||||
title: 'Create Multiple Empresa',
|
||||
},
|
||||
create: {
|
||||
route: '[POST].../v1/empresa',
|
||||
title: 'Create Empresa',
|
||||
},
|
||||
one: {
|
||||
route: '[GET].../v1/empresa/:idEmpresa',
|
||||
title: 'Find One Empresa',
|
||||
},
|
||||
find: {
|
||||
route: '[POST].../v1/empresa/find',
|
||||
title: 'Find Empresa WhereCondition[], Attribute & Direccion',
|
||||
},
|
||||
update: {
|
||||
route: '[PATCH].../v1/empresa/:idEmpresa',
|
||||
title: 'Update Empresa',
|
||||
},
|
||||
remove: {
|
||||
route: '[DELETE].../v1/empresa/:idEmpresa',
|
||||
title: 'Remove Empresa',
|
||||
},
|
||||
};
|
||||
193
src/modules/empresa/empresa.service.ts
Normal file
193
src/modules/empresa/empresa.service.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import sharp from 'sharp';
|
||||
import { Empresa } from './entities/empresa.entity.js';
|
||||
import { EmpresaDto } from './dto/empresa.dto.js';
|
||||
import { UpdateEmpresaDto } from './dto/update-empresa.dto.js';
|
||||
import { defaultServiceResult, ServiceResult } from '../../shared/interfaces/service.result.js';
|
||||
import { WhereCondition } from '../../shared/type/query-builder.types.js';
|
||||
|
||||
@Injectable()
|
||||
export class EmpresaService {
|
||||
private uploadsPath = path.join(process.cwd(), 'uploads', 'empresas');
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Empresa)
|
||||
private repo: Repository<Empresa>,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
fs.mkdirSync(this.uploadsPath, { recursive: true });
|
||||
}
|
||||
|
||||
async findOne(): Promise<ServiceResult> {
|
||||
let serviceResult = {
|
||||
boolean: false,
|
||||
message: '',
|
||||
number: 0,
|
||||
object: null,
|
||||
data: null,
|
||||
} as ServiceResult;
|
||||
|
||||
const result = await this.repo.findOne({ where: {} });
|
||||
|
||||
if (result) {
|
||||
serviceResult.boolean = true;
|
||||
serviceResult.message = 'Existe una Empresa.';
|
||||
serviceResult.number = 1;
|
||||
serviceResult.object = result;
|
||||
} else {
|
||||
serviceResult.message = 'No existe Empresa.';
|
||||
}
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async create(dto: EmpresaDto, logo?: Express.Multer.File): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
const count = await this.repo.count();
|
||||
if (count > 0) {
|
||||
serviceResult.message = 'Ya existe una empresa registrada. Solo se permite una empresa por instancia.';
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
let logoUrl: string | undefined;
|
||||
if (logo) {
|
||||
logoUrl = await this.processLogo(logo);
|
||||
}
|
||||
|
||||
const result = this.repo.create({ ...dto, logoUrl });
|
||||
await this.repo.save(result);
|
||||
|
||||
serviceResult.boolean = true;
|
||||
serviceResult.message = 'Empresa se ha agregado correctamente.';
|
||||
serviceResult.number = 1;
|
||||
serviceResult.object = result;
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async update(dto: UpdateEmpresaDto, logo?: Express.Multer.File): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
const empresa = await this.repo.findOne({ where: {} });
|
||||
if (!empresa) {
|
||||
serviceResult.message = 'Empresa no encontrada.';
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
if (logo) {
|
||||
if (empresa.logoUrl) {
|
||||
const oldPath = path.join(process.cwd(), empresa.logoUrl);
|
||||
if (fs.existsSync(oldPath)) {
|
||||
fs.unlinkSync(oldPath);
|
||||
}
|
||||
}
|
||||
empresa.logoUrl = await this.processLogo(logo);
|
||||
}
|
||||
|
||||
Object.assign(empresa, dto);
|
||||
const result = await this.repo.save(empresa);
|
||||
|
||||
serviceResult.boolean = true;
|
||||
serviceResult.message = 'Se ha actualizado correctamente.';
|
||||
serviceResult.number = 1;
|
||||
serviceResult.object = result;
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async find(
|
||||
whereConditions: WhereCondition[],
|
||||
orderByAttribute: string,
|
||||
orderByDirection: 'ASC' | 'DESC',
|
||||
limit: number,
|
||||
offset: number,
|
||||
): Promise<ServiceResult> {
|
||||
const serviceResult = defaultServiceResult();
|
||||
|
||||
try {
|
||||
let query = this.repo.createQueryBuilder('empresa');
|
||||
|
||||
whereConditions.forEach((condition, index) => {
|
||||
const { attribute, value, operator = '=' } = condition;
|
||||
const paramKey = `param${index}`;
|
||||
const param = {
|
||||
[paramKey]: operator === 'like' ? `%${value}%` : value,
|
||||
};
|
||||
const whereMethod = index === 0 ? 'where' : 'andWhere';
|
||||
|
||||
query = query[whereMethod](
|
||||
`empresa.${attribute} ${operator} :${paramKey}`,
|
||||
param,
|
||||
);
|
||||
});
|
||||
|
||||
query = query.orderBy(`empresa.${orderByAttribute}`, orderByDirection);
|
||||
|
||||
const totalRegistros = await query.getCount();
|
||||
const skip = (offset - 1) * limit;
|
||||
|
||||
query = query.skip(skip).take(limit);
|
||||
|
||||
const result = await query.getMany();
|
||||
const count = result.length;
|
||||
|
||||
serviceResult.boolean = count > 0;
|
||||
serviceResult.message = `${count} Empresa(s) encontrado(s).`;
|
||||
serviceResult.number = totalRegistros;
|
||||
serviceResult.data = result;
|
||||
} catch (error: any) {
|
||||
serviceResult.boolean = false;
|
||||
serviceResult.message = `Error en el servicio: ${error.message}`;
|
||||
}
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
private async processLogo(file: Express.Multer.File): Promise<string> {
|
||||
this.validateFile(file);
|
||||
|
||||
const maxSizeKB = Number(this.configService.get('EMPRESA_LOGO_MAX_SIZE', 200));
|
||||
const width = Number(this.configService.get('EMPRESA_LOGO_WIDTH', 400));
|
||||
const height = Number(this.configService.get('EMPRESA_LOGO_HEIGHT', 400));
|
||||
const quality = Number(this.configService.get('EMPRESA_LOGO_QUALITY', 80));
|
||||
|
||||
let buffer = file.buffer;
|
||||
let currentQuality = quality;
|
||||
|
||||
buffer = await sharp(file.buffer)
|
||||
.resize({ width, height, fit: 'inside', withoutEnlargement: true })
|
||||
.jpeg({ quality: currentQuality })
|
||||
.toBuffer();
|
||||
|
||||
while (buffer.length > maxSizeKB * 1024 && currentQuality > 10) {
|
||||
currentQuality -= 10;
|
||||
buffer = await sharp(file.buffer)
|
||||
.resize({ width, height, fit: 'inside', withoutEnlargement: true })
|
||||
.jpeg({ quality: currentQuality })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
const filename = `${uuidv4()}.jpg`;
|
||||
const filePath = path.join(this.uploadsPath, filename);
|
||||
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
|
||||
return `/uploads/empresas/${filename}`;
|
||||
}
|
||||
|
||||
private validateFile(file: Express.Multer.File): void {
|
||||
if (!file) return;
|
||||
|
||||
const allowedMimes = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
if (!allowedMimes.includes(file.mimetype)) {
|
||||
throw new Error('Formato de archivo no soportado. Use JPEG, PNG o WebP');
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/modules/empresa/entities/empresa.entity.ts
Normal file
25
src/modules/empresa/entities/empresa.entity.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Empresa {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
idEmpresa: string;
|
||||
|
||||
@Column()
|
||||
nombre: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
logoUrl: string;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
diasIncluidos: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
costoBase: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
costoDiaAdicional: number;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
diasParaAlmacen: number;
|
||||
}
|
||||
19
src/modules/feriado/dto/feriado.dto.ts
Normal file
19
src/modules/feriado/dto/feriado.dto.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString, IsDateString, IsUUID, IsOptional } from 'class-validator';
|
||||
|
||||
export class FeriadoDto {
|
||||
@ApiProperty({ example: 'uuid-id' })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
idFeriado: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsDateString()
|
||||
@ApiProperty({ example: '2026-01-01' })
|
||||
fecha: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'Año Nuevo' })
|
||||
nombre: string;
|
||||
}
|
||||
4
src/modules/feriado/dto/update-feriado.dto.ts
Normal file
4
src/modules/feriado/dto/update-feriado.dto.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { PartialType } from '@nestjs/swagger';
|
||||
import { FeriadoDto } from './feriado.dto.js';
|
||||
|
||||
export class UpdateFeriadoDto extends PartialType(FeriadoDto) { }
|
||||
13
src/modules/feriado/entities/feriado.entity.ts
Normal file
13
src/modules/feriado/entities/feriado.entity.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Feriado {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
idFeriado: string;
|
||||
|
||||
@Column({ type: 'date' })
|
||||
fecha: Date;
|
||||
|
||||
@Column()
|
||||
nombre: string;
|
||||
}
|
||||
190
src/modules/feriado/feriado.controller.ts
Normal file
190
src/modules/feriado/feriado.controller.ts
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import { Controller, Get, Post, Body, Patch, Param, Delete, HttpStatus } from '@nestjs/common';
|
||||
import { FeriadoService } from './feriado.service.js';
|
||||
import { FeriadoDto } from './dto/feriado.dto.js';
|
||||
import { UpdateFeriadoDto } from './dto/update-feriado.dto.js';
|
||||
import { ApiResult, defaultApiResult } from '../../shared/interfaces/api.result.js';
|
||||
import { routeFeriado } from './feriado.router.js';
|
||||
|
||||
@Controller('feriado')
|
||||
export class FeriadoController {
|
||||
constructor(private readonly feriadoService: FeriadoService) { }
|
||||
|
||||
@Get()
|
||||
async findAll(): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeFeriado.find.title,
|
||||
routeFeriado.find.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.feriadoService.findAll();
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = result.data;
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Get(':idFeriado')
|
||||
async findOne(@Param('idFeriado') idFeriado: string): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeFeriado.one.title,
|
||||
routeFeriado.one.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.feriadoService.findOne(idFeriado);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = [result.object];
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message || 'Error interno del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Body() feriadoDto: FeriadoDto): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeFeriado.create.title,
|
||||
routeFeriado.create.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.feriadoService.create(feriadoDto);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = [result.object];
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message || 'Error interno del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Post('generar/:year')
|
||||
async generar(@Param('year') year: number): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeFeriado.create.title,
|
||||
routeFeriado.create.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.feriadoService.generar(year);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = result.data;
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message || 'Error interno del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Patch(':idFeriado')
|
||||
async update(
|
||||
@Param('idFeriado') idFeriado: string,
|
||||
@Body() updateFeriadoDto: UpdateFeriadoDto,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeFeriado.update.title,
|
||||
routeFeriado.update.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.feriadoService.update(
|
||||
idFeriado,
|
||||
updateFeriadoDto,
|
||||
);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = [result.object];
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message || 'Error interno del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Delete(':idFeriado')
|
||||
async remove(@Param('idFeriado') idFeriado: string): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeFeriado.remove.title,
|
||||
routeFeriado.remove.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.feriadoService.remove(idFeriado);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message || 'Error interno del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
}
|
||||
13
src/modules/feriado/feriado.module.ts
Normal file
13
src/modules/feriado/feriado.module.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { FeriadoController } from './feriado.controller.js';
|
||||
import { FeriadoService } from './feriado.service.js';
|
||||
import { Feriado } from './entities/feriado.entity.js';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Feriado])],
|
||||
controllers: [FeriadoController],
|
||||
providers: [FeriadoService],
|
||||
exports: [FeriadoService],
|
||||
})
|
||||
export class FeriadoModule {}
|
||||
26
src/modules/feriado/feriado.router.ts
Normal file
26
src/modules/feriado/feriado.router.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export const routeFeriado = {
|
||||
multiple: {
|
||||
route: '[POST].../v1/feriado/multiple',
|
||||
title: 'Create Multiple Feriado',
|
||||
},
|
||||
create: {
|
||||
route: '[POST].../v1/feriado',
|
||||
title: 'Create Feriado',
|
||||
},
|
||||
one: {
|
||||
route: '[GET].../v1/feriado/:idFeriado',
|
||||
title: 'Find One Feriado',
|
||||
},
|
||||
find: {
|
||||
route: '[POST].../v1/feriado/find',
|
||||
title: 'Find Feriado WhereCondition[], Attribute & Direccion',
|
||||
},
|
||||
update: {
|
||||
route: '[PATCH].../v1/feriado/:idFeriado',
|
||||
title: 'Update Feriado',
|
||||
},
|
||||
remove: {
|
||||
route: '[DELETE].../v1/feriado/:idFeriado',
|
||||
title: 'Remove Feriado',
|
||||
},
|
||||
};
|
||||
168
src/modules/feriado/feriado.service.ts
Normal file
168
src/modules/feriado/feriado.service.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { FeriadoDto } from './dto/feriado.dto.js';
|
||||
import { UpdateFeriadoDto } from './dto/update-feriado.dto.js';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { defaultServiceResult, ServiceResult } from '../../shared/interfaces/service.result.js';
|
||||
import { Feriado } from './entities/feriado.entity.js';
|
||||
import { generarFeriadosBolivia } from '../../shared/utils/feriados.util.js';
|
||||
|
||||
@Injectable()
|
||||
export class FeriadoService {
|
||||
constructor(
|
||||
@InjectRepository(Feriado)
|
||||
private feriadoRepository: Repository<Feriado>,
|
||||
) { }
|
||||
|
||||
async findAll(): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
const result = await this.feriadoRepository.find({ order: { fecha: 'ASC' } });
|
||||
|
||||
serviceResult.boolean = result.length > 0;
|
||||
serviceResult.message = result.length > 0
|
||||
? `${result.length} feriado(s) encontrado(s).`
|
||||
: 'No se encontraron feriados.';
|
||||
serviceResult.number = result.length;
|
||||
serviceResult.data = result;
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async findOne(idFeriado: string): Promise<ServiceResult> {
|
||||
let serviceResult = {
|
||||
boolean: false,
|
||||
message: '',
|
||||
number: 0,
|
||||
object: null,
|
||||
data: null,
|
||||
} as ServiceResult;
|
||||
|
||||
const result = await this.feriadoRepository.findOneBy({ idFeriado });
|
||||
|
||||
if (result) {
|
||||
serviceResult.boolean = true;
|
||||
serviceResult.message = 'Existe un Feriado.';
|
||||
serviceResult.number = 1;
|
||||
serviceResult.object = result;
|
||||
} else {
|
||||
serviceResult.message = 'No existe Feriado.';
|
||||
}
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async create(feriadoDto: FeriadoDto): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
const fechaDate = new Date(feriadoDto.fecha + 'T00:00:00');
|
||||
const exists = await this.feriadoRepository.findOne({ where: { fecha: fechaDate } });
|
||||
|
||||
if (exists) {
|
||||
serviceResult.message = 'Ya existe un feriado en esa fecha.';
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
const result = this.feriadoRepository.create({ ...feriadoDto, fecha: fechaDate });
|
||||
await this.feriadoRepository.save(result);
|
||||
|
||||
serviceResult.boolean = true;
|
||||
serviceResult.message = 'Feriado se ha agregado correctamente.';
|
||||
serviceResult.number = 1;
|
||||
serviceResult.object = result;
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async generar(year: number): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
try {
|
||||
const feriadosGenerados = generarFeriadosBolivia(year);
|
||||
|
||||
const existentes = await this.feriadoRepository
|
||||
.createQueryBuilder('f')
|
||||
.where('EXTRACT(YEAR FROM f.fecha) = :year', { year })
|
||||
.getMany();
|
||||
|
||||
const existentesFechas = new Set(existentes.map((f) => new Date(f.fecha).toISOString().split('T')[0]));
|
||||
const nuevos = feriadosGenerados.filter((f) => !existentesFechas.has(f.fecha));
|
||||
|
||||
if (nuevos.length === 0) {
|
||||
serviceResult.message = `Ya existen feriados registrados para el año ${year}`;
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
const feriados = this.feriadoRepository.create(
|
||||
nuevos.map((f) => ({
|
||||
fecha: new Date(f.fecha + 'T00:00:00'),
|
||||
nombre: f.nombre,
|
||||
})),
|
||||
);
|
||||
const saved = await this.feriadoRepository.save(feriados);
|
||||
|
||||
serviceResult.boolean = true;
|
||||
serviceResult.message = `Se generaron ${saved.length} feriados para el año ${year}`;
|
||||
serviceResult.number = saved.length;
|
||||
serviceResult.data = saved;
|
||||
} catch (error: any) {
|
||||
serviceResult.message = `Error al generar feriados: ${error.message}`;
|
||||
}
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async update(
|
||||
idFeriado: string,
|
||||
updateFeriadoDto: UpdateFeriadoDto,
|
||||
): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
const feriado = await this.feriadoRepository.findOneBy({ idFeriado });
|
||||
|
||||
if (!feriado) {
|
||||
serviceResult.message = 'No existe Feriado.';
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
if (updateFeriadoDto.fecha) {
|
||||
const fechaDate = new Date(updateFeriadoDto.fecha + 'T00:00:00');
|
||||
const fechaActual = new Date(feriado.fecha);
|
||||
const fechaStr = fechaActual.toISOString().split('T')[0];
|
||||
if (updateFeriadoDto.fecha !== fechaStr) {
|
||||
const exists = await this.feriadoRepository.findOne({ where: { fecha: fechaDate } });
|
||||
if (exists) {
|
||||
serviceResult.message = 'Ya existe un feriado en esa fecha.';
|
||||
return serviceResult;
|
||||
}
|
||||
}
|
||||
Object.assign(feriado, { ...updateFeriadoDto, fecha: fechaDate });
|
||||
} else {
|
||||
Object.assign(feriado, updateFeriadoDto);
|
||||
}
|
||||
|
||||
const result = await this.feriadoRepository.save(feriado);
|
||||
|
||||
serviceResult.boolean = true;
|
||||
serviceResult.message = 'Se ha actualizado correctamente.';
|
||||
serviceResult.number = 1;
|
||||
serviceResult.object = result;
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async remove(idFeriado: string): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
const result = await this.feriadoRepository.delete(idFeriado);
|
||||
|
||||
serviceResult.boolean = result.affected === 1 ? true : false;
|
||||
serviceResult.message =
|
||||
result.affected === 1
|
||||
? 'Se ha eliminado correctamente.'
|
||||
: 'No se ha encontrado el Feriado.';
|
||||
serviceResult.number = result.affected || 0;
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
}
|
||||
47
src/modules/mensaje/dto/mensaje.dto.ts
Normal file
47
src/modules/mensaje/dto/mensaje.dto.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { IsString, IsNotEmpty, IsEnum, IsOptional, IsUUID } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class MensajeWhatsAppDto {
|
||||
@ApiProperty({ example: 'uuid-id' })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
idMensajeWhatsApp?: string;
|
||||
|
||||
@ApiProperty({ example: 'uuid-paquete' })
|
||||
@IsString()
|
||||
idPaquete: string;
|
||||
|
||||
@ApiProperty({ example: '59170123456' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
telefonoDestino: string;
|
||||
|
||||
@ApiProperty({ enum: ['RECEPCION_PAQUETE', 'PAQUETE_ENTREGADO'] })
|
||||
@IsEnum(['RECEPCION_PAQUETE', 'PAQUETE_ENTREGADO'])
|
||||
tipo: string;
|
||||
|
||||
@ApiProperty({ example: 'Tu paquete S1-0001-A está listo para recoger' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
mensaje1: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Encuéntranos en Av. Principal #123' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
mensaje2?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Horarios: 08:00 - 18:00' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
mensaje3?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '/uploads/qr/abc.png' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
urlQR?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
sessionId?: string;
|
||||
}
|
||||
19
src/modules/mensaje/dto/update-estado.dto.ts
Normal file
19
src/modules/mensaje/dto/update-estado.dto.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { IsString, IsNotEmpty, IsOptional, IsDateString } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class UpdateEstadoDto {
|
||||
@ApiProperty({ example: 'ENVIADO' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
estado: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Error de conexión' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
error?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
fechaEnvio?: string;
|
||||
}
|
||||
48
src/modules/mensaje/entities/mensaje.entity.ts
Normal file
48
src/modules/mensaje/entities/mensaje.entity.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class MensajeWhatsApp {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
idMensajeWhatsApp: string;
|
||||
|
||||
@Column()
|
||||
idPaquete: string;
|
||||
|
||||
@Column()
|
||||
telefonoDestino: string;
|
||||
|
||||
@Column({ type: 'enum', enum: ['RECEPCION_PAQUETE', 'PAQUETE_ENTREGADO', 'NOTIFICACION_EMPRENDEDOR'] })
|
||||
tipo: string;
|
||||
|
||||
// Mensajes (RECEPCION_PAQUETE usa los 3, PAQUETE_ENTREGADO solo mensaje1)
|
||||
@Column({ type: 'text' })
|
||||
mensaje1: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
mensaje2: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
mensaje3: string;
|
||||
|
||||
// QR (solo RECEPCION_PAQUETE)
|
||||
@Column({ nullable: true })
|
||||
urlQR: string;
|
||||
|
||||
@Column({ type: 'enum', enum: ['PENDIENTE', 'ENVIANDO', 'ENVIADO', 'ERROR'], default: 'PENDIENTE' })
|
||||
estado: string;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
intentos: number;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
error: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
sessionId: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
fechaCreacion: Date;
|
||||
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
fechaEnvio: Date;
|
||||
}
|
||||
182
src/modules/mensaje/mensaje.controller.ts
Normal file
182
src/modules/mensaje/mensaje.controller.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { Controller, Get, Post, Put, Body, Param, Query, HttpStatus } from '@nestjs/common';
|
||||
import { MensajeService } from './mensaje.service.js';
|
||||
import { MensajeWhatsAppDto } from './dto/mensaje.dto.js';
|
||||
import { UpdateEstadoDto } from './dto/update-estado.dto.js';
|
||||
import { ApiResult, defaultApiResult } from '../../shared/interfaces/api.result.js';
|
||||
import { FindDto } from '../../shared/class/find.dto.js';
|
||||
import { routeMensaje } from './mensaje.router.js';
|
||||
import { Public } from '../../shared/interfaces/auth.interface.js';
|
||||
import { RequireApiKey } from '../../shared/interfaces/api-key.interface.js';
|
||||
|
||||
@Controller('mensaje')
|
||||
export class MensajeController {
|
||||
constructor(private readonly service: MensajeService) {}
|
||||
|
||||
@Get('paquete/:idPaquete')
|
||||
async findByPaquete(@Param('idPaquete') idPaquete: string): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeMensaje.find.title,
|
||||
routeMensaje.find.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.service.findByPaquete(idPaquete);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = result.data;
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Get('pendientes')
|
||||
@Public()
|
||||
@RequireApiKey()
|
||||
async findPendientes(
|
||||
@Query('limit') limit?: number,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeMensaje.find.title,
|
||||
routeMensaje.find.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.service.findPendientes(limit || 5);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = result.data;
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Body() dto: MensajeWhatsAppDto): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeMensaje.create.title,
|
||||
routeMensaje.create.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.service.create(dto);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = [result.object];
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Post('find')
|
||||
async find(@Body() findByDto: FindDto): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeMensaje.find.title,
|
||||
routeMensaje.find.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const {
|
||||
orderByDirection,
|
||||
orderByAttribute,
|
||||
whereConditions,
|
||||
limit,
|
||||
offset,
|
||||
} = findByDto;
|
||||
|
||||
const result = await this.service.find(
|
||||
whereConditions,
|
||||
orderByAttribute,
|
||||
orderByDirection,
|
||||
limit,
|
||||
offset,
|
||||
);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = result.data;
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Put(':id/estado')
|
||||
@Public()
|
||||
@RequireApiKey()
|
||||
async updateEstado(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateEstadoDto,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeMensaje.update.title,
|
||||
routeMensaje.update.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.service.updateEstado(id, dto);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = [result.object];
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
}
|
||||
13
src/modules/mensaje/mensaje.module.ts
Normal file
13
src/modules/mensaje/mensaje.module.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MensajeController } from './mensaje.controller.js';
|
||||
import { MensajeService } from './mensaje.service.js';
|
||||
import { MensajeWhatsApp } from './entities/mensaje.entity.js';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([MensajeWhatsApp])],
|
||||
controllers: [MensajeController],
|
||||
providers: [MensajeService],
|
||||
exports: [MensajeService],
|
||||
})
|
||||
export class MensajeModule {}
|
||||
26
src/modules/mensaje/mensaje.router.ts
Normal file
26
src/modules/mensaje/mensaje.router.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export const routeMensaje = {
|
||||
multiple: {
|
||||
route: '[POST].../v1/mensaje/multiple',
|
||||
title: 'Create Multiple Mensaje',
|
||||
},
|
||||
create: {
|
||||
route: '[POST].../v1/mensaje',
|
||||
title: 'Create Mensaje',
|
||||
},
|
||||
one: {
|
||||
route: '[GET].../v1/mensaje/:idMensajeWhatsApp',
|
||||
title: 'Find One Mensaje',
|
||||
},
|
||||
find: {
|
||||
route: '[POST].../v1/mensaje/find',
|
||||
title: 'Find Mensaje WhereCondition[], Attribute & Direccion',
|
||||
},
|
||||
update: {
|
||||
route: '[PATCH].../v1/mensaje/:idMensajeWhatsApp',
|
||||
title: 'Update Mensaje',
|
||||
},
|
||||
remove: {
|
||||
route: '[DELETE].../v1/mensaje/:idMensajeWhatsApp',
|
||||
title: 'Remove Mensaje',
|
||||
},
|
||||
};
|
||||
187
src/modules/mensaje/mensaje.service.ts
Normal file
187
src/modules/mensaje/mensaje.service.ts
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { MensajeWhatsApp } from './entities/mensaje.entity.js';
|
||||
import { MensajeWhatsAppDto } from './dto/mensaje.dto.js';
|
||||
import { UpdateEstadoDto } from './dto/update-estado.dto.js';
|
||||
import { defaultServiceResult, ServiceResult } from '../../shared/interfaces/service.result.js';
|
||||
import { WhereCondition } from '../../shared/type/query-builder.types.js';
|
||||
|
||||
@Injectable()
|
||||
export class MensajeService {
|
||||
constructor(
|
||||
@InjectRepository(MensajeWhatsApp)
|
||||
private repo: Repository<MensajeWhatsApp>,
|
||||
) {}
|
||||
|
||||
async findByPaquete(idPaquete: string): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
try {
|
||||
const result = await this.repo.find({
|
||||
where: { idPaquete },
|
||||
order: { fechaEnvio: 'DESC' },
|
||||
});
|
||||
|
||||
serviceResult.boolean = result.length > 0;
|
||||
serviceResult.message = `${result.length} mensaje(s) encontrado(s).`;
|
||||
serviceResult.number = result.length;
|
||||
serviceResult.data = result;
|
||||
} catch (error: any) {
|
||||
serviceResult.boolean = false;
|
||||
serviceResult.message = `Error en el servicio: ${error.message}`;
|
||||
}
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async findPendientes(limit: number = 5): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
try {
|
||||
const mensajes = await this.repo.find({
|
||||
where: { estado: 'PENDIENTE' },
|
||||
order: { fechaCreacion: 'ASC' },
|
||||
take: limit,
|
||||
});
|
||||
|
||||
if (mensajes.length === 0) {
|
||||
serviceResult.boolean = false;
|
||||
serviceResult.message = 'No hay mensajes pendientes.';
|
||||
serviceResult.number = 0;
|
||||
serviceResult.data = [];
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
const data = mensajes.map(m => ({
|
||||
idMensajeWhatsApp: m.idMensajeWhatsApp,
|
||||
idPaquete: m.idPaquete,
|
||||
telefonoDestino: m.telefonoDestino,
|
||||
tipo: m.tipo,
|
||||
mensaje1: m.mensaje1,
|
||||
mensaje2: m.mensaje2,
|
||||
mensaje3: m.mensaje3,
|
||||
urlQR: m.urlQR,
|
||||
estado: m.estado,
|
||||
intentos: m.intentos,
|
||||
sessionId: m.sessionId,
|
||||
fechaCreacion: m.fechaCreacion,
|
||||
}));
|
||||
|
||||
serviceResult.boolean = true;
|
||||
serviceResult.message = `${data.length} mensaje(s) pendiente(s).`;
|
||||
serviceResult.number = data.length;
|
||||
serviceResult.data = data;
|
||||
} catch (error: any) {
|
||||
serviceResult.boolean = false;
|
||||
serviceResult.message = `Error en el servicio: ${error.message}`;
|
||||
}
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async create(dto: MensajeWhatsAppDto): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
try {
|
||||
const mensaje = this.repo.create({
|
||||
...dto,
|
||||
});
|
||||
const result = await this.repo.save(mensaje);
|
||||
|
||||
serviceResult.boolean = true;
|
||||
serviceResult.message = 'Mensaje registrado correctamente.';
|
||||
serviceResult.number = 1;
|
||||
serviceResult.object = result;
|
||||
} catch (error: any) {
|
||||
serviceResult.boolean = false;
|
||||
serviceResult.message = `Error en el servicio: ${error.message}`;
|
||||
}
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async find(
|
||||
whereConditions: WhereCondition[],
|
||||
orderByAttribute: string,
|
||||
orderByDirection: 'ASC' | 'DESC',
|
||||
limit: number,
|
||||
offset: number,
|
||||
): Promise<ServiceResult> {
|
||||
const serviceResult = defaultServiceResult();
|
||||
|
||||
try {
|
||||
let query = this.repo.createQueryBuilder('mensaje');
|
||||
|
||||
whereConditions.forEach((condition, index) => {
|
||||
const { attribute, value, operator = '=' } = condition;
|
||||
const paramKey = `param${index}`;
|
||||
const param = {
|
||||
[paramKey]: operator === 'like' ? `%${value}%` : value,
|
||||
};
|
||||
const whereMethod = index === 0 ? 'where' : 'andWhere';
|
||||
|
||||
query = query[whereMethod](
|
||||
`mensaje.${attribute} ${operator} :${paramKey}`,
|
||||
param,
|
||||
);
|
||||
});
|
||||
|
||||
query = query.orderBy(`mensaje.${orderByAttribute}`, orderByDirection);
|
||||
|
||||
const totalRegistros = await query.getCount();
|
||||
const skip = (offset - 1) * limit;
|
||||
|
||||
query = query.skip(skip).take(limit);
|
||||
|
||||
const result = await query.getMany();
|
||||
const count = result.length;
|
||||
|
||||
serviceResult.boolean = count > 0;
|
||||
serviceResult.message = `${count} Mensaje(s) encontrado(s).`;
|
||||
serviceResult.number = totalRegistros;
|
||||
serviceResult.data = result;
|
||||
} catch (error: any) {
|
||||
serviceResult.boolean = false;
|
||||
serviceResult.message = `Error en el servicio: ${error.message}`;
|
||||
}
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async updateEstado(id: string, dto: UpdateEstadoDto): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
try {
|
||||
const mensaje = await this.repo.findOne({ where: { idMensajeWhatsApp: id } });
|
||||
|
||||
if (!mensaje) {
|
||||
serviceResult.message = `Mensaje ${id} no encontrado.`;
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
mensaje.estado = dto.estado;
|
||||
if (dto.error) {
|
||||
mensaje.error = dto.error;
|
||||
}
|
||||
if (dto.fechaEnvio) {
|
||||
mensaje.fechaEnvio = new Date(dto.fechaEnvio);
|
||||
}
|
||||
if (dto.estado === 'PENDIENTE') {
|
||||
mensaje.intentos += 1;
|
||||
}
|
||||
|
||||
const result = await this.repo.save(mensaje);
|
||||
|
||||
serviceResult.boolean = true;
|
||||
serviceResult.message = 'Estado actualizado correctamente.';
|
||||
serviceResult.number = 1;
|
||||
serviceResult.object = result;
|
||||
} catch (error: any) {
|
||||
serviceResult.boolean = false;
|
||||
serviceResult.message = `Error en el servicio: ${error.message}`;
|
||||
}
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
}
|
||||
170
src/modules/mensaje/mensaje.templates.ts
Normal file
170
src/modules/mensaje/mensaje.templates.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
// Arrays de emojis por categoría — uno se elige al azar por mensaje
|
||||
const EMOJI = {
|
||||
SALUDO: ['👋', '🙌', '✨', '😊', '🎯'],
|
||||
PAQUETE: ['📦', '📬', '📫', '🎁', '📩'],
|
||||
CALENDARIO: ['📅', '📆', '🗓️', '⏳', '⏰'],
|
||||
DINERO: ['💰', '💵', '💲', '🪙', '🤑'],
|
||||
UBICACION: ['📍', '📌', '🗺️', '🏠', '🏢'],
|
||||
HORARIO: ['🕐', '⏰', '🕰️', '⌛', '➿'],
|
||||
AVISO: ['⚠️', '🔔', '💡', '❗', '📢'],
|
||||
MAPS: ['🗺️', '📍', '🧭', '📌', '🌍'],
|
||||
};
|
||||
|
||||
function pick(arr: string[]): string {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
function formatFecha(date: Date): string {
|
||||
const d = date.getDate().toString().padStart(2, '0');
|
||||
const m = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const y = date.getFullYear();
|
||||
return `${d}/${m}/${y}`;
|
||||
}
|
||||
|
||||
// ─── RECEPCION_PAQUETE (3 mensajes) ──────────────────────────────────
|
||||
|
||||
export interface TemplateRecibidoData {
|
||||
nombreCliente: string;
|
||||
nombreSucursal: string;
|
||||
fechaLimite: Date;
|
||||
costoBase: number;
|
||||
diasIncluidos: number;
|
||||
costoDiaAdicional: number;
|
||||
}
|
||||
|
||||
// Mensaje 1: Principal (se envía como caption con imagen QR)
|
||||
export function templateRecibido1(data: TemplateRecibidoData): string {
|
||||
const e = {
|
||||
saludo: pick(EMOJI.SALUDO),
|
||||
paquete: pick(EMOJI.PAQUETE),
|
||||
calendario: pick(EMOJI.CALENDARIO),
|
||||
dinero: pick(EMOJI.DINERO),
|
||||
};
|
||||
|
||||
const fechaHoy = new Date();
|
||||
const fechaDesde = formatFecha(fechaHoy);
|
||||
const fechaHasta = formatFecha(data.fechaLimite);
|
||||
|
||||
return `${e.paquete} *PAQUETE RECIBIDO*
|
||||
|
||||
¡Hola ${e.saludo} *${data.nombreCliente}*!
|
||||
|
||||
Tu *paquete* ha sido recibido en nuestra sucursal *"${data.nombreSucursal}"*, puedes pasar a recogerlo desde el día *${fechaDesde}* hasta el *${fechaHasta}* sin costo adicional.
|
||||
|
||||
Recuerda que el costo para recoger tu paquete es de ${e.dinero} *${data.costoBase} Bs.* y tienes *${data.diasIncluidos} días* para hacerlo. Pasados los *${data.diasIncluidos} días* se aplicará un adicional de ${e.dinero} *${data.costoDiaAdicional}* Bs. por día y por paquete.`;
|
||||
}
|
||||
|
||||
// ======================================================================================================== //
|
||||
// Mensaje 2: Ubicación
|
||||
export interface TemplateRecibidoUbicacionData {
|
||||
nombreSucursal: string;
|
||||
descripcion: string;
|
||||
direccion: string;
|
||||
mapsUrl: string | null;
|
||||
}
|
||||
|
||||
export function templateRecibido2(data: TemplateRecibidoUbicacionData): string {
|
||||
const e = {
|
||||
ubicacion: pick(EMOJI.UBICACION),
|
||||
maps: pick(EMOJI.MAPS),
|
||||
};
|
||||
|
||||
let mensaje = `*UBICACIÓN SUCURSAL "${data.nombreSucursal}"*\n`;
|
||||
mensaje += `\n`;
|
||||
mensaje += `*Encuéntranos en:* ${e.ubicacion} ${data.direccion}\n`;
|
||||
|
||||
if (data.descripcion) {
|
||||
mensaje += `${data.descripcion}\n`;
|
||||
}
|
||||
|
||||
if (data.mapsUrl) {
|
||||
mensaje += `\n`;
|
||||
mensaje += `*Ubicación en Google Maps:* ${e.maps} ${data.mapsUrl}`;
|
||||
}
|
||||
|
||||
return mensaje;
|
||||
}
|
||||
|
||||
// ======================================================================================================== //
|
||||
// Mensaje 3: Horarios
|
||||
export interface TemplateRecibidoHorarioData {
|
||||
horariosAtencion: string;
|
||||
}
|
||||
|
||||
export function templateRecibido3(data: TemplateRecibidoHorarioData): string {
|
||||
const e = {
|
||||
horario: pick(EMOJI.HORARIO),
|
||||
};
|
||||
|
||||
return `${e.horario} *HORARIOS DE ATENCIÓN*
|
||||
|
||||
${data.horariosAtencion}
|
||||
|
||||
Excepto domingos y feriados.`;
|
||||
}
|
||||
|
||||
// ─── PAQUETE_ENTREGADO (1 mensaje) ──────────────────────────────────
|
||||
|
||||
export interface TemplateEntregadoData {
|
||||
nombreCliente: string;
|
||||
whatsappCliente: string;
|
||||
codigoCompleto: string;
|
||||
nombreSucursal: string;
|
||||
montoCobrar: number;
|
||||
}
|
||||
|
||||
export function templateEntregado(data: TemplateEntregadoData): string {
|
||||
const e = {
|
||||
paquete: pick(EMOJI.PAQUETE),
|
||||
aviso: pick(EMOJI.AVISO),
|
||||
dinero: pick(EMOJI.DINERO),
|
||||
saludo: pick(EMOJI.SALUDO),
|
||||
};
|
||||
|
||||
return `${e.paquete} PAQUETE ENTREGADO
|
||||
|
||||
${e.saludo} Tu paquete *${data.codigoCompleto}* ha sido recogido por ${data.whatsappCliente} en "${data.nombreSucursal}".
|
||||
|
||||
${e.aviso} !Gracias por confiar en nosotros!`;
|
||||
}
|
||||
|
||||
// ─── NOTIFICACION_EMPRENDEDOR (lote de paquetes recibidos) ──────────────────────────────────
|
||||
|
||||
export interface PaqueteLoteInfo {
|
||||
codigoCompleto: string;
|
||||
nombreCliente: string;
|
||||
whatsappCliente: string;
|
||||
}
|
||||
|
||||
export interface TemplateRecibidoEmprendedorData {
|
||||
nombreEmprendimiento: string;
|
||||
nombreSucursal: string;
|
||||
paquetes: PaqueteLoteInfo[];
|
||||
}
|
||||
|
||||
export function templateRecibidoEmprendedor(data: TemplateRecibidoEmprendedorData): string {
|
||||
const e = {
|
||||
paquete: pick(EMOJI.PAQUETE),
|
||||
ubicacion: pick(EMOJI.UBICACION),
|
||||
calendario: pick(EMOJI.CALENDARIO),
|
||||
saludo: pick(EMOJI.SALUDO),
|
||||
};
|
||||
|
||||
const fecha = formatFecha(new Date());
|
||||
const hora = new Date().toLocaleTimeString('es-BO', { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
|
||||
let lista = data.paquetes.map((p, i) =>
|
||||
`${i + 1}. *${p.codigoCompleto}* → ${p.nombreCliente} (${p.whatsappCliente})`
|
||||
).join('\n');
|
||||
|
||||
return `${e.paquete} *PAQUETES RECIBIDOS*
|
||||
|
||||
${e.saludo} *${data.nombreEmprendimiento}*,
|
||||
|
||||
Se han registrado *${data.paquetes.length} paquete(s)* a tu nombre:
|
||||
|
||||
${lista}
|
||||
|
||||
${e.ubicacion} Sucursal: *${data.nombreSucursal}*
|
||||
${e.calendario} Fecha: *${fecha} ${hora}*`;
|
||||
}
|
||||
9
src/modules/paquete/dto/cambiar-ubicacion.dto.ts
Normal file
9
src/modules/paquete/dto/cambiar-ubicacion.dto.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { IsString, IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CambiarUbicacionDto {
|
||||
@ApiProperty({ example: 'uuid-zona' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
idZonaAlmacenamiento: string;
|
||||
}
|
||||
75
src/modules/paquete/dto/crear-lote.dto.ts
Normal file
75
src/modules/paquete/dto/crear-lote.dto.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { IsString, IsOptional, IsBoolean, IsDecimal, IsArray, ValidateNested, IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class PaqueteLoteItemDto {
|
||||
@ApiProperty({ example: 'Juan Pérez' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
nombreCliente: string;
|
||||
|
||||
@ApiProperty({ example: '591' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
codigoPaisCliente: string;
|
||||
|
||||
@ApiProperty({ example: '70123456' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
whatsappCliente: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'uuid-categoria' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
idCategoria?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'uuid-zona' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
idZonaAlmacenamiento?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '/uploads/paquetes/abc.jpg' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
fotoUrl?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Type(() => Boolean)
|
||||
paquetePagado?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Type(() => Boolean)
|
||||
saldo?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ example: 0 })
|
||||
@IsDecimal()
|
||||
@IsOptional()
|
||||
saldoPendiente?: number;
|
||||
}
|
||||
|
||||
export class CrearLoteDto {
|
||||
@ApiProperty({ example: 'uuid-sucursal' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
idSucursal: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'uuid-emprendimiento' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
idEmprendimiento?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'uuid-usuario' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
idUsuarioRecepcion?: string;
|
||||
|
||||
@ApiProperty({ type: [PaqueteLoteItemDto] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PaqueteLoteItemDto)
|
||||
paquetes: PaqueteLoteItemDto[];
|
||||
}
|
||||
19
src/modules/paquete/dto/entregar-paquete.dto.ts
Normal file
19
src/modules/paquete/dto/entregar-paquete.dto.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { IsOptional, IsDecimal, IsString } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class EntregarPaqueteDto {
|
||||
@ApiPropertyOptional({ example: 11.00 })
|
||||
@IsDecimal()
|
||||
@IsOptional()
|
||||
montoCobrarPaquete?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 11.00 })
|
||||
@IsDecimal()
|
||||
@IsOptional()
|
||||
montoFinal?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 'uuid-usuario' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
idUsuarioEntrega?: string;
|
||||
}
|
||||
72
src/modules/paquete/dto/paquete.dto.ts
Normal file
72
src/modules/paquete/dto/paquete.dto.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { IsString, IsOptional, IsBoolean, IsDecimal, IsUUID, IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class PaqueteDto {
|
||||
@ApiProperty({ example: 'uuid-id' })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
idPaquete: string;
|
||||
|
||||
@ApiProperty({ example: 'uuid-sucursal' })
|
||||
@IsString()
|
||||
idSucursal: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'uuid-categoria' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
idCategoria?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'uuid-zona' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
idZonaAlmacenamiento?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'uuid-emprendimiento' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
idEmprendimiento?: string;
|
||||
|
||||
// Datos del cliente (receptor)
|
||||
@ApiProperty({ example: 'Juan Pérez' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
nombreCliente: string;
|
||||
|
||||
@ApiProperty({ example: '51' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
codigoPaisCliente: string;
|
||||
|
||||
@ApiProperty({ example: '70123456' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
whatsappCliente: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '/uploads/paquetes/abc.jpg' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
fotoUrl?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Type(() => Boolean)
|
||||
paquetePagado?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Type(() => Boolean)
|
||||
saldo?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ example: 0 })
|
||||
@IsDecimal()
|
||||
@IsOptional()
|
||||
saldoPendiente?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 'uuid-usuario' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
idUsuarioRecepcion?: string;
|
||||
}
|
||||
4
src/modules/paquete/dto/update-paquete.dto.ts
Normal file
4
src/modules/paquete/dto/update-paquete.dto.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { PartialType } from '@nestjs/swagger';
|
||||
import { PaqueteDto } from './paquete.dto.js';
|
||||
|
||||
export class UpdatePaqueteDto extends PartialType(PaqueteDto) { }
|
||||
108
src/modules/paquete/entities/paquete.entity.ts
Normal file
108
src/modules/paquete/entities/paquete.entity.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Paquete {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
idPaquete: string;
|
||||
|
||||
@Column()
|
||||
idSucursal: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
idLote: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
idCategoria: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
idZonaAlmacenamiento: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
idEmprendimiento: string;
|
||||
|
||||
// Datos del cliente (receptor)
|
||||
@Column()
|
||||
nombreCliente: string;
|
||||
|
||||
@Column()
|
||||
codigoPaisCliente: string;
|
||||
|
||||
@Column()
|
||||
whatsappCliente: string;
|
||||
|
||||
// Campos de identificación
|
||||
@Column()
|
||||
prefijoSucursal: string;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
numeroCorrelativo: number;
|
||||
|
||||
@Column()
|
||||
letraCorrelativo: string;
|
||||
|
||||
@Column()
|
||||
color: string;
|
||||
|
||||
@Column()
|
||||
codigoCompleto: string;
|
||||
|
||||
// Imágenes
|
||||
@Column()
|
||||
fotoUrl: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
tokenRetiro: string;
|
||||
|
||||
@Column()
|
||||
urlQR: string;
|
||||
|
||||
// Estado
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['RECIBIDO', 'ALMACENADO', 'ENTREGADO', 'DEVUELTO'],
|
||||
default: 'RECIBIDO',
|
||||
})
|
||||
estado: string;
|
||||
|
||||
// Recepción
|
||||
@CreateDateColumn()
|
||||
fechaRecepcion: Date;
|
||||
|
||||
@Column({ type: 'timestamp' })
|
||||
fechaLimite: Date;
|
||||
|
||||
@Column()
|
||||
idUsuarioRecepcion: string;
|
||||
|
||||
// Pago
|
||||
@Column({ default: false })
|
||||
paquetePagado: boolean;
|
||||
|
||||
// Cobro Paquetería
|
||||
@Column({ type: 'int', default: 0 })
|
||||
diasAdicionales: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
montoCalculadoPaquete: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
montoCobrarPaquete: number;
|
||||
|
||||
// Saldo del Paquete
|
||||
@Column({ default: false })
|
||||
saldo: boolean;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
saldoPendiente: number;
|
||||
|
||||
// Total
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
montoFinal: number;
|
||||
|
||||
// Entrega
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
fechaEntrega: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
idUsuarioEntrega: string;
|
||||
}
|
||||
376
src/modules/paquete/paquete.controller.ts
Normal file
376
src/modules/paquete/paquete.controller.ts
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Patch,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseInterceptors,
|
||||
UploadedFile,
|
||||
Req,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { PaqueteService } from './paquete.service.js';
|
||||
import { PaqueteDto } from './dto/paquete.dto.js';
|
||||
import { UpdatePaqueteDto } from './dto/update-paquete.dto.js';
|
||||
import { EntregarPaqueteDto } from './dto/entregar-paquete.dto.js';
|
||||
import { CambiarUbicacionDto } from './dto/cambiar-ubicacion.dto.js';
|
||||
import { CrearLoteDto } from './dto/crear-lote.dto.js';
|
||||
import { ApiResult, defaultApiResult } from '../../shared/interfaces/api.result.js';
|
||||
import { routePaquete } from './paquete.router.js';
|
||||
|
||||
@Controller('paquete')
|
||||
export class PaqueteController {
|
||||
constructor(private readonly service: PaqueteService) {}
|
||||
|
||||
@Get()
|
||||
async findAll(
|
||||
@Query('page') page?: number,
|
||||
@Query('limit') limit?: number,
|
||||
@Query('estado') estado?: string,
|
||||
@Query('idCategoria') idCategoria?: string,
|
||||
@Query('buscar') buscar?: string,
|
||||
@Query('fechaDesde') fechaDesde?: string,
|
||||
@Query('fechaHasta') fechaHasta?: string,
|
||||
@Query('porVencer') porVencer?: boolean,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routePaquete.find.title,
|
||||
routePaquete.find.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.service.findAll({
|
||||
page,
|
||||
limit,
|
||||
estado,
|
||||
idCategoria,
|
||||
buscar,
|
||||
fechaDesde,
|
||||
fechaHasta,
|
||||
porVencer,
|
||||
});
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'OK';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.total;
|
||||
apiResult.data = result.data;
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || 500;
|
||||
apiResult.message = error.message || 'Error del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Get('estadisticas/hoy')
|
||||
async estadisticasHoy(): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routePaquete.find.title,
|
||||
routePaquete.find.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await this.service.estadisticasHoy();
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'OK';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [data];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || 500;
|
||||
apiResult.message = error.message || 'Error del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Get('estadisticas/periodo')
|
||||
async estadisticasPeriodo(
|
||||
@Query('fechaDesde') fechaDesde: string,
|
||||
@Query('fechaHasta') fechaHasta: string,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routePaquete.find.title,
|
||||
routePaquete.find.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await this.service.estadisticasPeriodo(fechaDesde, fechaHasta);
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'OK';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [data];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || 500;
|
||||
apiResult.message = error.message || 'Error del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Get('limpiar/preview')
|
||||
async previewLimpiar(
|
||||
@Query('fechaDesde') fechaDesde: string,
|
||||
@Query('fechaHasta') fechaHasta: string,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routePaquete.find.title,
|
||||
routePaquete.find.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await this.service.previewLimpiar(fechaDesde, fechaHasta);
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'OK';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [data];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || 500;
|
||||
apiResult.message = error.message || 'Error del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Delete('limpiar')
|
||||
async limpiar(
|
||||
@Query('fechaDesde') fechaDesde: string,
|
||||
@Query('fechaHasta') fechaHasta: string,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routePaquete.remove.title,
|
||||
routePaquete.remove.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await this.service.limpiar(fechaDesde, fechaHasta);
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Limpieza completada';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [data];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || 500;
|
||||
apiResult.message = error.message || 'Error del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Get('token/:tokenRetiro')
|
||||
async findByToken(@Param('tokenRetiro') tokenRetiro: string): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routePaquete.one.title,
|
||||
routePaquete.one.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await this.service.findByToken(tokenRetiro);
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'OK';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [data];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || 500;
|
||||
apiResult.message = error.message || 'Error del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Get(':idPaquete')
|
||||
async findOne(@Param('idPaquete') idPaquete: string): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routePaquete.one.title,
|
||||
routePaquete.one.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.service.findOne(idPaquete);
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'OK';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [result];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || 500;
|
||||
apiResult.message = error.message || 'Error del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async create(
|
||||
@UploadedFile() file: Express.Multer.File | undefined,
|
||||
@Body() dto: PaqueteDto,
|
||||
@Req() req: any,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routePaquete.create.title,
|
||||
routePaquete.create.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await this.service.create(dto, file, dto.idUsuarioRecepcion || req?.user?.idUsuario || '');
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.CREATED;
|
||||
apiResult.message = 'Paquete registrado correctamente';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [data];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || 500;
|
||||
apiResult.message = error.message || 'Error del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Post('lote')
|
||||
async createLote(
|
||||
@Body() dto: CrearLoteDto,
|
||||
@Req() req: any,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routePaquete.createLote.title,
|
||||
routePaquete.createLote.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const idUsuario = dto.idUsuarioRecepcion || req?.user?.idUsuario || '';
|
||||
const data = await this.service.createLote(dto, idUsuario);
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.CREATED;
|
||||
apiResult.message = `${dto.paquetes.length} paquete(s) registrado(s) correctamente`;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = data.paquetes.length;
|
||||
apiResult.data = [data];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || 500;
|
||||
apiResult.message = error.message || 'Error del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Patch(':idPaquete')
|
||||
async update(
|
||||
@Param('idPaquete') idPaquete: string,
|
||||
@Body() dto: UpdatePaqueteDto,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routePaquete.update.title,
|
||||
routePaquete.update.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await this.service.update(idPaquete, dto);
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Paquete actualizado correctamente';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [data];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || 500;
|
||||
apiResult.message = error.message || 'Error del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Put(':idPaquete/entregar')
|
||||
async entregar(
|
||||
@Param('idPaquete') idPaquete: string,
|
||||
@Body() dto: EntregarPaqueteDto,
|
||||
@Req() req: any,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routePaquete.update.title,
|
||||
routePaquete.update.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const idUsuario = req?.user?.idUsuario || dto.idUsuarioEntrega || '';
|
||||
const data = await this.service.entregar(idPaquete, dto, idUsuario);
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Paquete entregado correctamente';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [data];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || 500;
|
||||
apiResult.message = error.message || 'Error del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Put(':idPaquete/devolver')
|
||||
async devolver(@Param('idPaquete') idPaquete: string): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routePaquete.update.title,
|
||||
routePaquete.update.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await this.service.devolver(idPaquete);
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Paquete devuelto correctamente';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [data];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || 500;
|
||||
apiResult.message = error.message || 'Error del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Put(':idPaquete/ubicacion')
|
||||
async cambiarUbicacion(
|
||||
@Param('idPaquete') idPaquete: string,
|
||||
@Body() dto: CambiarUbicacionDto,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routePaquete.update.title,
|
||||
routePaquete.update.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await this.service.cambiarUbicacion(idPaquete, dto);
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Ubicación actualizada correctamente';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [data];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || 500;
|
||||
apiResult.message = error.message || 'Error del servidor';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
}
|
||||
23
src/modules/paquete/paquete.module.ts
Normal file
23
src/modules/paquete/paquete.module.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PaqueteController } from './paquete.controller.js';
|
||||
import { PaqueteService } from './paquete.service.js';
|
||||
import { Paquete } from './entities/paquete.entity.js';
|
||||
import { Sucursal } from '../sucursal/entities/sucursal.entity.js';
|
||||
import { Feriado } from '../feriado/entities/feriado.entity.js';
|
||||
import { Categoria } from '../categoria/entities/categoria.entity.js';
|
||||
import { Emprendimiento } from '../emprendimiento/entities/emprendimiento.entity.js';
|
||||
import { QrModule } from '../../shared/services/qr/qr.module.js';
|
||||
import { MensajeModule } from '../mensaje/mensaje.module.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Paquete, Sucursal, Feriado, Categoria, Emprendimiento]),
|
||||
QrModule,
|
||||
MensajeModule,
|
||||
],
|
||||
controllers: [PaqueteController],
|
||||
providers: [PaqueteService],
|
||||
exports: [PaqueteService],
|
||||
})
|
||||
export class PaqueteModule {}
|
||||
30
src/modules/paquete/paquete.router.ts
Normal file
30
src/modules/paquete/paquete.router.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
export const routePaquete = {
|
||||
multiple: {
|
||||
route: '[POST].../v1/paquete/multiple',
|
||||
title: 'Create Multiple Paquete',
|
||||
},
|
||||
create: {
|
||||
route: '[POST].../v1/paquete',
|
||||
title: 'Create Paquete',
|
||||
},
|
||||
createLote: {
|
||||
route: '[POST].../v1/paquete/lote',
|
||||
title: 'Create Lote Paquete',
|
||||
},
|
||||
one: {
|
||||
route: '[GET].../v1/paquete/:idPaquete',
|
||||
title: 'Find One Paquete',
|
||||
},
|
||||
find: {
|
||||
route: '[POST].../v1/paquete/find',
|
||||
title: 'Find Paquete WhereCondition[], Attribute & Direccion',
|
||||
},
|
||||
update: {
|
||||
route: '[PATCH].../v1/paquete/:idPaquete',
|
||||
title: 'Update Paquete',
|
||||
},
|
||||
remove: {
|
||||
route: '[DELETE].../v1/paquete/:idPaquete',
|
||||
title: 'Remove Paquete',
|
||||
},
|
||||
};
|
||||
739
src/modules/paquete/paquete.service.ts
Normal file
739
src/modules/paquete/paquete.service.ts
Normal file
|
|
@ -0,0 +1,739 @@
|
|||
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import sharp from 'sharp';
|
||||
import { Paquete } from './entities/paquete.entity.js';
|
||||
import { Sucursal } from '../sucursal/entities/sucursal.entity.js';
|
||||
import { Feriado } from '../feriado/entities/feriado.entity.js';
|
||||
import { Categoria } from '../categoria/entities/categoria.entity.js';
|
||||
import { Emprendimiento } from '../emprendimiento/entities/emprendimiento.entity.js';
|
||||
import { PaqueteDto } from './dto/paquete.dto.js';
|
||||
import { UpdatePaqueteDto } from './dto/update-paquete.dto.js';
|
||||
import { EntregarPaqueteDto } from './dto/entregar-paquete.dto.js';
|
||||
import { CambiarUbicacionDto } from './dto/cambiar-ubicacion.dto.js';
|
||||
import { CrearLoteDto } from './dto/crear-lote.dto.js';
|
||||
import { QrRenderer } from '../../shared/services/qr/qr.renderer.js';
|
||||
import { MensajeService } from '../mensaje/mensaje.service.js';
|
||||
import { templateRecibido1, templateRecibido2, templateRecibido3, templateEntregado, templateRecibidoEmprendedor } from '../mensaje/mensaje.templates.js';
|
||||
import {
|
||||
calcularDiasAdicionales,
|
||||
calcularFechaLimite,
|
||||
obtenerFeriadosDelRango,
|
||||
} from '../../shared/utils/fecha.util.js';
|
||||
|
||||
@Injectable()
|
||||
export class PaqueteService {
|
||||
private uploadsPath = path.join(process.cwd(), 'uploads');
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Paquete)
|
||||
private paqueteRepo: Repository<Paquete>,
|
||||
@InjectRepository(Sucursal)
|
||||
private sucursalRepo: Repository<Sucursal>,
|
||||
@InjectRepository(Feriado)
|
||||
private feriadoRepo: Repository<Feriado>,
|
||||
@InjectRepository(Categoria)
|
||||
private categoriaRepo: Repository<Categoria>,
|
||||
@InjectRepository(Emprendimiento)
|
||||
private emprendimientoRepo: Repository<Emprendimiento>,
|
||||
private readonly qrRenderer: QrRenderer,
|
||||
private readonly mensajeService: MensajeService,
|
||||
) {}
|
||||
|
||||
async findAll(filters: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
estado?: string;
|
||||
idCategoria?: string;
|
||||
buscar?: string;
|
||||
fechaDesde?: string;
|
||||
fechaHasta?: string;
|
||||
porVencer?: boolean;
|
||||
}) {
|
||||
const { page = 1, limit = 20, ...rest } = filters;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const qb = this.paqueteRepo.createQueryBuilder('p');
|
||||
|
||||
if (rest.estado) {
|
||||
qb.andWhere('p.estado = :estado', { estado: rest.estado });
|
||||
}
|
||||
if (rest.idCategoria) {
|
||||
qb.andWhere('p.idCategoria = :idCategoria', { idCategoria: rest.idCategoria });
|
||||
}
|
||||
if (rest.buscar) {
|
||||
qb.andWhere('p.codigoCompleto ILIKE :buscar', { buscar: `%${rest.buscar}%` });
|
||||
}
|
||||
if (rest.fechaDesde) {
|
||||
qb.andWhere('p.fechaRecepcion >= :fechaDesde', { fechaDesde: rest.fechaDesde });
|
||||
}
|
||||
if (rest.fechaHasta) {
|
||||
qb.andWhere('p.fechaRecepcion <= :fechaHasta', { fechaHasta: rest.fechaHasta });
|
||||
}
|
||||
if (rest.porVencer) {
|
||||
const manana = new Date();
|
||||
manana.setDate(manana.getDate() + 2);
|
||||
qb.andWhere('p.fechaLimite <= :manana AND p.estado != :entregado', {
|
||||
manana: manana.toISOString(),
|
||||
entregado: 'ENTREGADO',
|
||||
});
|
||||
}
|
||||
|
||||
const [data, total] = await qb
|
||||
.skip(skip)
|
||||
.take(limit)
|
||||
.orderBy('p.fechaRecepcion', 'DESC')
|
||||
.getManyAndCount();
|
||||
|
||||
return { data, total, page, limit };
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const paquete = await this.paqueteRepo.findOne({ where: { idPaquete: id } });
|
||||
if (!paquete) throw new NotFoundException('Paquete no encontrado');
|
||||
|
||||
// Asegurar que las fechas se retornen en formato ISO con timezone
|
||||
paquete.fechaRecepcion = new Date(paquete.fechaRecepcion);
|
||||
paquete.fechaLimite = new Date(paquete.fechaLimite);
|
||||
if (paquete.fechaEntrega) {
|
||||
paquete.fechaEntrega = new Date(paquete.fechaEntrega);
|
||||
}
|
||||
|
||||
return paquete;
|
||||
}
|
||||
|
||||
async findByToken(tokenRetiro: string) {
|
||||
const paquete = await this.paqueteRepo.findOne({ where: { tokenRetiro } });
|
||||
if (!paquete) throw new NotFoundException('No se encontró un paquete con ese token');
|
||||
|
||||
if (paquete.estado === 'RECIBIDO' || paquete.estado === 'ALMACENADO') {
|
||||
const sucursal = await this.sucursalRepo.findOne({ where: { idSucursal: paquete.idSucursal } });
|
||||
if (!sucursal) throw new NotFoundException('Sucursal no encontrada');
|
||||
|
||||
const hoy = new Date();
|
||||
const feriados = await obtenerFeriadosDelRango(this.feriadoRepo, paquete.fechaRecepcion, hoy);
|
||||
|
||||
const diasAdicionales = calcularDiasAdicionales(
|
||||
paquete.fechaRecepcion,
|
||||
hoy,
|
||||
sucursal.diasIncluidos,
|
||||
feriados,
|
||||
);
|
||||
|
||||
let montoCalculado: number;
|
||||
if (paquete.paquetePagado) {
|
||||
montoCalculado = Number(sucursal.costoDiaAdicional) * diasAdicionales;
|
||||
} else {
|
||||
montoCalculado = Number(sucursal.costoBase) + Number(sucursal.costoDiaAdicional) * diasAdicionales;
|
||||
}
|
||||
|
||||
paquete.diasAdicionales = diasAdicionales;
|
||||
paquete.montoCalculadoPaquete = montoCalculado;
|
||||
paquete.montoCobrarPaquete = montoCalculado;
|
||||
paquete.montoFinal = montoCalculado + Number(paquete.saldoPendiente);
|
||||
}
|
||||
|
||||
// Asegurar que las fechas se retornen en formato ISO con timezone
|
||||
paquete.fechaRecepcion = new Date(paquete.fechaRecepcion);
|
||||
paquete.fechaLimite = new Date(paquete.fechaLimite);
|
||||
|
||||
return paquete;
|
||||
}
|
||||
|
||||
async create(
|
||||
dto: PaqueteDto,
|
||||
file: Express.Multer.File | undefined,
|
||||
idUsuarioRecepcion: string,
|
||||
) {
|
||||
// 1. Validar sucursal
|
||||
const sucursal = await this.sucursalRepo.findOne({ where: { idSucursal: dto.idSucursal } });
|
||||
if (!sucursal) throw new NotFoundException('Sucursal no encontrada');
|
||||
|
||||
// 2. Validar categoría
|
||||
if (dto.idCategoria) {
|
||||
const categoria = await this.categoriaRepo.findOne({ where: { idCategoria: dto.idCategoria } });
|
||||
if (!categoria) throw new NotFoundException('Categoría no encontrada');
|
||||
}
|
||||
|
||||
// 3. Validar emprendimiento
|
||||
if (dto.idEmprendimiento) {
|
||||
const emprendimiento = await this.emprendimientoRepo.findOne({ where: { idEmprendimiento: dto.idEmprendimiento } });
|
||||
if (!emprendimiento) throw new NotFoundException('Emprendimiento no encontrado');
|
||||
}
|
||||
|
||||
// 4. Generar token y QR
|
||||
const tokenRetiro = uuidv4();
|
||||
const urlQR = await this.qrRenderer.generar(tokenRetiro, sucursal.nombre);
|
||||
|
||||
// 6. Manejar imagen
|
||||
let fotoUrl: string;
|
||||
|
||||
if (file) {
|
||||
// Caso A: Imagen enviada en la petición
|
||||
const compressBuffer = await sharp(file.buffer)
|
||||
.resize({ width: 1920, height: 1920, fit: 'inside', withoutEnlargement: true })
|
||||
.jpeg({ quality: 80 })
|
||||
.toBuffer();
|
||||
|
||||
const fotoFilename = `${uuidv4()}.jpg`;
|
||||
const fotoPath = path.join(this.uploadsPath, 'paquetes', fotoFilename);
|
||||
fs.mkdirSync(path.dirname(fotoPath), { recursive: true });
|
||||
fs.writeFileSync(fotoPath, compressBuffer);
|
||||
fotoUrl = `/uploads/paquetes/${fotoFilename}`;
|
||||
} else {
|
||||
// Caso B: Imagen de flujo previo (OCR/upload)
|
||||
if (!dto.fotoUrl) {
|
||||
throw new NotFoundException('No se proporcionó imagen (file o fotoUrl)');
|
||||
}
|
||||
fotoUrl = dto.fotoUrl;
|
||||
}
|
||||
|
||||
// 7. Calcular código correlativo
|
||||
const ultimoPaquete = await this.paqueteRepo.findOne({
|
||||
where: { idSucursal: sucursal.idSucursal },
|
||||
order: { numeroCorrelativo: 'DESC', letraCorrelativo: 'DESC' },
|
||||
});
|
||||
|
||||
let numeroCorrelativo = 1;
|
||||
let letraCorrelativo = sucursal.letraActual;
|
||||
|
||||
if (ultimoPaquete) {
|
||||
if (ultimoPaquete.numeroCorrelativo >= sucursal.cantidadCodigos) {
|
||||
letraCorrelativo = String.fromCharCode(ultimoPaquete.letraCorrelativo.charCodeAt(0) + 1);
|
||||
numeroCorrelativo = 1;
|
||||
} else {
|
||||
numeroCorrelativo = ultimoPaquete.numeroCorrelativo + 1;
|
||||
letraCorrelativo = ultimoPaquete.letraCorrelativo;
|
||||
}
|
||||
}
|
||||
|
||||
const prefijo = sucursal.usarPrefijoSucursal ? sucursal.prefijo : '';
|
||||
const codigoCompleto = sucursal.formatoCodigo
|
||||
.replace('{prefijo}', prefijo)
|
||||
.replace('{numero}', String(numeroCorrelativo).padStart(4, '0'))
|
||||
.replace('{letra}', letraCorrelativo);
|
||||
|
||||
// 8. Calcular fecha límite
|
||||
const hoy = new Date();
|
||||
const feriados = await obtenerFeriadosDelRango(this.feriadoRepo, hoy, new Date(hoy.getFullYear(), 11, 31));
|
||||
const fechaLimite = calcularFechaLimite(hoy, sucursal.diasIncluidos, feriados);
|
||||
|
||||
// 8. Crear paquete
|
||||
const paquete = this.paqueteRepo.create({
|
||||
idSucursal: sucursal.idSucursal,
|
||||
idCategoria: dto.idCategoria,
|
||||
idZonaAlmacenamiento: dto.idZonaAlmacenamiento,
|
||||
idEmprendimiento: dto.idEmprendimiento,
|
||||
nombreCliente: dto.nombreCliente,
|
||||
codigoPaisCliente: dto.codigoPaisCliente,
|
||||
whatsappCliente: dto.whatsappCliente,
|
||||
prefijoSucursal: sucursal.prefijo,
|
||||
numeroCorrelativo,
|
||||
letraCorrelativo,
|
||||
color: sucursal.colorActual,
|
||||
codigoCompleto,
|
||||
fotoUrl,
|
||||
tokenRetiro,
|
||||
urlQR,
|
||||
fechaLimite,
|
||||
idUsuarioRecepcion,
|
||||
paquetePagado: dto.paquetePagado || false,
|
||||
saldo: dto.saldo || false,
|
||||
saldoPendiente: dto.saldoPendiente || 0,
|
||||
montoCalculadoPaquete: 0,
|
||||
montoCobrarPaquete: 0,
|
||||
montoFinal: 0,
|
||||
});
|
||||
|
||||
// 10. Actualizar sucursal
|
||||
if (numeroCorrelativo === 1 && letraCorrelativo !== sucursal.letraActual) {
|
||||
sucursal.letraActual = letraCorrelativo;
|
||||
}
|
||||
sucursal.numeroActual = numeroCorrelativo;
|
||||
await this.sucursalRepo.save(sucursal);
|
||||
|
||||
const paqueteGuardado = await this.paqueteRepo.save(paquete);
|
||||
|
||||
// 10. Crear mensaje WhatsApp (3 mensajes)
|
||||
const textoMensaje1 = templateRecibido1({
|
||||
nombreCliente: dto.nombreCliente,
|
||||
nombreSucursal: sucursal.nombre,
|
||||
fechaLimite: fechaLimite,
|
||||
costoBase: Number(sucursal.costoBase),
|
||||
diasIncluidos: sucursal.diasIncluidos,
|
||||
costoDiaAdicional: Number(sucursal.costoDiaAdicional),
|
||||
});
|
||||
|
||||
const textoMensaje2 = templateRecibido2({
|
||||
nombreSucursal: sucursal.nombre,
|
||||
descripcion: sucursal.descripcion || '',
|
||||
direccion: sucursal.direccion,
|
||||
mapsUrl: sucursal.mapsUrl,
|
||||
});
|
||||
|
||||
const textoMensaje3 = templateRecibido3({
|
||||
horariosAtencion: sucursal.horariosAtencion,
|
||||
});
|
||||
|
||||
const mensaje = await this.mensajeService.create({
|
||||
idPaquete: paqueteGuardado.idPaquete,
|
||||
telefonoDestino: `${dto.codigoPaisCliente}${dto.whatsappCliente}`,
|
||||
tipo: 'RECEPCION_PAQUETE',
|
||||
mensaje1: textoMensaje1,
|
||||
mensaje2: textoMensaje2,
|
||||
mensaje3: textoMensaje3,
|
||||
urlQR: paqueteGuardado.urlQR,
|
||||
sessionId: sucursal.sessionId,
|
||||
});
|
||||
|
||||
return { paquete: paqueteGuardado, mensaje };
|
||||
}
|
||||
|
||||
async createLote(dto: CrearLoteDto, idUsuarioRecepcion: string) {
|
||||
// 1. Validar sucursal
|
||||
const sucursal = await this.sucursalRepo.findOne({ where: { idSucursal: dto.idSucursal } });
|
||||
if (!sucursal) throw new NotFoundException('Sucursal no encontrada');
|
||||
|
||||
// 2. Validar emprendimiento (opcional)
|
||||
let emprendimiento = null;
|
||||
if (dto.idEmprendimiento) {
|
||||
emprendimiento = await this.emprendimientoRepo.findOne({ where: { idEmprendimiento: dto.idEmprendimiento } });
|
||||
if (!emprendimiento) throw new NotFoundException('Emprendimiento no encontrado');
|
||||
}
|
||||
|
||||
// 3. Obtener feriados del año
|
||||
const hoy = new Date();
|
||||
const feriados = await obtenerFeriadosDelRango(this.feriadoRepo, hoy, new Date(hoy.getFullYear(), 11, 31));
|
||||
|
||||
// 4. Generar idLote único para este batch
|
||||
const idLote = uuidv4();
|
||||
|
||||
const paquetesCreados: any[] = [];
|
||||
const infoPaquetesEmprendedor: { codigoCompleto: string; nombreCliente: string; whatsappCliente: string }[] = [];
|
||||
|
||||
// 4. Procesar cada paquete del lote
|
||||
for (const item of dto.paquetes) {
|
||||
// Validar categoría si se proporciona
|
||||
if (item.idCategoria) {
|
||||
const categoria = await this.categoriaRepo.findOne({ where: { idCategoria: item.idCategoria } });
|
||||
if (!categoria) throw new NotFoundException(`Categoría ${item.idCategoria} no encontrada`);
|
||||
}
|
||||
|
||||
// Generar token y QR
|
||||
const tokenRetiro = uuidv4();
|
||||
const urlQR = await this.qrRenderer.generar(tokenRetiro, sucursal.nombre);
|
||||
|
||||
// Calcular código correlativo
|
||||
const ultimoPaquete = await this.paqueteRepo.findOne({
|
||||
where: { idSucursal: sucursal.idSucursal },
|
||||
order: { numeroCorrelativo: 'DESC', letraCorrelativo: 'DESC' },
|
||||
});
|
||||
|
||||
let numeroCorrelativo = 1;
|
||||
let letraCorrelativo = sucursal.letraActual;
|
||||
|
||||
if (ultimoPaquete) {
|
||||
if (ultimoPaquete.numeroCorrelativo >= sucursal.cantidadCodigos) {
|
||||
letraCorrelativo = String.fromCharCode(ultimoPaquete.letraCorrelativo.charCodeAt(0) + 1);
|
||||
numeroCorrelativo = 1;
|
||||
} else {
|
||||
numeroCorrelativo = ultimoPaquete.numeroCorrelativo + 1;
|
||||
letraCorrelativo = ultimoPaquete.letraCorrelativo;
|
||||
}
|
||||
}
|
||||
|
||||
const prefijo = sucursal.usarPrefijoSucursal ? sucursal.prefijo : '';
|
||||
const codigoCompleto = sucursal.formatoCodigo
|
||||
.replace('{prefijo}', prefijo)
|
||||
.replace('{numero}', String(numeroCorrelativo).padStart(4, '0'))
|
||||
.replace('{letra}', letraCorrelativo);
|
||||
|
||||
// Calcular fecha límite
|
||||
const fechaLimite = calcularFechaLimite(hoy, sucursal.diasIncluidos, feriados);
|
||||
|
||||
// Crear paquete
|
||||
const paquete = this.paqueteRepo.create({
|
||||
idSucursal: sucursal.idSucursal,
|
||||
idLote,
|
||||
idCategoria: item.idCategoria,
|
||||
idZonaAlmacenamiento: item.idZonaAlmacenamiento,
|
||||
idEmprendimiento: dto.idEmprendimiento,
|
||||
nombreCliente: item.nombreCliente,
|
||||
codigoPaisCliente: item.codigoPaisCliente,
|
||||
whatsappCliente: item.whatsappCliente,
|
||||
prefijoSucursal: sucursal.prefijo,
|
||||
numeroCorrelativo,
|
||||
letraCorrelativo,
|
||||
color: sucursal.colorActual,
|
||||
codigoCompleto,
|
||||
fotoUrl: item.fotoUrl || '',
|
||||
tokenRetiro,
|
||||
urlQR,
|
||||
fechaLimite,
|
||||
idUsuarioRecepcion,
|
||||
paquetePagado: item.paquetePagado || false,
|
||||
saldo: item.saldo || false,
|
||||
saldoPendiente: item.saldoPendiente || 0,
|
||||
montoCalculadoPaquete: 0,
|
||||
montoCobrarPaquete: 0,
|
||||
montoFinal: 0,
|
||||
});
|
||||
|
||||
// Actualizar sucursal
|
||||
if (numeroCorrelativo === 1 && letraCorrelativo !== sucursal.letraActual) {
|
||||
sucursal.letraActual = letraCorrelativo;
|
||||
}
|
||||
sucursal.numeroActual = numeroCorrelativo;
|
||||
await this.sucursalRepo.save(sucursal);
|
||||
|
||||
const paqueteGuardado = await this.paqueteRepo.save(paquete);
|
||||
|
||||
// Crear mensaje WhatsApp al cliente (3 mensajes)
|
||||
const textoMensaje1 = templateRecibido1({
|
||||
nombreCliente: item.nombreCliente,
|
||||
nombreSucursal: sucursal.nombre,
|
||||
fechaLimite: fechaLimite,
|
||||
costoBase: Number(sucursal.costoBase),
|
||||
diasIncluidos: sucursal.diasIncluidos,
|
||||
costoDiaAdicional: Number(sucursal.costoDiaAdicional),
|
||||
});
|
||||
|
||||
const textoMensaje2 = templateRecibido2({
|
||||
nombreSucursal: sucursal.nombre,
|
||||
descripcion: sucursal.descripcion || '',
|
||||
direccion: sucursal.direccion,
|
||||
mapsUrl: sucursal.mapsUrl,
|
||||
});
|
||||
|
||||
const textoMensaje3 = templateRecibido3({
|
||||
horariosAtencion: sucursal.horariosAtencion,
|
||||
});
|
||||
|
||||
await this.mensajeService.create({
|
||||
idPaquete: paqueteGuardado.idPaquete,
|
||||
telefonoDestino: `${item.codigoPaisCliente}${item.whatsappCliente}`,
|
||||
tipo: 'RECEPCION_PAQUETE',
|
||||
mensaje1: textoMensaje1,
|
||||
mensaje2: textoMensaje2,
|
||||
mensaje3: textoMensaje3,
|
||||
urlQR: paqueteGuardado.urlQR,
|
||||
sessionId: sucursal.sessionId,
|
||||
});
|
||||
|
||||
paquetesCreados.push(paqueteGuardado);
|
||||
infoPaquetesEmprendedor.push({
|
||||
codigoCompleto,
|
||||
nombreCliente: item.nombreCliente,
|
||||
whatsappCliente: `${item.codigoPaisCliente}${item.whatsappCliente}`,
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Crear mensaje consolidado al emprendedor (si existe)
|
||||
let mensajeEmprendedor = null;
|
||||
if (emprendimiento) {
|
||||
const textoEmprendedor = templateRecibidoEmprendedor({
|
||||
nombreEmprendimiento: emprendimiento.emprendimiento,
|
||||
nombreSucursal: sucursal.nombre,
|
||||
paquetes: infoPaquetesEmprendedor,
|
||||
});
|
||||
|
||||
mensajeEmprendedor = await this.mensajeService.create({
|
||||
idPaquete: paquetesCreados[0].idPaquete,
|
||||
telefonoDestino: `${emprendimiento.codigoPais}${emprendimiento.whatsapp}`,
|
||||
tipo: 'NOTIFICACION_EMPRENDEDOR',
|
||||
mensaje1: textoEmprendedor,
|
||||
sessionId: sucursal.sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
return { idLote, paquetes: paquetesCreados, mensajeEmprendedor };
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdatePaqueteDto) {
|
||||
const paquete = await this.paqueteRepo.findOne({ where: { idPaquete: id } });
|
||||
if (!paquete) throw new NotFoundException('Paquete no encontrado');
|
||||
|
||||
if (paquete.estado === 'ENTREGADO') {
|
||||
throw new ConflictException('El paquete ya fue entregado');
|
||||
}
|
||||
|
||||
Object.assign(paquete, dto);
|
||||
return this.paqueteRepo.save(paquete);
|
||||
}
|
||||
|
||||
async entregar(id: string, dto: EntregarPaqueteDto, idUsuarioEntrega: string) {
|
||||
const paquete = await this.paqueteRepo.findOne({ where: { idPaquete: id } });
|
||||
if (!paquete) throw new NotFoundException('Paquete no encontrado');
|
||||
|
||||
if (paquete.estado !== 'RECIBIDO' && paquete.estado !== 'ALMACENADO') {
|
||||
throw new ConflictException('El paquete no tiene estado válido para entrega');
|
||||
}
|
||||
|
||||
const sucursal = await this.sucursalRepo.findOne({ where: { idSucursal: paquete.idSucursal } });
|
||||
if (!sucursal) throw new NotFoundException('Sucursal no encontrada');
|
||||
|
||||
const hoy = new Date();
|
||||
const feriados = await obtenerFeriadosDelRango(this.feriadoRepo, paquete.fechaRecepcion, hoy);
|
||||
|
||||
const diasAdicionales = calcularDiasAdicionales(
|
||||
paquete.fechaRecepcion,
|
||||
hoy,
|
||||
sucursal.diasIncluidos,
|
||||
feriados,
|
||||
);
|
||||
|
||||
let montoCalculado: number;
|
||||
if (paquete.paquetePagado) {
|
||||
montoCalculado = Number(sucursal.costoDiaAdicional) * diasAdicionales;
|
||||
} else {
|
||||
montoCalculado = Number(sucursal.costoBase) + Number(sucursal.costoDiaAdicional) * diasAdicionales;
|
||||
}
|
||||
|
||||
paquete.estado = 'ENTREGADO';
|
||||
paquete.fechaEntrega = hoy;
|
||||
paquete.idUsuarioEntrega = idUsuarioEntrega;
|
||||
paquete.diasAdicionales = diasAdicionales;
|
||||
paquete.montoCalculadoPaquete = montoCalculado;
|
||||
paquete.montoCobrarPaquete = dto.montoCobrarPaquete ?? montoCalculado;
|
||||
paquete.montoFinal = (dto.montoFinal ?? paquete.montoCobrarPaquete) + Number(paquete.saldoPendiente);
|
||||
|
||||
const paqueteGuardado = await this.paqueteRepo.save(paquete);
|
||||
|
||||
// Generar mensaje PAQUETE_ENTREGADO → se envía al emprendedor
|
||||
let mensaje = null;
|
||||
if (paquete.idEmprendimiento) {
|
||||
const emprendimiento = await this.emprendimientoRepo.findOne({
|
||||
where: { idEmprendimiento: paquete.idEmprendimiento },
|
||||
});
|
||||
|
||||
if (emprendimiento) {
|
||||
const textoMensaje = templateEntregado({
|
||||
nombreCliente: paquete.nombreCliente,
|
||||
whatsappCliente: emprendimiento.whatsapp,
|
||||
codigoCompleto: paquete.codigoCompleto,
|
||||
nombreSucursal: sucursal.nombre,
|
||||
montoCobrar: Number(paquete.montoCobrarPaquete),
|
||||
});
|
||||
|
||||
const telefonoDestino = `${emprendimiento.codigoPais}${emprendimiento.whatsapp}`;
|
||||
|
||||
mensaje = await this.mensajeService.create({
|
||||
idPaquete: paqueteGuardado.idPaquete,
|
||||
telefonoDestino,
|
||||
tipo: 'PAQUETE_ENTREGADO',
|
||||
mensaje1: textoMensaje,
|
||||
sessionId: sucursal.sessionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { paquete: paqueteGuardado, mensaje };
|
||||
}
|
||||
|
||||
async devolver(id: string) {
|
||||
const paquete = await this.paqueteRepo.findOne({ where: { idPaquete: id } });
|
||||
if (!paquete) throw new NotFoundException('Paquete no encontrado');
|
||||
|
||||
if (paquete.estado === 'ENTREGADO') {
|
||||
throw new ConflictException('El paquete ya fue entregado');
|
||||
}
|
||||
if (paquete.estado === 'DEVUELTO') {
|
||||
throw new ConflictException('El paquete ya fue devuelto');
|
||||
}
|
||||
|
||||
paquete.estado = 'DEVUELTO';
|
||||
return this.paqueteRepo.save(paquete);
|
||||
}
|
||||
|
||||
async cambiarUbicacion(id: string, dto: CambiarUbicacionDto) {
|
||||
const paquete = await this.paqueteRepo.findOne({ where: { idPaquete: id } });
|
||||
if (!paquete) throw new NotFoundException('Paquete no encontrado');
|
||||
|
||||
if (paquete.estado === 'ENTREGADO' || paquete.estado === 'DEVUELTO') {
|
||||
throw new ConflictException('El paquete fue entregado o devuelto');
|
||||
}
|
||||
|
||||
paquete.idZonaAlmacenamiento = dto.idZonaAlmacenamiento;
|
||||
return this.paqueteRepo.save(paquete);
|
||||
}
|
||||
|
||||
async estadisticasHoy() {
|
||||
const hoy = new Date();
|
||||
const inicioHoy = new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate());
|
||||
|
||||
const recibidos = await this.paqueteRepo.count({
|
||||
where: { estado: 'RECIBIDO' },
|
||||
});
|
||||
|
||||
const entregados = await this.paqueteRepo.count({
|
||||
where: { estado: 'ENTREGADO' },
|
||||
});
|
||||
|
||||
const devueltos = await this.paqueteRepo.count({
|
||||
where: { estado: 'DEVUELTO' },
|
||||
});
|
||||
|
||||
const porVencer = await this.paqueteRepo
|
||||
.createQueryBuilder('p')
|
||||
.where('p.fechaLimite <= :manana AND p.estado != :entregado', {
|
||||
manana: new Date(hoy.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
entregado: 'ENTREGADO',
|
||||
})
|
||||
.getCount();
|
||||
|
||||
const vencidos = await this.paqueteRepo
|
||||
.createQueryBuilder('p')
|
||||
.where('p.fechaLimite < :hoy AND p.estado != :entregado', {
|
||||
hoy: inicioHoy.toISOString(),
|
||||
entregado: 'ENTREGADO',
|
||||
})
|
||||
.getCount();
|
||||
|
||||
const ingresosResult = await this.paqueteRepo
|
||||
.createQueryBuilder('p')
|
||||
.select('SUM(p.montoCobrarPaquete)', 'total')
|
||||
.where('p.estado = :entregado', { entregado: 'ENTREGADO' })
|
||||
.getRawOne();
|
||||
|
||||
return {
|
||||
fecha: hoy.toISOString().split('T')[0],
|
||||
recibidos,
|
||||
entregados,
|
||||
devueltos,
|
||||
porVencer,
|
||||
vencidos,
|
||||
ingresosDelDia: Number(ingresosResult?.total) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
async estadisticasPeriodo(fechaDesde: string, fechaHasta: string) {
|
||||
const recibidos = await this.paqueteRepo
|
||||
.createQueryBuilder('p')
|
||||
.where('p.fechaRecepcion >= :desde AND p.fechaRecepcion <= :hasta', {
|
||||
desde: fechaDesde,
|
||||
hasta: fechaHasta,
|
||||
})
|
||||
.getCount();
|
||||
|
||||
const entregados = await this.paqueteRepo
|
||||
.createQueryBuilder('p')
|
||||
.where('p.fechaEntrega >= :desde AND p.fechaEntrega <= :hasta', {
|
||||
desde: fechaDesde,
|
||||
hasta: fechaHasta,
|
||||
})
|
||||
.getCount();
|
||||
|
||||
const devueltos = await this.paqueteRepo
|
||||
.createQueryBuilder('p')
|
||||
.where('p.estado = :devuelto AND p.fechaRecepcion >= :desde AND p.fechaRecepcion <= :hasta', {
|
||||
devuelto: 'DEVUELTO',
|
||||
desde: fechaDesde,
|
||||
hasta: fechaHasta,
|
||||
})
|
||||
.getCount();
|
||||
|
||||
const ingresosResult = await this.paqueteRepo
|
||||
.createQueryBuilder('p')
|
||||
.select('SUM(p.montoCobrarPaquete)', 'total')
|
||||
.where('p.estado = :entregado AND p.fechaEntrega >= :desde AND p.fechaEntrega <= :hasta', {
|
||||
entregado: 'ENTREGADO',
|
||||
desde: fechaDesde,
|
||||
hasta: fechaHasta,
|
||||
})
|
||||
.getRawOne();
|
||||
|
||||
const pendientes = await this.paqueteRepo.count({
|
||||
where: { estado: 'RECIBIDO' },
|
||||
});
|
||||
|
||||
return {
|
||||
fechaDesde,
|
||||
fechaHasta,
|
||||
totalRecibidos: recibidos,
|
||||
totalEntregados: entregados,
|
||||
totalDevueltos: devueltos,
|
||||
ingresosTotales: Number(ingresosResult?.total) || 0,
|
||||
paquetesPendientes: pendientes,
|
||||
};
|
||||
}
|
||||
|
||||
async previewLimpiar(fechaDesde: string, fechaHasta: string) {
|
||||
const paquetes = await this.paqueteRepo
|
||||
.createQueryBuilder('p')
|
||||
.where('p.estado = :entregado AND p.fechaEntrega >= :desde AND p.fechaEntrega <= :hasta', {
|
||||
entregado: 'ENTREGADO',
|
||||
desde: fechaDesde,
|
||||
hasta: fechaHasta,
|
||||
})
|
||||
.getMany();
|
||||
|
||||
let totalArchivos = 0;
|
||||
let totalBytes = 0;
|
||||
|
||||
for (const paquete of paquetes) {
|
||||
const fotoPath = path.join(process.cwd(), paquete.fotoUrl);
|
||||
const qrPath = path.join(process.cwd(), paquete.urlQR);
|
||||
|
||||
if (fs.existsSync(fotoPath)) {
|
||||
totalArchivos++;
|
||||
totalBytes += fs.statSync(fotoPath).size;
|
||||
}
|
||||
if (fs.existsSync(qrPath)) {
|
||||
totalArchivos++;
|
||||
totalBytes += fs.statSync(qrPath).size;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
paquetesEncontrados: paquetes.length,
|
||||
archivosAEliminar: totalArchivos,
|
||||
espacioLiberado: `${(totalBytes / 1024 / 1024).toFixed(2)} MB`,
|
||||
};
|
||||
}
|
||||
|
||||
async limpiar(fechaDesde: string, fechaHasta: string) {
|
||||
const paquetes = await this.paqueteRepo
|
||||
.createQueryBuilder('p')
|
||||
.where('p.estado = :entregado AND p.fechaEntrega >= :desde AND p.fechaEntrega <= :hasta', {
|
||||
entregado: 'ENTREGADO',
|
||||
desde: fechaDesde,
|
||||
hasta: fechaHasta,
|
||||
})
|
||||
.getMany();
|
||||
|
||||
let archivosEliminados = 0;
|
||||
|
||||
for (const paquete of paquetes) {
|
||||
const fotoPath = path.join(process.cwd(), paquete.fotoUrl);
|
||||
if (fs.existsSync(fotoPath)) {
|
||||
fs.unlinkSync(fotoPath);
|
||||
archivosEliminados++;
|
||||
}
|
||||
|
||||
const qrPath = path.join(process.cwd(), paquete.urlQR);
|
||||
if (fs.existsSync(qrPath)) {
|
||||
fs.unlinkSync(qrPath);
|
||||
archivosEliminados++;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.paqueteRepo
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(Paquete)
|
||||
.where('estado = :entregado AND fechaEntrega >= :desde AND fechaEntrega <= :hasta', {
|
||||
entregado: 'ENTREGADO',
|
||||
desde: fechaDesde,
|
||||
hasta: fechaHasta,
|
||||
})
|
||||
.execute();
|
||||
|
||||
return {
|
||||
paquetesEliminados: result.affected || 0,
|
||||
archivosEliminados,
|
||||
};
|
||||
}
|
||||
}
|
||||
86
src/modules/sucursal/dto/sucursal.dto.ts
Normal file
86
src/modules/sucursal/dto/sucursal.dto.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString, IsNumber, IsOptional, IsBoolean, IsUUID } from 'class-validator';
|
||||
|
||||
export class SucursalDto {
|
||||
@ApiProperty({ example: 'uuid-id' })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
idSucursal: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 'Sucursal Central' })
|
||||
nombre: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 'Av. 123 Edificio 456' })
|
||||
direccion: string;
|
||||
|
||||
@IsOptional()
|
||||
@ApiProperty({ example: 'Subiendo las gradas' })
|
||||
descripcion?: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 'Lunes a Viernes de 08:00 a 19:00' })
|
||||
horariosAtencion: string;
|
||||
|
||||
@IsOptional()
|
||||
@ApiProperty({ example: 'https://maps.app.goo.gl/...' })
|
||||
mapsUrl?: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 'S1' })
|
||||
prefijo: string;
|
||||
|
||||
@IsOptional()
|
||||
@ApiProperty({ example: '#4338CA' })
|
||||
colorSucursal?: string;
|
||||
|
||||
@IsNumber()
|
||||
@ApiProperty({ example: 1000 })
|
||||
cantidadCodigos: number;
|
||||
|
||||
@IsOptional()
|
||||
@ApiProperty({ example: '{prefijo}-{numero}-{letra}' })
|
||||
formatoCodigo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ApiProperty({ example: true })
|
||||
usarPrefijoSucursal?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiProperty({ example: true })
|
||||
usarPrefijoZonaAlmacenamiento?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiProperty({ example: false })
|
||||
rotarColorAlFinalizarZ?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiProperty({ example: true })
|
||||
whatsappActivo?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiProperty({ example: 'session-abc-123' })
|
||||
sessionId?: string;
|
||||
|
||||
// Campos heredados de Empresa (editables por sucursal)
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@ApiProperty({ example: 5 })
|
||||
diasIncluidos?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@ApiProperty({ example: 5.00 })
|
||||
costoBase?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@ApiProperty({ example: 2.00 })
|
||||
costoDiaAdicional?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@ApiProperty({ example: 15 })
|
||||
diasParaAlmacen?: number;
|
||||
}
|
||||
4
src/modules/sucursal/dto/update-sucursal.dto.ts
Normal file
4
src/modules/sucursal/dto/update-sucursal.dto.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { PartialType } from '@nestjs/swagger';
|
||||
import { SucursalDto } from './sucursal.dto.js';
|
||||
|
||||
export class UpdateSucursalDto extends PartialType(SucursalDto) { }
|
||||
74
src/modules/sucursal/entities/sucursal.entity.ts
Normal file
74
src/modules/sucursal/entities/sucursal.entity.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Sucursal {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
idSucursal: string;
|
||||
|
||||
@Column()
|
||||
nombre: string;
|
||||
|
||||
@Column()
|
||||
direccion: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
descripcion: string;
|
||||
|
||||
@Column()
|
||||
horariosAtencion: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
mapsUrl: string;
|
||||
|
||||
@Column()
|
||||
colorSucursal: string;
|
||||
|
||||
@Column()
|
||||
prefijo: string;
|
||||
|
||||
// Configuración heredada de Empresa
|
||||
@Column({ type: 'int' })
|
||||
diasIncluidos: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
costoBase: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
costoDiaAdicional: number;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
diasParaAlmacen: number;
|
||||
|
||||
// Gestión del ciclo visual (Letras y Colores)
|
||||
@Column({ default: 'A' })
|
||||
letraActual: string;
|
||||
|
||||
@Column({ type: 'int', default: 1 })
|
||||
numeroActual: number;
|
||||
|
||||
@Column({ type: 'int', default: 1000 })
|
||||
cantidadCodigos: number;
|
||||
|
||||
@Column()
|
||||
colorActual: string;
|
||||
|
||||
// Personalización del código
|
||||
@Column({ default: false })
|
||||
rotarColorAlFinalizarZ: boolean;
|
||||
|
||||
@Column({ default: true })
|
||||
usarPrefijoSucursal: boolean;
|
||||
|
||||
@Column({ default: true })
|
||||
usarPrefijoZonaAlmacenamiento: boolean;
|
||||
|
||||
@Column({ default: '{prefijo}-{numero}-{letra}' })
|
||||
formatoCodigo: string;
|
||||
|
||||
// WhatsApp
|
||||
@Column({ nullable: true })
|
||||
sessionId: string;
|
||||
|
||||
@Column({ default: true })
|
||||
whatsappActivo: boolean;
|
||||
}
|
||||
176
src/modules/sucursal/sucursal.controller.ts
Normal file
176
src/modules/sucursal/sucursal.controller.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import { Controller, Get, Post, Body, Patch, Param, Delete, HttpStatus } from '@nestjs/common';
|
||||
import { SucursalService } from './sucursal.service.js';
|
||||
import { SucursalDto } from './dto/sucursal.dto.js';
|
||||
import { UpdateSucursalDto } from './dto/update-sucursal.dto.js';
|
||||
import { ApiResult, defaultApiResult } from '../../shared/interfaces/api.result.js';
|
||||
import { FindDto } from '../../shared/class/find.dto.js';
|
||||
import { routeSucursal } from './sucursal.router.js';
|
||||
|
||||
@Controller('sucursal')
|
||||
export class SucursalController {
|
||||
constructor(private readonly sucursalService: SucursalService) { }
|
||||
|
||||
@Post()
|
||||
async create(@Body() sucursalDto: SucursalDto): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeSucursal.create.title,
|
||||
routeSucursal.create.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.sucursalService.create(sucursalDto);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = [result.object];
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status;
|
||||
apiResult.message = error;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Get(':idSucursal')
|
||||
async findOne(@Param('idSucursal') idSucursal: string): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeSucursal.one.title,
|
||||
routeSucursal.one.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.sucursalService.findOne(idSucursal);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = [result.object];
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status;
|
||||
apiResult.message = error;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Post('find')
|
||||
async find(@Body() findByDto: FindDto): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeSucursal.find.title,
|
||||
routeSucursal.find.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const {
|
||||
orderByDirection,
|
||||
orderByAttribute,
|
||||
whereConditions,
|
||||
limit,
|
||||
offset,
|
||||
} = findByDto;
|
||||
|
||||
const result = await this.sucursalService.find(
|
||||
whereConditions,
|
||||
orderByAttribute,
|
||||
orderByDirection,
|
||||
limit,
|
||||
offset,
|
||||
);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = result.data;
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Patch(':idSucursal')
|
||||
async update(
|
||||
@Param('idSucursal') idSucursal: string,
|
||||
@Body() updateSucursalDto: UpdateSucursalDto,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeSucursal.update.title,
|
||||
routeSucursal.update.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.sucursalService.update(
|
||||
idSucursal,
|
||||
updateSucursalDto,
|
||||
);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
apiResult.data = [result.object];
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status;
|
||||
apiResult.message = error;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Delete(':idSucursal')
|
||||
async remove(@Param('idSucursal') idSucursal: string): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeSucursal.remove.title,
|
||||
routeSucursal.remove.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.sucursalService.remove(idSucursal);
|
||||
|
||||
if (result.boolean) {
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = result.message;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.number;
|
||||
} else {
|
||||
apiResult.code = HttpStatus.BAD_REQUEST;
|
||||
apiResult.message = result.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status;
|
||||
apiResult.message = error;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
}
|
||||
14
src/modules/sucursal/sucursal.module.ts
Normal file
14
src/modules/sucursal/sucursal.module.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { SucursalController } from './sucursal.controller.js';
|
||||
import { SucursalService } from './sucursal.service.js';
|
||||
import { Sucursal } from './entities/sucursal.entity.js';
|
||||
import { Empresa } from '../empresa/entities/empresa.entity.js';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Sucursal, Empresa])],
|
||||
controllers: [SucursalController],
|
||||
providers: [SucursalService],
|
||||
exports: [SucursalService],
|
||||
})
|
||||
export class SucursalModule {}
|
||||
26
src/modules/sucursal/sucursal.router.ts
Normal file
26
src/modules/sucursal/sucursal.router.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export const routeSucursal = {
|
||||
multiple: {
|
||||
route: '[POST].../v1/sucursal/multiple',
|
||||
title: 'Create Multiple Sucursal',
|
||||
},
|
||||
create: {
|
||||
route: '[POST].../v1/sucursal',
|
||||
title: 'Create Sucursal',
|
||||
},
|
||||
one: {
|
||||
route: '[GET].../v1/sucursal/:idSucursal',
|
||||
title: 'Find One Sucursal',
|
||||
},
|
||||
find: {
|
||||
route: '[POST].../v1/sucursal/find',
|
||||
title: 'Find Sucursal WhereCondition[], Attribute & Direccion',
|
||||
},
|
||||
update: {
|
||||
route: '[PATCH].../v1/sucursal/:idSucursal',
|
||||
title: 'Update Sucursal',
|
||||
},
|
||||
remove: {
|
||||
route: '[DELETE].../v1/sucursal/:idSucursal',
|
||||
title: 'Remove Sucursal',
|
||||
},
|
||||
};
|
||||
150
src/modules/sucursal/sucursal.service.ts
Normal file
150
src/modules/sucursal/sucursal.service.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { SucursalDto } from './dto/sucursal.dto.js';
|
||||
import { UpdateSucursalDto } from './dto/update-sucursal.dto.js';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { defaultServiceResult, ServiceResult } from '../../shared/interfaces/service.result.js';
|
||||
import { WhereCondition } from '../../shared/type/query-builder.types.js';
|
||||
import { Sucursal } from './entities/sucursal.entity.js';
|
||||
import { Empresa } from '../empresa/entities/empresa.entity.js';
|
||||
|
||||
@Injectable()
|
||||
export class SucursalService {
|
||||
constructor(
|
||||
@InjectRepository(Sucursal)
|
||||
private sucursalRepo: Repository<Sucursal>,
|
||||
@InjectRepository(Empresa)
|
||||
private empresaRepo: Repository<Empresa>,
|
||||
) { }
|
||||
|
||||
async create(sucursalDto: SucursalDto): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
// Heredar valores de Empresa
|
||||
const empresa = await this.empresaRepo.findOne({ where: {} });
|
||||
|
||||
const sucursalData = {
|
||||
...sucursalDto,
|
||||
diasIncluidos: empresa?.diasIncluidos ?? 5,
|
||||
costoBase: empresa?.costoBase ?? 5,
|
||||
costoDiaAdicional: empresa?.costoDiaAdicional ?? 2,
|
||||
diasParaAlmacen: empresa?.diasParaAlmacen ?? 15,
|
||||
colorActual: sucursalDto.colorSucursal ?? '#4338CA',
|
||||
};
|
||||
|
||||
const result = this.sucursalRepo.create(sucursalData);
|
||||
await this.sucursalRepo.save(result);
|
||||
|
||||
serviceResult.boolean = true;
|
||||
serviceResult.message = 'Sucursal se ha agregado correctamente.';
|
||||
serviceResult.number = 1;
|
||||
serviceResult.object = result;
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async findOne(idSucursal: string): Promise<ServiceResult> {
|
||||
let serviceResult = {
|
||||
boolean: false,
|
||||
message: '',
|
||||
number: 0,
|
||||
object: null,
|
||||
data: null,
|
||||
} as ServiceResult;
|
||||
|
||||
const result = await this.sucursalRepo.findOneBy({ idSucursal });
|
||||
|
||||
if (result) {
|
||||
serviceResult.boolean = true;
|
||||
serviceResult.message = 'Existe una Sucursal.';
|
||||
serviceResult.number = 1;
|
||||
serviceResult.object = result;
|
||||
} else {
|
||||
serviceResult.message = 'No existe Sucursal.';
|
||||
}
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async find(
|
||||
whereConditions: WhereCondition[],
|
||||
orderByAttribute: string,
|
||||
orderByDirection: 'ASC' | 'DESC',
|
||||
limit: number,
|
||||
offset: number,
|
||||
): Promise<ServiceResult> {
|
||||
const serviceResult = defaultServiceResult();
|
||||
|
||||
try {
|
||||
let query = this.sucursalRepo.createQueryBuilder('sucursal');
|
||||
|
||||
whereConditions.forEach((condition, index) => {
|
||||
const { attribute, value, operator = '=' } = condition;
|
||||
const paramKey = `param${index}`;
|
||||
const param = {
|
||||
[paramKey]: operator === 'like' ? `%${value}%` : value,
|
||||
};
|
||||
const whereMethod = index === 0 ? 'where' : 'andWhere';
|
||||
|
||||
query = query[whereMethod](
|
||||
`sucursal.${attribute} ${operator} :${paramKey}`,
|
||||
param,
|
||||
);
|
||||
});
|
||||
|
||||
query = query.orderBy(`sucursal.${orderByAttribute}`, orderByDirection);
|
||||
|
||||
const totalRegistros = await query.getCount();
|
||||
const skip = (offset - 1) * limit;
|
||||
|
||||
query = query.skip(skip).take(limit);
|
||||
|
||||
const result = await query.getMany();
|
||||
const count = result.length;
|
||||
|
||||
serviceResult.boolean = count > 0;
|
||||
serviceResult.message = `${count} Sucursal(es) encontrado(s).`;
|
||||
serviceResult.number = totalRegistros;
|
||||
serviceResult.data = result;
|
||||
} catch (error: any) {
|
||||
serviceResult.boolean = false;
|
||||
serviceResult.message = `Error en el servicio: ${error.message}`;
|
||||
}
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async update(
|
||||
idSucursal: string,
|
||||
updateSucursalDto: UpdateSucursalDto,
|
||||
): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
const result = await this.sucursalRepo.update(
|
||||
idSucursal,
|
||||
updateSucursalDto,
|
||||
);
|
||||
|
||||
serviceResult.boolean = result.affected === 1 ? true : false;
|
||||
serviceResult.message = 'Se ha actualizado correctamente.';
|
||||
serviceResult.number = result.affected || 0;
|
||||
serviceResult.object = result;
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
|
||||
async remove(idSucursal: string): Promise<ServiceResult> {
|
||||
let serviceResult = defaultServiceResult();
|
||||
|
||||
const result = await this.sucursalRepo.delete(idSucursal);
|
||||
|
||||
serviceResult.boolean = result.affected === 1 ? true : false;
|
||||
serviceResult.message =
|
||||
result.affected === 1
|
||||
? 'Se ha eliminado correctamente.'
|
||||
: 'No se ha encontrado la Sucursal.';
|
||||
serviceResult.number = result.affected || 0;
|
||||
|
||||
return serviceResult;
|
||||
}
|
||||
}
|
||||
14
src/modules/upload/dto/generar-qr.dto.ts
Normal file
14
src/modules/upload/dto/generar-qr.dto.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { IsString, IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class GenerarQrDto {
|
||||
@ApiProperty({ description: 'UUID v4 del token de retiro' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
tokenRetiro: string;
|
||||
|
||||
@ApiProperty({ description: 'Código completo del paquete' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
codigoPaquete: string;
|
||||
}
|
||||
16
src/modules/upload/dto/procesar-ocr.dto.ts
Normal file
16
src/modules/upload/dto/procesar-ocr.dto.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { IsOptional, IsIn } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export const OCR_PROVIDERS = ['tesseract', 'ocrspace', 'google-vision', 'mimo'] as const;
|
||||
export type OcrProviderName = (typeof OCR_PROVIDERS)[number];
|
||||
|
||||
export class ProcesarOcrDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Proveedor de OCR a utilizar',
|
||||
enum: OCR_PROVIDERS,
|
||||
default: 'tesseract',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(OCR_PROVIDERS)
|
||||
provider?: OcrProviderName;
|
||||
}
|
||||
123
src/modules/upload/upload.controller.ts
Normal file
123
src/modules/upload/upload.controller.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Param,
|
||||
Body,
|
||||
UseInterceptors,
|
||||
UploadedFile,
|
||||
Res,
|
||||
NotFoundException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { UploadService } from './upload.service.js';
|
||||
import { GenerarQrDto } from './dto/generar-qr.dto.js';
|
||||
import { ApiResult, defaultApiResult } from '../../shared/interfaces/api.result.js';
|
||||
import { routeUpload } from './upload.router.js';
|
||||
|
||||
@Controller('upload')
|
||||
export class UploadController {
|
||||
constructor(private readonly service: UploadService) {}
|
||||
|
||||
@Post('paquete')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async subirPaquete(@UploadedFile() file: Express.Multer.File): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeUpload.subirPaquete.title,
|
||||
routeUpload.subirPaquete.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.service.subirPaquete(file);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Imagen subida correctamente';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [result];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message || 'Error al procesar';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Post('qr')
|
||||
async generarQr(@Body() dto: GenerarQrDto): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeUpload.generarQr.title,
|
||||
routeUpload.generarQr.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.service.generarQr(dto.tokenRetiro, dto.codigoPaquete);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'QR generado correctamente';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [result];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message || 'Error al procesar';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Post('ocr')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async procesarOcr(
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeUpload.procesarOcr.title,
|
||||
routeUpload.procesarOcr.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.service.procesarOcr(file);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'OCR procesado correctamente';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [result];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message || 'Error al procesar';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Get(':filename')
|
||||
async servirArchivo(@Param('filename') filename: string, @Res() res: any): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeUpload.servirArchivo.title,
|
||||
routeUpload.servirArchivo.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), 'uploads', filename);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new NotFoundException('Archivo no encontrado');
|
||||
}
|
||||
|
||||
return res.sendFile(filePath);
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message || 'Error al procesar';
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
}
|
||||
17
src/modules/upload/upload.module.ts
Normal file
17
src/modules/upload/upload.module.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { MulterModule } from '@nestjs/platform-express';
|
||||
import * as multer from 'multer';
|
||||
import { UploadController } from './upload.controller.js';
|
||||
import { UploadService } from './upload.service.js';
|
||||
import { OcrModule } from '../../shared/services/ocr/ocr.module.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MulterModule.register({ storage: multer.memoryStorage() }),
|
||||
OcrModule,
|
||||
],
|
||||
controllers: [UploadController],
|
||||
providers: [UploadService],
|
||||
exports: [UploadService],
|
||||
})
|
||||
export class UploadModule {}
|
||||
18
src/modules/upload/upload.router.ts
Normal file
18
src/modules/upload/upload.router.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export const routeUpload = {
|
||||
subirPaquete: {
|
||||
route: '[POST].../v1/upload/paquete',
|
||||
title: 'Subir Imagen Paquete',
|
||||
},
|
||||
generarQr: {
|
||||
route: '[POST].../v1/upload/qr',
|
||||
title: 'Generar QR Paquete',
|
||||
},
|
||||
procesarOcr: {
|
||||
route: '[POST].../v1/upload/ocr',
|
||||
title: 'Procesar OCR Imagen',
|
||||
},
|
||||
servirArchivo: {
|
||||
route: '[GET].../v1/upload/:filename',
|
||||
title: 'Servir Archivo',
|
||||
},
|
||||
};
|
||||
121
src/modules/upload/upload.service.ts
Normal file
121
src/modules/upload/upload.service.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import sharp from 'sharp';
|
||||
import * as QRCode from 'qrcode';
|
||||
import { OcrFactory } from '../../shared/services/ocr/ocr.factory.js';
|
||||
|
||||
@Injectable()
|
||||
export class UploadService {
|
||||
private uploadsPath = path.join(process.cwd(), 'uploads');
|
||||
|
||||
constructor(
|
||||
private readonly ocrFactory: OcrFactory,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async subirPaquete(file: Express.Multer.File): Promise<{ url: string }> {
|
||||
this.validarArchivo(file);
|
||||
|
||||
const compressBuffer = await this.comprimirSiNecesario(file.buffer);
|
||||
const filename = `${uuidv4()}.jpg`;
|
||||
const filePath = path.join(this.uploadsPath, 'paquetes', filename);
|
||||
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, compressBuffer);
|
||||
|
||||
return { url: `/uploads/paquetes/${filename}` };
|
||||
}
|
||||
|
||||
async generarQr(tokenRetiro: string, codigoPaquete: string): Promise<{ url: string }> {
|
||||
const filename = `${tokenRetiro}.png`;
|
||||
const filePath = path.join(this.uploadsPath, 'qr', filename);
|
||||
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
|
||||
const qrBuffer = await QRCode.toBuffer(tokenRetiro, {
|
||||
type: 'png',
|
||||
width: 400,
|
||||
margin: 2,
|
||||
color: { dark: '#000000', light: '#FFFFFF' },
|
||||
});
|
||||
|
||||
fs.writeFileSync(filePath, qrBuffer);
|
||||
|
||||
return { url: `/uploads/qr/${filename}` };
|
||||
}
|
||||
|
||||
async procesarOcr(
|
||||
file: Express.Multer.File,
|
||||
): Promise<{
|
||||
fotoUrl: string;
|
||||
cliente: { nombreCliente: string; whatsappCliente: string };
|
||||
}> {
|
||||
this.validarArchivo(file);
|
||||
|
||||
const provider = this.ocrFactory.getProvider();
|
||||
|
||||
// Comprimir solo para mimo (reduce size para API)
|
||||
let ocrBuffer: Buffer;
|
||||
const providerName = this.configService.get<string>('OCR_PROVIDER', 'tesseract');
|
||||
if (providerName === 'mimo') {
|
||||
ocrBuffer = await this.comprimirSiNecesario(file.buffer);
|
||||
} else {
|
||||
ocrBuffer = file.buffer;
|
||||
}
|
||||
|
||||
const filename = `${uuidv4()}.jpg`;
|
||||
const filePath = path.join(this.uploadsPath, 'paquetes', filename);
|
||||
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, ocrBuffer);
|
||||
|
||||
let nombreCliente = '';
|
||||
let whatsappCliente = '';
|
||||
|
||||
try {
|
||||
const ocrResult = await provider.reconocer(ocrBuffer);
|
||||
console.log('DEBUG OCR result from provider:', JSON.stringify(ocrResult));
|
||||
nombreCliente = ocrResult.nombre || '';
|
||||
whatsappCliente = ocrResult.whatsapp || '';
|
||||
} catch (error: any) {
|
||||
console.log('DEBUG OCR failed, continuing without client data:', error.message);
|
||||
}
|
||||
|
||||
return {
|
||||
fotoUrl: `/uploads/paquetes/${filename}`,
|
||||
cliente: { nombreCliente, whatsappCliente },
|
||||
};
|
||||
}
|
||||
|
||||
private validarArchivo(file: Express.Multer.File): void {
|
||||
if (!file) {
|
||||
throw new BadRequestException('No se proporcionó archivo');
|
||||
}
|
||||
|
||||
const maxSize = 400 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
throw new BadRequestException('El archivo supera los 400 KB');
|
||||
}
|
||||
|
||||
const allowedMimes = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
if (!allowedMimes.includes(file.mimetype)) {
|
||||
throw new BadRequestException('Formato de archivo no soportado. Use JPEG, PNG o WebP');
|
||||
}
|
||||
}
|
||||
|
||||
private async comprimirSiNecesario(buffer: Buffer): Promise<Buffer> {
|
||||
const maxSize = 400 * 1024;
|
||||
|
||||
if (buffer.length <= maxSize) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return sharp(buffer)
|
||||
.resize({ width: 1920, height: 1920, fit: 'inside', withoutEnlargement: true })
|
||||
.jpeg({ quality: 80 })
|
||||
.toBuffer();
|
||||
}
|
||||
}
|
||||
4
src/modules/usuario/dto/update-usuario.dto.ts
Normal file
4
src/modules/usuario/dto/update-usuario.dto.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { PartialType } from '@nestjs/swagger';
|
||||
import { UsuarioDto } from './usuario.dto.js';
|
||||
|
||||
export class UpdateUsuarioDto extends PartialType(UsuarioDto) {}
|
||||
38
src/modules/usuario/dto/usuario.dto.ts
Normal file
38
src/modules/usuario/dto/usuario.dto.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { IsString, IsNotEmpty, IsEnum, MinLength, IsOptional, IsUUID } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UsuarioDto {
|
||||
@ApiProperty({ example: 'uuid-id' })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
idUsuario?: string;
|
||||
|
||||
@ApiProperty({ example: 'Juan Pérez' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
nombre: string;
|
||||
|
||||
@ApiProperty({ example: 'juan' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
usuario: string;
|
||||
|
||||
@ApiProperty({ example: 'password123' })
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
password: string;
|
||||
|
||||
@ApiProperty({ example: '¿Cuál es tu color favorito?' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
pregunta: string;
|
||||
|
||||
@ApiProperty({ example: 'azul' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
respuesta: string;
|
||||
|
||||
@ApiProperty({ enum: ['ADMIN', 'ENCARGADO', 'RECEPCION', 'ENTREGA'] })
|
||||
@IsEnum(['ADMIN', 'ENCARGADO', 'RECEPCION', 'ENTREGA'])
|
||||
rol: string;
|
||||
}
|
||||
25
src/modules/usuario/entities/usuario.entity.ts
Normal file
25
src/modules/usuario/entities/usuario.entity.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Usuario {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
idUsuario: string;
|
||||
|
||||
@Column()
|
||||
nombre: string;
|
||||
|
||||
@Column()
|
||||
usuario: string;
|
||||
|
||||
@Column()
|
||||
password: string;
|
||||
|
||||
@Column()
|
||||
pregunta: string;
|
||||
|
||||
@Column()
|
||||
respuesta: string;
|
||||
|
||||
@Column({ type: 'enum', enum: ['ADMIN', 'ENCARGADO', 'RECEPCION', 'ENTREGA'] })
|
||||
rol: string;
|
||||
}
|
||||
136
src/modules/usuario/usuario.controller.ts
Normal file
136
src/modules/usuario/usuario.controller.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { Controller, Get, Post, Patch, Delete, Body, Param, HttpStatus } from '@nestjs/common';
|
||||
import { UsuarioService } from './usuario.service.js';
|
||||
import { UsuarioDto } from './dto/usuario.dto.js';
|
||||
import { UpdateUsuarioDto } from './dto/update-usuario.dto.js';
|
||||
import { ApiResult, defaultApiResult } from '../../shared/interfaces/api.result.js';
|
||||
import { routeUsuario } from './usuario.router.js';
|
||||
import { Public } from '../../shared/interfaces/auth.interface.js';
|
||||
import { RequireApiKey } from '../../shared/interfaces/api-key.interface.js';
|
||||
|
||||
@Controller('usuario')
|
||||
export class UsuarioController {
|
||||
constructor(private readonly service: UsuarioService) {}
|
||||
|
||||
@Get()
|
||||
async findAll(): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeUsuario.find.title,
|
||||
routeUsuario.find.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await this.service.findAll();
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'OK';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = data.length;
|
||||
apiResult.data = data;
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Get(':idUsuario')
|
||||
async findOne(@Param('idUsuario') idUsuario: string): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeUsuario.one.title,
|
||||
routeUsuario.one.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await this.service.findOne(idUsuario);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'OK';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [data];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Public()
|
||||
@RequireApiKey()
|
||||
async create(@Body() dto: UsuarioDto): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeUsuario.create.title,
|
||||
routeUsuario.create.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await this.service.create(dto);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.CREATED;
|
||||
apiResult.message = 'Usuario registrado correctamente';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [data];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Patch(':idUsuario')
|
||||
async update(
|
||||
@Param('idUsuario') idUsuario: string,
|
||||
@Body() dto: UpdateUsuarioDto,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeUsuario.update.title,
|
||||
routeUsuario.update.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await this.service.update(idUsuario, dto);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Usuario actualizado correctamente';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [data];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Delete(':idUsuario')
|
||||
async remove(@Param('idUsuario') idUsuario: string): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeUsuario.remove.title,
|
||||
routeUsuario.remove.route,
|
||||
);
|
||||
|
||||
try {
|
||||
await this.service.remove(idUsuario);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Usuario eliminado correctamente';
|
||||
apiResult.boolean = true;
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
}
|
||||
25
src/modules/usuario/usuario.module.ts
Normal file
25
src/modules/usuario/usuario.module.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { UsuarioController } from './usuario.controller.js';
|
||||
import { UsuarioService } from './usuario.service.js';
|
||||
import { Usuario } from './entities/usuario.entity.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Usuario]),
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
secret: config.get('JWT_SECRET'),
|
||||
signOptions: { expiresIn: config.get('JWT_EXPIRATION', '7d') },
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [UsuarioController],
|
||||
providers: [UsuarioService],
|
||||
exports: [UsuarioService],
|
||||
})
|
||||
export class UsuarioModule {}
|
||||
26
src/modules/usuario/usuario.router.ts
Normal file
26
src/modules/usuario/usuario.router.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export const routeUsuario = {
|
||||
multiple: {
|
||||
route: '[POST].../v1/usuario/multiple',
|
||||
title: 'Create Multiple Usuario',
|
||||
},
|
||||
create: {
|
||||
route: '[POST].../v1/usuario',
|
||||
title: 'Create Usuario',
|
||||
},
|
||||
one: {
|
||||
route: '[GET].../v1/usuario/:idUsuario',
|
||||
title: 'Find One Usuario',
|
||||
},
|
||||
find: {
|
||||
route: '[POST].../v1/usuario/find',
|
||||
title: 'Find Usuario WhereCondition[], Attribute & Direccion',
|
||||
},
|
||||
update: {
|
||||
route: '[PATCH].../v1/usuario/:idUsuario',
|
||||
title: 'Update Usuario',
|
||||
},
|
||||
remove: {
|
||||
route: '[DELETE].../v1/usuario/:idUsuario',
|
||||
title: 'Remove Usuario',
|
||||
},
|
||||
};
|
||||
93
src/modules/usuario/usuario.service.ts
Normal file
93
src/modules/usuario/usuario.service.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { Injectable, ConflictException, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { Usuario } from './entities/usuario.entity.js';
|
||||
import { UsuarioDto } from './dto/usuario.dto.js';
|
||||
import { UpdateUsuarioDto } from './dto/update-usuario.dto.js';
|
||||
|
||||
@Injectable()
|
||||
export class UsuarioService {
|
||||
constructor(
|
||||
@InjectRepository(Usuario)
|
||||
private repo: Repository<Usuario>,
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async findAll() {
|
||||
return this.repo.find({
|
||||
select: {
|
||||
idUsuario: true,
|
||||
nombre: true,
|
||||
usuario: true,
|
||||
rol: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const usuario = await this.repo.findOne({
|
||||
where: { idUsuario: id },
|
||||
select: {
|
||||
idUsuario: true,
|
||||
nombre: true,
|
||||
usuario: true,
|
||||
rol: true,
|
||||
},
|
||||
});
|
||||
if (!usuario) throw new NotFoundException('Usuario no encontrado');
|
||||
return usuario;
|
||||
}
|
||||
|
||||
async create(data: UsuarioDto) {
|
||||
const exists = await this.repo.findOne({ where: { usuario: data.usuario } });
|
||||
if (exists) throw new ConflictException('El nombre de usuario ya existe');
|
||||
|
||||
const hashedPassword = await bcrypt.hash(data.password!, 10);
|
||||
const hashedRespuesta = await bcrypt.hash(data.respuesta!, 10);
|
||||
|
||||
const usuario = this.repo.create({
|
||||
...data,
|
||||
password: hashedPassword,
|
||||
respuesta: hashedRespuesta,
|
||||
} as any);
|
||||
|
||||
const saved = await this.repo.save(usuario);
|
||||
const { password, respuesta, ...result } = saved as any;
|
||||
|
||||
const payload = {
|
||||
sub: result.idUsuario,
|
||||
usuario: result.usuario,
|
||||
rol: result.rol,
|
||||
};
|
||||
|
||||
return {
|
||||
token: this.jwtService.sign(payload),
|
||||
usuario: result,
|
||||
};
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateUsuarioDto) {
|
||||
const usuario = await this.repo.findOne({ where: { idUsuario: id } });
|
||||
if (!usuario) throw new NotFoundException('Usuario no encontrado');
|
||||
|
||||
if (data.password) {
|
||||
data.password = await bcrypt.hash(data.password, 10);
|
||||
}
|
||||
if (data.respuesta) {
|
||||
data.respuesta = await bcrypt.hash(data.respuesta, 10);
|
||||
}
|
||||
|
||||
Object.assign(usuario, data);
|
||||
const saved = await this.repo.save(usuario);
|
||||
const { password, respuesta, ...result } = saved;
|
||||
return result;
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const usuario = await this.repo.findOne({ where: { idUsuario: id } });
|
||||
if (!usuario) throw new NotFoundException('Usuario no encontrado');
|
||||
await this.repo.remove(usuario);
|
||||
}
|
||||
}
|
||||
4
src/modules/zona/dto/update-zona.dto.ts
Normal file
4
src/modules/zona/dto/update-zona.dto.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { PartialType } from '@nestjs/swagger';
|
||||
import { ZonaAlmacenamientoDto } from './zona.dto.js';
|
||||
|
||||
export class UpdateZonaAlmacenamientoDto extends PartialType(ZonaAlmacenamientoDto) {}
|
||||
28
src/modules/zona/dto/zona.dto.ts
Normal file
28
src/modules/zona/dto/zona.dto.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { IsString, IsOptional, IsNotEmpty, IsUUID } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class ZonaAlmacenamientoDto {
|
||||
@ApiProperty({ example: 'uuid-id' })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
idZonaAlmacenamiento: string;
|
||||
|
||||
@ApiProperty({ example: 'uuid-sucursal' })
|
||||
@IsString()
|
||||
idSucursal: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'AM' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
prefijo?: string;
|
||||
|
||||
@ApiProperty({ example: 'Almacén' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
nombre: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '#FF5722' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
color?: string;
|
||||
}
|
||||
19
src/modules/zona/entities/zona.entity.ts
Normal file
19
src/modules/zona/entities/zona.entity.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class ZonaAlmacenamiento {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
idZonaAlmacenamiento: string;
|
||||
|
||||
@Column()
|
||||
idSucursal: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
prefijo: string;
|
||||
|
||||
@Column()
|
||||
nombre: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
color: string;
|
||||
}
|
||||
132
src/modules/zona/zona.controller.ts
Normal file
132
src/modules/zona/zona.controller.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import { Controller, Get, Post, Put, Delete, Body, Param, HttpStatus } from '@nestjs/common';
|
||||
import { ZonaService } from './zona.service.js';
|
||||
import { ZonaAlmacenamientoDto } from './dto/zona.dto.js';
|
||||
import { UpdateZonaAlmacenamientoDto } from './dto/update-zona.dto.js';
|
||||
import { ApiResult, defaultApiResult } from '../../shared/interfaces/api.result.js';
|
||||
import { routeZona } from './zona.router.js';
|
||||
|
||||
@Controller('zona')
|
||||
export class ZonaController {
|
||||
constructor(private readonly zonaService: ZonaService) {}
|
||||
|
||||
@Get()
|
||||
async findAll(): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeZona.find.title,
|
||||
routeZona.find.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.zonaService.findAll();
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = `${result.length} Zona(s) encontrado(s).`;
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = result.length;
|
||||
apiResult.data = result;
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Get(':idZonaAlmacenamiento')
|
||||
async findOne(@Param('idZonaAlmacenamiento') idZonaAlmacenamiento: string): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeZona.one.title,
|
||||
routeZona.one.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.zonaService.findOne(idZonaAlmacenamiento);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Existe un Zona.';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [result];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Body() zonaAlmacenamientoDto: ZonaAlmacenamientoDto): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeZona.create.title,
|
||||
routeZona.create.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.zonaService.create(zonaAlmacenamientoDto);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Zona se ha agregado correctamente.';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [result];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Put(':idZonaAlmacenamiento')
|
||||
async update(
|
||||
@Param('idZonaAlmacenamiento') idZonaAlmacenamiento: string,
|
||||
@Body() updateZonaAlmacenamientoDto: UpdateZonaAlmacenamientoDto,
|
||||
): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeZona.update.title,
|
||||
routeZona.update.route,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.zonaService.update(idZonaAlmacenamiento, updateZonaAlmacenamientoDto);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Se ha actualizado correctamente.';
|
||||
apiResult.boolean = true;
|
||||
apiResult.rows = 1;
|
||||
apiResult.data = [result];
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
@Delete(':idZonaAlmacenamiento')
|
||||
async remove(@Param('idZonaAlmacenamiento') idZonaAlmacenamiento: string): Promise<ApiResult> {
|
||||
let apiResult = defaultApiResult(
|
||||
routeZona.remove.title,
|
||||
routeZona.remove.route,
|
||||
);
|
||||
|
||||
try {
|
||||
await this.zonaService.remove(idZonaAlmacenamiento);
|
||||
|
||||
apiResult.status = 'correct';
|
||||
apiResult.code = HttpStatus.OK;
|
||||
apiResult.message = 'Se ha eliminado correctamente.';
|
||||
apiResult.boolean = true;
|
||||
} catch (error: any) {
|
||||
apiResult.code = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
apiResult.message = error.message;
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
}
|
||||
13
src/modules/zona/zona.module.ts
Normal file
13
src/modules/zona/zona.module.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ZonaController } from './zona.controller.js';
|
||||
import { ZonaService } from './zona.service.js';
|
||||
import { ZonaAlmacenamiento } from './entities/zona.entity.js';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ZonaAlmacenamiento])],
|
||||
controllers: [ZonaController],
|
||||
providers: [ZonaService],
|
||||
exports: [ZonaService],
|
||||
})
|
||||
export class ZonaModule {}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue