commit f75db0e0d1789841f04702518aa32cfd9ef60fff Author: DillusDev001 Date: Fri Sep 18 20:22:24 2026 -0400 First Commit diff --git a/.env b/.env new file mode 100644 index 0000000..aee017c --- /dev/null +++ b/.env @@ -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 \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f0814d3 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc4052f --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..e60ae71 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +ignore-scripts=false diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a20502b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..5eb95fd --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +

+ Nest Logo +

+ +[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 +[circleci-url]: https://circleci.com/gh/nestjs/nest + +

A progressive Node.js framework for building efficient and scalable server-side applications.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Discord +Backers on Open Collective +Sponsors on Open Collective + Donate us + Support us + Follow us on Twitter +

+ + +## 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). diff --git a/assets/templates/qr-template.png b/assets/templates/qr-template.png new file mode 100644 index 0000000..406fea1 Binary files /dev/null and b/assets/templates/qr-template.png differ diff --git a/http/auth.http b/http/auth.http new file mode 100644 index 0000000..6a3d4b1 --- /dev/null +++ b/http/auth.http @@ -0,0 +1,11 @@ +@baseUrl = http://localhost:3000/v1 +@contentType = application/json + +### Login +POST {{baseUrl}}/auth/login +Content-Type: {{contentType}} + +{ + "usuario": "admin", + "password": "admin123" +} diff --git a/http/environment.http b/http/environment.http new file mode 100644 index 0000000..0a5bcbd --- /dev/null +++ b/http/environment.http @@ -0,0 +1,4 @@ +### Variables de entorno +@baseUrl = http://localhost:3000/v1 +@contentType = application/json +@apiKey = 3C03C2A5-F739-4353-9B7E-038A2113285A diff --git a/http/usuario.http b/http/usuario.http new file mode 100644 index 0000000..dec137a --- /dev/null +++ b/http/usuario.http @@ -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}} diff --git a/nest-cli.json b/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/oxlint.json b/oxlint.json new file mode 100644 index 0000000..07ec3bd --- /dev/null +++ b/oxlint.json @@ -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 + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0118b99 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..64f88fe --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5398 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@nestjs/common': + specifier: ^12.0.1 + version: 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + '@nestjs/config': + specifier: ^12.0.0 + version: 12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(rxjs@7.8.2) + '@nestjs/core': + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)) + '@nestjs/passport': + specifier: ^12.0.0 + version: 12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(passport@0.7.0) + '@nestjs/platform-express': + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.1)(supports-color@7.2.0) + '@nestjs/serve-static': + specifier: ^12.0.0 + version: 12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.1)(express@5.2.1(supports-color@7.2.0)) + '@nestjs/swagger': + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.1)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) + '@nestjs/typeorm': + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@1.1.0(pg@8.23.0)(supports-color@7.2.0)) + bcrypt: + specifier: ^6.0.0 + version: 6.0.0 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.15.1 + version: 0.15.1 + dotenv: + specifier: ^17.4.2 + version: 17.4.2 + multer: + specifier: ^2.3.0 + version: 2.3.0 + openai: + specifier: ^4.0.0 + version: 4.104.0 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + pg: + specifier: ^8.23.0 + version: 8.23.0 + qrcode: + specifier: ^1.5.4 + version: 1.5.4 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + sharp: + specifier: ^0.33.0 + version: 0.33.5 + tesseract.js: + specifier: ^5.1.0 + version: 5.1.1 + typeorm: + specifier: ^1.1.0 + version: 1.1.0(pg@8.23.0)(supports-color@7.2.0) + typeorm-extension: + specifier: ^4.1.0 + version: 4.1.0(typeorm@1.1.0(pg@8.23.0)(supports-color@7.2.0)) + uuid: + specifier: ^14.0.2 + version: 14.0.2 + devDependencies: + '@nestjs/cli': + specifier: ^12.0.0 + version: 12.0.0(@types/node@24.13.3)(prettier@3.9.6) + '@nestjs/mau': + specifier: ^0.2.6 + version: 0.2.6 + '@nestjs/schematics': + specifier: ^12.0.0 + version: 12.0.0(chokidar@5.0.0)(prettier@3.9.6)(typescript@6.0.3) + '@nestjs/testing': + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.1)(@nestjs/platform-express@12.0.1) + '@types/bcrypt': + specifier: ^6.0.0 + version: 6.0.0 + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/multer': + specifier: ^2.2.0 + version: 2.2.0 + '@types/node': + specifier: ^24.0.0 + version: 24.13.3 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 + '@types/pg': + specifier: ^8.23.1 + version: 8.23.1 + '@types/qrcode': + specifier: ^1.5.5 + version: 1.5.6 + '@types/supertest': + specifier: ^7.0.0 + version: 7.2.1 + '@vitest/coverage-v8': + specifier: ^4.1.2 + version: 4.1.11(vitest@4.1.11) + oxlint: + specifier: ^1.58.0 + version: 1.80.0 + prettier: + specifier: ^3.4.2 + version: 3.9.6 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + supertest: + specifier: ^7.0.0 + version: 7.2.2(supports-color@7.2.0) + tsx: + specifier: ^4.23.13 + version: 4.23.13 + typescript: + specifier: ^6.0.2 + version: 6.0.3 + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(supports-color@7.2.0)(typescript@6.0.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) + vitest: + specifier: ^4.1.2 + version: 4.1.11(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) + +packages: + + '@angular-devkit/core@22.1.5': + resolution: {integrity: sha512-HiY6d5dkIdJs5grP9OHvgkf14QOcDIo+hbuT7YKuLItQ++ZxCwUabJMLCCJ5R0KrstE5NqED0dNcyk5WD01t5Q==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^5.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/schematics-cli@22.1.5': + resolution: {integrity: sha512-VB/5fH2b/2j8eeMm25vRYCJzBbpaM/VyHc8/+WqPzJahdWuvL0CeLWrxjHHT1exQ/kgbqHLaIWMgSywZe3EBhA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true + + '@angular-devkit/schematics@22.1.5': + resolution: {integrity: sha512-HTmo9y8wjXKtJGVTYomTjZsjWzhirpu1pPRAzsVob3eiYOuxv1qDRAgWiHXNGp5CpnbHunZweXY/WffNGOFRyA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@ebec/core@1.3.2': + resolution: {integrity: sha512-YuQ5mk6jhkA+8LSh9e0vdJ2Nez00tZqiIGe5HXv4qo0dIncvSoeovr7IM8md6dDNt1ODrwHqof4YzzJqFZLXmw==} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/checkbox@5.2.3': + resolution: {integrity: sha512-XEYX2WA8SBkLPczL6/yXPHLPCvDoptmh9v56Cy05BSV1Smk1vWy19bTC4qJBuIffw7+6l4CcaYYzGqG60RfW1g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@6.3.0': + resolution: {integrity: sha512-pZHXJImFtERmSNMBHcjwuz8Ck5vEFEYNUZnwbb8aJpjHv/TwGuFErNxF2Hp8+V+pNJs2EYPMlyWscvFEqO9jOQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@12.0.1': + resolution: {integrity: sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@5.3.1': + resolution: {integrity: sha512-y43COoyVUjPWIobn2Qep/uI1drPS78aaZZZ9kVi94Tyu/GuW2N8d8Q4rifJXGAXCEAXCPTTMjD8gC1HyvM5ukA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@5.1.3': + resolution: {integrity: sha512-3NQJiXNJ/aj9wiAsr7pECdp5Qe9J0X9YUJCKsaFXS+ddOxfL6J4AIl3w3T4Gq3kK0WsQY5GMoDokK5X94m6lHw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@3.0.4': + resolution: {integrity: sha512-tZbbaK2ovq6vlrRBNQvjrypmrED/p5x2ncIHQ79cD55tei3dD96v5glMMA+6tiq7K104i/25DVYKWVPJuV6ptA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.8': + resolution: {integrity: sha512-tApbon79GM9ry56ja/Ud3SY2CL4TQsao9fIwDQbgTeNY55025GdMzQ2+UdegV/lx51VNGUB59M0v0nMpybYY4Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/input@5.1.4': + resolution: {integrity: sha512-3xQkQrOvgOzpSN2ciTVdRDlg1FWMCA8l+0KfB6SNlILoTCGzJTzO/gc0Rwjcb3usuGyKdaGtI6OiyMdeMeLWkg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@4.2.1': + resolution: {integrity: sha512-5KaqwZNLRpUuWcoCrYghPP9TMaXL5v2Sk4xqePM7RCVegcJStoXdWibio60YIC1bec+z1fCyb67N6XPJIkZtGA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@5.2.0': + resolution: {integrity: sha512-CvVcW09emkBESEOW+4R8CjLNkP3fB3XrjeL8CDvfpjgrJN+V9oerXmJAXXM3l+4xqYPD5Yaujzy/Ph0PLOdDuA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@8.5.2': + resolution: {integrity: sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@8.7.0': + resolution: {integrity: sha512-yQwBMYvpJ6jqrXtKiOwRD5XezjJoyt3VQvIyjsr5Arqb519nfIohOQymWVJ8/vEgg8xtZerrCsqlSaqt/LPC9A==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@5.3.3': + resolution: {integrity: sha512-Mu7WrtmDLaXBDEyrRLS70SZgX9ZSm4Up1w0ZxiH8C1OOp9oaVCn2k8q3QGgmlnhsKYUhuaU3zFWhAP6wxkVIMA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@4.3.1': + resolution: {integrity: sha512-0VWOvsHWI0rPj6CG70MoP4oXNCB6adcyN8bVFZXnh11eLDdPIK2f2XCmva78acPPDfJisfab7qNakwBh7hdBXw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@5.2.3': + resolution: {integrity: sha512-KuRTodDa6xBXX2noIpjuitpX/QT7Sfav7dIZ/OfUY54Hxg95nrGoshSzxx6Ey7qqLbImKdiGkSDt7KjPXjQgmA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@4.1.0': + resolution: {integrity: sha512-FMiJpuHUG3Dk0ex+UIXkre7i+i4OcwHWk9YdcVtZHFwb/r2rnrU2ipTCNAB7A+QOP0ryzIcqOfy76fRyyvOEAw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + + '@nestjs/cli@12.0.0': + resolution: {integrity: sha512-DxgGUKA4gj92lU2lF23Mlvm6y7AQBKEBuanNKNcmHeP6Cn3yqPZxN0LqyaJys25JCqO1bN8KgE3LICrvYlB93g==} + engines: {node: '>= 20.11'} + hasBin: true + peerDependencies: + '@rspack/core': ^1.7.7 || ^2.1.10 + '@swc/cli': ^0.8.0 + '@swc/core': ^1.15.18 + fork-ts-checker-webpack-plugin: ^9.1.0 + ts-loader: ^9.5.4 + tsconfig-paths-webpack-plugin: ^4.2.0 + webpack: ^5.105.4 + webpack-node-externals: ^3.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + '@swc/cli': + optional: true + '@swc/core': + optional: true + fork-ts-checker-webpack-plugin: + optional: true + ts-loader: + optional: true + tsconfig-paths-webpack-plugin: + optional: true + webpack: + optional: true + webpack-node-externals: + optional: true + + '@nestjs/common@12.0.1': + resolution: {integrity: sha512-v0zTaRCTV2K2xSnb3GnJoQKtaR6VxouJtm+P2gpr5w61dlXeWpXl1WVknCSzUqx2QwTV10tBkHETxvQvkf2Exg==} + peerDependencies: + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/config@12.0.0': + resolution: {integrity: sha512-pG5+fFfCo3cdgVkLJLvvhUGfxEjwzv9hIAQ1aJVHvHlHVniDWfwX5pR/utgoAkJzSikv2oTR1BmIF03FXdi42Q==} + peerDependencies: + '@nestjs/common': ^11.0.0 || ^12.0.0 + rxjs: ^7.1.0 + + '@nestjs/core@12.0.1': + resolution: {integrity: sha512-rU6tAi8vDdyzHgN0iW0J4UJvziroOqzHqzlqL7phOSPizaXVEePfv+OUOsdJEOh0Qd14mslc3udOheLrAEcZow==} + engines: {node: '>= 20'} + peerDependencies: + '@nestjs/common': ^12.0.0 + '@nestjs/microservices': ^12.0.0 + '@nestjs/platform-express': ^12.0.0 + '@nestjs/websockets': ^12.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + + '@nestjs/jwt@12.0.1': + resolution: {integrity: sha512-AOvjo0JIuVMNr+6JgbEz4fP+4afclGR5ffZnHtjOFs4+HhzerUYKcIVUL06u5uQYQi/ra8WHO+NZR7OFdQeGvA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 + + '@nestjs/mapped-types@12.0.0': + resolution: {integrity: sha512-aX7lxZcqXldtnSr0bgT2ZPQvpTpIwBMW1GMX29o4s3q02vmq6JoTYuPW0GwYXa8BiFMRX7WbGZVDA703pNqOMQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 || ^12.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/mau@0.2.6': + resolution: {integrity: sha512-fp+zZbvKka7ntnT+97jhxYk6JzUwTd/LrI4TRW1Ncczb7ZUzNScmZBJcbifOow4PwWpE+GP4pr8s8yn2J/8tzw==} + engines: {node: '>= 20.0.0'} + hasBin: true + + '@nestjs/passport@12.0.0': + resolution: {integrity: sha512-FM9ioo2Dyj+cC7aeO4g19ijAXoRaHlW/GQZFbgXsM4Z9pyQedA8yOseb7ivfqlPwUKJlH9PFgeWniu6c0vb6dw==} + peerDependencies: + '@nestjs/common': ^11.0.0 || ^12.0.0 + passport: ^0.5.0 || ^0.6.0 || ^0.7.0 + + '@nestjs/platform-express@12.0.1': + resolution: {integrity: sha512-nF8bRgROptMyixYBfiJUAsjj2o9k13zxAjUKYaqHTPk8mTSd+gk7hXz6NykXNrfHWmMEYC9RttQlMUdMHNTbuA==} + peerDependencies: + '@nestjs/common': ^12.0.0 + '@nestjs/core': ^12.0.0 + + '@nestjs/schematics@12.0.0': + resolution: {integrity: sha512-ilsxam7i+SWd2psr12faLVSk86FYKnxRUGO6jAGNi1P9+O0kRaYVOScyrKeIj97W9T9UJoJF38fg+7KgjY/nOg==} + engines: {node: '>=22.12.0'} + peerDependencies: + prettier: ^3.0.0 + typescript: '>=6.0.0' + peerDependenciesMeta: + prettier: + optional: true + + '@nestjs/serve-static@12.0.0': + resolution: {integrity: sha512-3YsDpylQN90o5r7rIN2M5LQ3uK8WUJ8oSo0wH0SRrBb7X/FKD31vNY4J7cOlbRDZiX1kxDWyI8wSlygqj/57Yg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23.0.0} + peerDependencies: + '@fastify/static': ^10.0.0 + '@nestjs/common': ^12.0.0 + '@nestjs/core': ^12.0.0 + express: ^5.0.1 + fastify: ^5.2.1 + peerDependenciesMeta: + '@fastify/static': + optional: true + express: + optional: true + fastify: + optional: true + + '@nestjs/swagger@12.0.1': + resolution: {integrity: sha512-fQ0WyutSSHedO9HzSkOFKFaRygYNGBno5EggcZljs+KL8tHzPPTO5oyJpaf3BoOKFGGsN/0B13EbIpH2pkPx8g==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@fastify/static': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/common': ^12.0.0 + '@nestjs/core': ^12.0.0 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/testing@12.0.1': + resolution: {integrity: sha512-prf3fsUaoKS6t04XcQt5aSTp68Po1GxUcbEBPv+3iqAef07hKQEO15r0jmmwA+rWZfONYT1UcV3YIylR++/tCg==} + peerDependencies: + '@nestjs/common': ^12.0.0 + '@nestjs/core': ^12.0.0 + '@nestjs/microservices': ^12.0.0 + '@nestjs/platform-express': ^12.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + + '@nestjs/typeorm@12.0.1': + resolution: {integrity: sha512-7dsdaD/PSiwCAAhv78+ScS3e6lW41BOMZ8LB7/v5p/hovSUc1wUH1DBXhLgqYx0ChXYLzDybBIADN6lvBhiM6w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 || ^12.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 || ^12.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + rxjs: ^7.2.0 + typeorm: ^0.3.0 || ^1.0.0-dev + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxc-project/types@0.147.0': + resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==} + + '@oxlint/binding-android-arm-eabi@1.80.0': + resolution: {integrity: sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.80.0': + resolution: {integrity: sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.80.0': + resolution: {integrity: sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.80.0': + resolution: {integrity: sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.80.0': + resolution: {integrity: sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.80.0': + resolution: {integrity: sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.80.0': + resolution: {integrity: sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.80.0': + resolution: {integrity: sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.80.0': + resolution: {integrity: sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.80.0': + resolution: {integrity: sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.80.0': + resolution: {integrity: sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.80.0': + resolution: {integrity: sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.80.0': + resolution: {integrity: sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.80.0': + resolution: {integrity: sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.80.0': + resolution: {integrity: sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.80.0': + resolution: {integrity: sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.80.0': + resolution: {integrity: sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.80.0': + resolution: {integrity: sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.80.0': + resolution: {integrity: sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@paralleldrive/cuid2@2.3.1': + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + + '@rolldown/binding-android-arm-eabi@1.2.6': + resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.6': + resolution: {integrity: sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.6': + resolution: {integrity: sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.6': + resolution: {integrity: sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.6': + resolution: {integrity: sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': + resolution: {integrity: sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.6': + resolution: {integrity: sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.6': + resolution: {integrity: sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.6': + resolution: {integrity: sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.6': + resolution: {integrity: sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.6': + resolution: {integrity: sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.6': + resolution: {integrity: sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.6': + resolution: {integrity: sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.6': + resolution: {integrity: sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.6': + resolution: {integrity: sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@sqltools/formatter@1.2.5': + resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@types/bcrypt@6.0.0': + resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/express-serve-static-core@5.1.3': + resolution: {integrity: sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/multer@2.2.0': + resolution: {integrity: sha512-3U1troeqGV8Ntp7Q3klwf4zr23VEoqYVocYXaswm9+8z3O9UHDYAqLxjJ/h550iRADTjKdOdhhasXw6gD6kYtg==} + + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + + '@types/passport-jwt@4.0.1': + resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==} + + '@types/passport-strategy@0.2.38': + resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==} + + '@types/passport@1.0.17': + resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} + + '@types/pg@8.23.1': + resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==} + + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/superagent@8.1.11': + resolution: {integrity: sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==} + + '@types/supertest@7.2.1': + resolution: {integrity: sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==} + + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + + '@vitest/coverage-v8@4.1.11': + resolution: {integrity: sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==} + peerDependencies: + '@vitest/browser': 4.1.11 + vitest: 4.1.11 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} + engines: {node: '>=14'} + + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@1.0.5: + resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bcrypt@6.0.0: + resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} + engines: {node: '>= 18'} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bmp-js@0.1.0: + resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==} + + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} + + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + class-validator@0.15.1: + resolution: {integrity: sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-progress@3.12.0: + resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} + engines: {node: '>=4'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-spinners@3.4.0: + resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} + engines: {node: '>=18.20'} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + comment-json@5.0.0: + resolution: {integrity: sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==} + engines: {node: '>= 6'} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} + engines: {node: '>=18'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + dayjs@1.11.23: + resolution: {integrity: sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dotenv-expand@13.0.0: + resolution: {integrity: sha512-aBfBS8eYIeXmpHI9ThIlA7/WLq+SLt18iXUZhb52rW89QLKQFoIpPG1bPeewoPZsTyjSSO3T7234FBVUM1V2rA==} + engines: {node: '>=12'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + envix@1.5.0: + resolution: {integrity: sha512-IOxTKT+tffjxgvX2O5nq6enbkv6kBQ/QdMy18bZWo0P0rKPvsRp2/EypIPwTvJfnmk3VdOlq/KcRSZCswefM/w==} + engines: {node: '>=18.0.0'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-toolkit@1.51.0: + resolution: {integrity: sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-uri@3.1.6: + resolution: {integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + file-type@22.0.2: + resolution: {integrity: sha512-0H8TsCUGBLx+V5adH3EY52hTAcyLKbV1D4gq5cIOJ6DnQAHeV9Z2Hhuc5CoBX4YmvB2oL+JIC84z0qO7JsCoNw==} + engines: {node: '>=22'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + flat@6.0.1: + resolution: {integrity: sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw==} + engines: {node: '>=18'} + hasBin: true + + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + idb-keyval@6.3.0: + resolution: {integrity: sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + inquirer@8.2.6: + resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} + engines: {node: '>=12.0.0'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + + is-electron@2.2.2: + resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-url@1.2.4: + resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-yaml@5.4.1: + resolution: {integrity: sha512-28R/k+NAjeuf7+CKlTxWZVExJGwVVLwY06DgEnOMz2gEpfNkDcD7QvyiVPT0xy0XXhU8vHsd4Ot42OOPdJG7dQ==} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + libphonenumber-js@1.13.12: + resolution: {integrity: sha512-uLVeV1c9OTk6qkdqnj+mpMD+ZdnZ0szVyWu58HwMmpwkHA1gCEkyjd3veZQXDnuw9KEwSRjcc9B1pS9XKIN1fA==} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + load-esm@1.0.3: + resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} + engines: {node: '>=13.2.0'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locter@4.1.1: + resolution: {integrity: sha512-2nbVtJIBUh72ViYSeyLI03PmTdheXiwH9e3mMO4wt10EUXeoKap3CdmcJOMkXIHTbfxzUoALLX+wRme/AFUxuw==} + engines: {node: '>=22.0.0'} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magic-string@1.0.0: + resolution: {integrity: sha512-CGvjzMN08iv6w1mm4/x3Gh1hLb4VnyRUA15FFpl6CsCIGGoe36k7kY5KNz9QDbSBN5I/fWHM6ZlIkUTa5xdUEA==} + + magicast@0.5.4: + resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multer@2.2.0: + resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} + engines: {node: '>= 10.16.0'} + + multer@2.3.0: + resolution: {integrity: sha512-cjNbm3sttszgZeGfJR124D+jFEfkXCVAsoPBmFn9X7UxmDSFHWqE2CoEj0vrmSpuAFnqWR1Szcm9QTsiHr60Xw==} + engines: {node: '>= 10.16.0'} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@1.1.0: + resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} + engines: {node: '>=18'} + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-addon-api@8.9.2: + resolution: {integrity: sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==} + engines: {node: ^18 || ^20 || >= 21} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-emoji@2.1.3: + resolution: {integrity: sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==} + engines: {node: '>=18'} + + node-emoji@2.2.0: + resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} + engines: {node: '>=18'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + openai@4.104.0: + resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + opencollective-postinstall@2.0.3: + resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==} + hasBin: true + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + ora@9.4.1: + resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==} + engines: {node: '>=20'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + oxlint@1.80.0: + resolution: {integrity: sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=7.0.2001' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + passport-jwt@4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + + passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + + passport@0.7.0: + resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==} + engines: {node: '>= 0.4.0'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pause@0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.16.0: + resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.23.0: + resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rolldown@1.2.6: + resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + + skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + + smob@1.6.2: + resolution: {integrity: sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==} + engines: {node: '>=20.0.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sql-highlight@6.1.0: + resolution: {integrity: sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==} + engines: {node: '>=14'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + stdin-discarder@0.3.2: + resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} + engines: {node: '>=18'} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.2: + resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} + engines: {node: '>=20'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} + engines: {node: '>=18'} + + superagent@10.3.0: + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} + engines: {node: '>=14.18.0'} + + supertest@7.2.2: + resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} + engines: {node: '>=14.18.0'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + swagger-ui-dist@5.32.14: + resolution: {integrity: sha512-nOA2pSQhcmODMUQZpJHYKNuwniDUqcOWGNaSCOoZv12FdOSJ9JxV95HtyRGNMqEBj6h6lCNTy20TgZDYTSuUIg==} + + tesseract.js-core@5.1.1: + resolution: {integrity: sha512-KX3bYSU5iGcO1XJa+QGPbi+Zjo2qq6eBhNjSGR5E5q0JtzkoipJKOUQD7ph8kFyteCEfEQ0maWLu8MCXtvX5uQ==} + + tesseract.js@5.1.1: + resolution: {integrity: sha512-lzVl/Ar3P3zhpUT31NjqeCo1f+D5+YfpZ5J62eo2S14QNVOmHBTtbchHm/YAbOOOzCegFnKf4B3Qih9LuldcYQ==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + deprecated: unmaintained + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.23.13: + resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} + engines: {node: '>=18.0.0'} + hasBin: true + + twinop@1.0.0: + resolution: {integrity: sha512-CAq8nn9jgOnKqeh0iSSg0sz7YMv1+T7FjKsSh0+wwQfGs2+pnUsLl5TEbXkTyndlvbhWbxTQ9YhxMMy2YxJOWQ==} + engines: {node: '>=22.0.0'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typeorm-extension@4.1.0: + resolution: {integrity: sha512-rYw4fuuzOgjQvUgrRJompI85CygrzzBfcOiWrBxUIAtm2rjsidWHLiYaXSDgckyEBQw45TKJAAcENeY3blUmAQ==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + typeorm: ^1.1.0 + + typeorm@1.1.0: + resolution: {integrity: sha512-iX/kvsV42/htCNAQUyElGW87E4Z12neZc6YUFBTEu0BnLiREYDNT5Wfw3wRqZw9vyOEC36zUBAY6Qvywoig06Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.11.0} + hasBin: true + peerDependencies: + '@google-cloud/spanner': ^8.0.0 + '@sap/hana-client': ^2.14.22 + better-sqlite3: ^12.0.0 + ioredis: ^5.0.4 + mongodb: ^7.0.0 + mssql: ^12.0.0 + mysql2: ^3.15.3 + oracledb: ^6.3.0 || ^7.0.0 + pg: ^8.5.1 + pg-native: ^3.0.0 + pg-query-stream: ^4.0.0 + redis: ^5.0.0 || ^6.0.0 + sql.js: ^1.4.0 + ts-node: ^10.9.2 + typeorm-aurora-data-api-driver: ^3.0.0 + peerDependenciesMeta: + '@google-cloud/spanner': + optional: true + '@sap/hana-client': + optional: true + better-sqlite3: + optional: true + ioredis: + optional: true + mongodb: + optional: true + mssql: + optional: true + mysql2: + optional: true + oracledb: + optional: true + pg: + optional: true + pg-native: + optional: true + pg-query-stream: + optional: true + redis: + optional: true + sql.js: + optional: true + ts-node: + optional: true + typeorm-aurora-data-api-driver: + optional: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + undici@6.20.1: + resolution: {integrity: sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==} + engines: {node: '>=18.17'} + + unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + validator@13.15.35: + resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==} + engines: {node: '>= 0.10'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + wasm-feature-detect@1.9.0: + resolution: {integrity: sha512-zonE+xlIIYtxPy++L24ow0hAD8CICb4+FgPyROd3buyXIqsJvUEDkBgfCCoXOd1Hu3DUr0GOfnPIdcGV+YpNaA==} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@18.1.0: + resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yoctocolors@2.2.0: + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} + engines: {node: '>=18'} + + zlibjs@0.3.1: + resolution: {integrity: sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==} + +snapshots: + + '@angular-devkit/core@22.1.5(chokidar@5.0.0)': + dependencies: + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.5 + rxjs: 7.8.2 + source-map: 0.7.6 + optionalDependencies: + chokidar: 5.0.0 + + '@angular-devkit/schematics-cli@22.1.5(@types/node@24.13.3)(chokidar@5.0.0)': + dependencies: + '@angular-devkit/core': 22.1.5(chokidar@5.0.0) + '@angular-devkit/schematics': 22.1.5(chokidar@5.0.0) + '@inquirer/prompts': 8.5.2(@types/node@24.13.3) + transitivePeerDependencies: + - '@types/node' + - chokidar + + '@angular-devkit/schematics@22.1.5(chokidar@5.0.0)': + dependencies: + '@angular-devkit/core': 22.1.5(chokidar@5.0.0) + jsonc-parser: 3.3.1 + magic-string: 1.0.0 + ora: 9.4.1 + rxjs: 7.8.2 + transitivePeerDependencies: + - chokidar + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + + '@borewit/text-codec@0.2.2': {} + + '@colors/colors@1.5.0': + optional: true + + '@ebec/core@1.3.2': {} + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + + '@inquirer/ansi@2.0.7': {} + + '@inquirer/checkbox@5.2.3(@types/node@24.13.3)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 12.0.1(@types/node@24.13.3) + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/confirm@6.3.0(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@24.13.3) + '@inquirer/type': 4.1.0(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/core@12.0.1(@types/node@24.13.3)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@24.13.3) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/editor@5.3.1(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@24.13.3) + '@inquirer/external-editor': 3.0.4(@types/node@24.13.3) + '@inquirer/type': 4.1.0(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/expand@5.1.3(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@24.13.3) + '@inquirer/type': 4.1.0(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/external-editor@3.0.4(@types/node@24.13.3)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.3 + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/figures@2.0.8': {} + + '@inquirer/input@5.1.4(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@24.13.3) + '@inquirer/type': 4.1.0(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/number@4.2.1(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@24.13.3) + '@inquirer/type': 4.1.0(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/password@5.2.0(@types/node@24.13.3)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 12.0.1(@types/node@24.13.3) + '@inquirer/type': 4.1.0(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/prompts@8.5.2(@types/node@24.13.3)': + dependencies: + '@inquirer/checkbox': 5.2.3(@types/node@24.13.3) + '@inquirer/confirm': 6.3.0(@types/node@24.13.3) + '@inquirer/editor': 5.3.1(@types/node@24.13.3) + '@inquirer/expand': 5.1.3(@types/node@24.13.3) + '@inquirer/input': 5.1.4(@types/node@24.13.3) + '@inquirer/number': 4.2.1(@types/node@24.13.3) + '@inquirer/password': 5.2.0(@types/node@24.13.3) + '@inquirer/rawlist': 5.3.3(@types/node@24.13.3) + '@inquirer/search': 4.3.1(@types/node@24.13.3) + '@inquirer/select': 5.2.3(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/prompts@8.7.0(@types/node@24.13.3)': + dependencies: + '@inquirer/checkbox': 5.2.3(@types/node@24.13.3) + '@inquirer/confirm': 6.3.0(@types/node@24.13.3) + '@inquirer/editor': 5.3.1(@types/node@24.13.3) + '@inquirer/expand': 5.1.3(@types/node@24.13.3) + '@inquirer/input': 5.1.4(@types/node@24.13.3) + '@inquirer/number': 4.2.1(@types/node@24.13.3) + '@inquirer/password': 5.2.0(@types/node@24.13.3) + '@inquirer/rawlist': 5.3.3(@types/node@24.13.3) + '@inquirer/search': 4.3.1(@types/node@24.13.3) + '@inquirer/select': 5.2.3(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/rawlist@5.3.3(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@24.13.3) + '@inquirer/type': 4.1.0(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/search@4.3.1(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@24.13.3) + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/select@5.2.3(@types/node@24.13.3)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 12.0.1(@types/node@24.13.3) + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/type@4.1.0(@types/node@24.13.3)': + optionalDependencies: + '@types/node': 24.13.3 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@lukeed/csprng@1.1.0': {} + + '@microsoft/tsdoc@0.16.0': {} + + '@nestjs/cli@12.0.0(@types/node@24.13.3)(prettier@3.9.6)': + dependencies: + '@angular-devkit/core': 22.1.5(chokidar@5.0.0) + '@angular-devkit/schematics': 22.1.5(chokidar@5.0.0) + '@angular-devkit/schematics-cli': 22.1.5(@types/node@24.13.3)(chokidar@5.0.0) + '@inquirer/prompts': 8.7.0(@types/node@24.13.3) + '@nestjs/schematics': 12.0.0(chokidar@5.0.0)(prettier@3.9.6)(typescript@6.0.3) + ansis: 4.3.1 + chokidar: 5.0.0 + cli-table3: 0.6.5 + commander: 15.0.0 + minimatch: 10.2.6 + node-emoji: 2.2.0 + ora: 9.4.1 + tsconfig-paths: 4.2.0 + typescript: 6.0.3 + transitivePeerDependencies: + - '@types/node' + - prettier + + '@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)': + dependencies: + '@standard-schema/spec': 1.1.0 + file-type: 22.0.2(supports-color@7.2.0) + iterare: 1.2.1 + load-esm: 1.0.3 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.15.1 + transitivePeerDependencies: + - supports-color + + '@nestjs/config@12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + '@standard-schema/spec': 1.1.0 + dotenv: 17.4.2 + dotenv-expand: 13.0.0 + es-toolkit: 1.51.0 + rxjs: 7.8.2 + + '@nestjs/core@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + '@nestjs/platform-express': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.1)(supports-color@7.2.0) + + '@nestjs/jwt@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))': + dependencies: + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + '@types/jsonwebtoken': 9.0.10 + jsonwebtoken: 9.0.3 + + '@nestjs/mapped-types@12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.15.1 + + '@nestjs/mau@0.2.6': + dependencies: + chalk: 4.1.2 + cli-progress: 3.12.0 + commander: 4.1.1 + inquirer: 8.2.6 + node-emoji: 2.1.3 + ora: 5.4.1 + undici: 6.20.1 + + '@nestjs/passport@12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(passport@0.7.0)': + dependencies: + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + passport: 0.7.0 + + '@nestjs/platform-express@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.1)(supports-color@7.2.0)': + dependencies: + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + '@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.6 + express: 5.2.1(supports-color@7.2.0) + multer: 2.2.0 + path-to-regexp: 8.4.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@nestjs/schematics@12.0.0(chokidar@5.0.0)(prettier@3.9.6)(typescript@6.0.3)': + dependencies: + '@angular-devkit/core': 22.1.5(chokidar@5.0.0) + '@angular-devkit/schematics': 22.1.5(chokidar@5.0.0) + comment-json: 5.0.0 + jsonc-parser: 3.3.1 + pluralize: 8.0.0 + typescript: 6.0.3 + optionalDependencies: + prettier: 3.9.6 + transitivePeerDependencies: + - chokidar + + '@nestjs/serve-static@12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.1)(express@5.2.1(supports-color@7.2.0))': + dependencies: + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + '@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + path-to-regexp: 8.4.2 + optionalDependencies: + express: 5.2.1(supports-color@7.2.0) + + '@nestjs/swagger@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.1)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + '@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) + '@standard-schema/spec': 1.1.0 + es-toolkit: 1.51.0 + js-yaml: 5.4.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.32.14 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.15.1 + + '@nestjs/testing@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.1)(@nestjs/platform-express@12.0.1)': + dependencies: + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + '@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 + optionalDependencies: + '@nestjs/platform-express': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.1)(supports-color@7.2.0) + + '@nestjs/typeorm@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@1.1.0(pg@8.23.0)(supports-color@7.2.0))': + dependencies: + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + '@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + typeorm: 1.1.0(pg@8.23.0)(supports-color@7.2.0) + + '@noble/hashes@1.8.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.3 + + '@oxc-project/types@0.147.0': {} + + '@oxlint/binding-android-arm-eabi@1.80.0': + optional: true + + '@oxlint/binding-android-arm64@1.80.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.80.0': + optional: true + + '@oxlint/binding-darwin-x64@1.80.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.80.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.80.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.80.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.80.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.80.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.80.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.80.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.80.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.80.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.80.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.80.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.80.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.80.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.80.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.80.0': + optional: true + + '@paralleldrive/cuid2@2.3.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@rolldown/binding-android-arm-eabi@1.2.6': + optional: true + + '@rolldown/binding-android-arm64@1.2.6': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.6': + optional: true + + '@rolldown/binding-darwin-x64@1.2.6': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.6': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.6': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.6': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.6': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.6': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.6': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@scarf/scarf@1.4.0': {} + + '@sindresorhus/is@4.6.0': {} + + '@sqltools/formatter@1.2.5': {} + + '@standard-schema/spec@1.1.0': {} + + '@tokenizer/inflate@0.4.1(supports-color@7.2.0)': + dependencies: + debug: 4.4.3(supports-color@7.2.0) + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + + '@types/bcrypt@6.0.0': + dependencies: + '@types/node': 24.13.3 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 24.13.3 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 24.13.3 + + '@types/cookiejar@2.1.5': {} + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/express-serve-static-core@5.1.3': + dependencies: + '@types/node': 24.13.3 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.3 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 24.13.3 + + '@types/methods@1.1.4': {} + + '@types/ms@2.1.0': {} + + '@types/multer@2.2.0': + dependencies: + '@types/express': 5.0.6 + + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 24.13.3 + form-data: 4.0.6 + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@types/passport-jwt@4.0.1': + dependencies: + '@types/jsonwebtoken': 9.0.10 + '@types/passport-strategy': 0.2.38 + + '@types/passport-strategy@0.2.38': + dependencies: + '@types/express': 5.0.6 + '@types/passport': 1.0.17 + + '@types/passport@1.0.17': + dependencies: + '@types/express': 5.0.6 + + '@types/pg@8.23.1': + dependencies: + '@types/node': 24.13.3 + pg-protocol: 1.16.0 + pg-types: 2.2.0 + + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 24.13.3 + + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@1.2.1': + dependencies: + '@types/node': 24.13.3 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.13.3 + + '@types/superagent@8.1.11': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 24.13.3 + form-data: 4.0.6 + + '@types/supertest@7.2.1': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.11 + + '@types/validator@13.15.10': {} + + '@vitest/coverage-v8@4.1.11(vitest@4.1.11)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.11 + ast-v8-to-istanbul: 1.0.5 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.4 + obug: 2.1.4 + std-env: 4.2.0 + tinyrainbow: 3.1.1 + vitest: 4.1.11(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) + + '@vitest/expect@4.1.11': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) + + '@vitest/pretty-format@4.1.11': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.11': + dependencies: + '@vitest/utils': 4.1.11 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.11': {} + + '@vitest/utils@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.1.0 + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.3.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + ansis@4.3.1: {} + + append-field@1.0.0: {} + + argparse@2.0.1: {} + + array-timsort@1.0.3: {} + + asap@2.0.6: {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@1.0.5: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + asynckit@0.4.0: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + bcrypt@6.0.0: + dependencies: + node-addon-api: 8.9.2 + node-gyp-build: 4.8.4 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bmp-js@0.1.0: {} + + body-parser@2.3.0(supports-color@7.2.0): + dependencies: + bytes: 3.1.2 + content-type: 2.1.0 + debug: 4.4.3(supports-color@7.2.0) + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.16.0 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + camelcase@5.3.1: {} + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + char-regex@1.0.2: {} + + chardet@0.7.0: {} + + chardet@2.2.0: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + + citty@0.2.2: {} + + class-transformer@0.5.1: {} + + class-validator@0.15.1: + dependencies: + '@types/validator': 13.15.10 + libphonenumber-js: 1.13.12 + validator: 13.15.35 + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-progress@3.12.0: + dependencies: + string-width: 4.2.3 + + cli-spinners@2.9.2: {} + + cli-spinners@3.4.0: {} + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cli-width@3.0.0: {} + + cli-width@4.1.0: {} + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + clone@1.0.4: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@15.0.0: {} + + commander@4.1.1: {} + + comment-json@5.0.0: + dependencies: + array-timsort: 1.0.3 + esprima: 4.0.1 + + component-emitter@1.3.1: {} + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.1.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookiejar@2.1.4: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + dayjs@1.11.23: {} + + debug@4.4.3(supports-color@7.2.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 + + decamelize@1.2.0: {} + + dedent@1.7.2: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + + dijkstrajs@1.0.3: {} + + dotenv-expand@13.0.0: + dependencies: + dotenv: 17.4.2 + + dotenv@17.4.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + emojilib@2.4.0: {} + + encodeurl@2.0.0: {} + + envix@1.5.0: + dependencies: + std-env: 3.10.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.3.2: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-toolkit@1.51.0: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + esprima@4.0.1: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + expect-type@1.4.0: {} + + express@5.2.1(supports-color@7.2.0): + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0(supports-color@7.2.0) + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3(supports-color@7.2.0) + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1(supports-color@7.2.0) + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.16.0 + range-parser: 1.3.0 + router: 2.2.0(supports-color@7.2.0) + send: 1.2.1(supports-color@7.2.0) + serve-static: 2.2.1(supports-color@7.2.0) + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-safe-stringify@2.1.1: {} + + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-uri@3.1.6: {} + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + + fastq@1.20.3: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-type@22.0.2(supports-color@7.2.0): + dependencies: + '@tokenizer/inflate': 0.4.1(supports-color@7.2.0) + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1(supports-color@7.2.0): + dependencies: + debug: 4.4.3(supports-color@7.2.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + flat@6.0.1: {} + + form-data-encoder@1.7.2: {} + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.3.1 + dezalgo: 1.0.4 + once: 1.4.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globrex@0.1.2: {} + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + html-escaper@2.0.2: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + idb-keyval@6.3.0: {} + + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + inquirer@8.2.6: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.18.1 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.2 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 6.2.0 + + ipaddr.js@1.9.1: {} + + is-arrayish@0.3.4: {} + + is-electron@2.2.2: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-interactive@2.0.0: {} + + is-number@7.0.0: {} + + is-promise@4.0.0: {} + + is-unicode-supported@0.1.0: {} + + is-unicode-supported@2.1.0: {} + + is-url@1.2.4: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + iterare@1.2.1: {} + + jiti@2.7.0: {} + + js-tokens@10.0.0: {} + + js-yaml@5.4.1: + dependencies: + argparse: 2.0.1 + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.8.5 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + libphonenumber-js@1.13.12: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + load-esm@1.0.3: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locter@4.1.1: + dependencies: + '@ebec/core': 1.3.2 + destr: 2.0.5 + fast-glob: 3.3.3 + flat: 6.0.1 + jiti: 2.7.0 + twinop: 1.0.0 + yaml: 2.9.0 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + lodash@4.18.1: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.2.0 + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magic-string@1.0.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magicast@0.5.4: + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.5 + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + media-typer@1.1.1: {} + + merge-descriptors@2.0.0: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + mimic-function@5.0.1: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + multer@2.2.0: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + + multer@2.3.0: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + + mute-stream@0.0.8: {} + + mute-stream@3.0.0: {} + + nanoid@3.3.18: {} + + negotiator@1.1.0: + dependencies: + content-type: 2.1.0 + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + node-addon-api@8.9.2: {} + + node-domexception@1.0.0: {} + + node-emoji@2.1.3: + dependencies: + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + + node-emoji@2.2.0: + dependencies: + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build@4.8.4: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + obug@2.1.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + openai@4.104.0: + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + opencollective-postinstall@2.0.3: {} + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + ora@9.4.1: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 3.4.0 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 7.0.1 + stdin-discarder: 0.3.2 + string-width: 8.2.2 + + os-tmpdir@1.0.2: {} + + oxlint@1.80.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.80.0 + '@oxlint/binding-android-arm64': 1.80.0 + '@oxlint/binding-darwin-arm64': 1.80.0 + '@oxlint/binding-darwin-x64': 1.80.0 + '@oxlint/binding-freebsd-x64': 1.80.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.80.0 + '@oxlint/binding-linux-arm-musleabihf': 1.80.0 + '@oxlint/binding-linux-arm64-gnu': 1.80.0 + '@oxlint/binding-linux-arm64-musl': 1.80.0 + '@oxlint/binding-linux-ppc64-gnu': 1.80.0 + '@oxlint/binding-linux-riscv64-gnu': 1.80.0 + '@oxlint/binding-linux-riscv64-musl': 1.80.0 + '@oxlint/binding-linux-s390x-gnu': 1.80.0 + '@oxlint/binding-linux-x64-gnu': 1.80.0 + '@oxlint/binding-linux-x64-musl': 1.80.0 + '@oxlint/binding-openharmony-arm64': 1.80.0 + '@oxlint/binding-win32-arm64-msvc': 1.80.0 + '@oxlint/binding-win32-ia32-msvc': 1.80.0 + '@oxlint/binding-win32-x64-msvc': 1.80.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + parseurl@1.3.3: {} + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + passport-jwt@4.0.1: + dependencies: + jsonwebtoken: 9.0.3 + passport-strategy: 1.0.0 + + passport-strategy@1.0.0: {} + + passport@0.7.0: + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 + + path-exists@4.0.0: {} + + path-to-regexp@8.4.2: {} + + pathe@2.0.3: {} + + pause@0.0.1: {} + + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.23.0): + dependencies: + pg: 8.23.0 + + pg-protocol@1.16.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.23.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.23.0) + pg-protocol: 1.16.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.5: {} + + picomatch@4.0.7: {} + + pluralize@8.0.0: {} + + pngjs@5.0.0: {} + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + prettier@3.9.6: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + qs@6.16.0: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + queue-microtask@1.2.3: {} + + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@5.1.1: {} + + reflect-metadata@0.2.2: {} + + regenerator-runtime@0.13.11: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-main-filename@2.0.0: {} + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + reusify@1.1.0: {} + + rolldown@1.2.6: + dependencies: + '@oxc-project/types': 0.147.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.6 + '@rolldown/binding-android-arm64': 1.2.6 + '@rolldown/binding-darwin-arm64': 1.2.6 + '@rolldown/binding-darwin-x64': 1.2.6 + '@rolldown/binding-freebsd-x64': 1.2.6 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.6 + '@rolldown/binding-linux-arm64-gnu': 1.2.6 + '@rolldown/binding-linux-arm64-musl': 1.2.6 + '@rolldown/binding-linux-ppc64-gnu': 1.2.6 + '@rolldown/binding-linux-s390x-gnu': 1.2.6 + '@rolldown/binding-linux-x64-gnu': 1.2.6 + '@rolldown/binding-linux-x64-musl': 1.2.6 + '@rolldown/binding-openharmony-arm64': 1.2.6 + '@rolldown/binding-win32-arm64-msvc': 1.2.6 + '@rolldown/binding-win32-x64-msvc': 1.2.6 + + router@2.2.0(supports-color@7.2.0): + dependencies: + debug: 4.4.3(supports-color@7.2.0) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + run-async@2.4.1: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + semver@7.8.5: {} + + send@1.2.1(supports-color@7.2.0): + dependencies: + debug: 4.4.3(supports-color@7.2.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1(supports-color@7.2.0): + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: {} + + setprototypeof@1.2.0: {} + + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + + skin-tone@2.0.0: + dependencies: + unicode-emoji-modifier-base: 1.0.0 + + smob@1.6.2: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + split2@4.2.0: {} + + sql-highlight@6.1.0: {} + + stackback@0.0.2: {} + + statuses@2.0.2: {} + + std-env@3.10.0: {} + + std-env@4.2.0: {} + + stdin-discarder@0.3.2: {} + + streamsearch@1.1.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string-width@8.2.2: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.3.0 + + strip-bom@3.0.0: {} + + strtok3@10.3.5: + dependencies: + '@tokenizer/token': 0.3.0 + + superagent@10.3.0(supports-color@7.2.0): + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3(supports-color@7.2.0) + fast-safe-stringify: 2.1.1 + form-data: 4.0.6 + formidable: 3.5.4 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.16.0 + transitivePeerDependencies: + - supports-color + + supertest@7.2.2(supports-color@7.2.0): + dependencies: + cookie-signature: 1.2.2 + methods: 1.1.2 + superagent: 10.3.0(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + swagger-ui-dist@5.32.14: + dependencies: + '@scarf/scarf': 1.4.0 + + tesseract.js-core@5.1.1: {} + + tesseract.js@5.1.1: + dependencies: + bmp-js: 0.1.0 + idb-keyval: 6.3.0 + is-electron: 2.2.2 + is-url: 1.2.4 + node-fetch: 2.7.0 + opencollective-postinstall: 2.0.3 + regenerator-runtime: 0.13.11 + tesseract.js-core: 5.1.1 + wasm-feature-detect: 1.9.0 + zlibjs: 0.3.1 + transitivePeerDependencies: + - encoding + + through@2.3.8: {} + + tinybench@2.9.0: {} + + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.1.1: {} + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + tr46@0.0.3: {} + + tsconfck@3.1.6(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tsx@4.23.13: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + twinop@1.0.0: {} + + type-fest@0.21.3: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.1.0: + dependencies: + content-type: 2.1.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + + typedarray@0.0.6: {} + + typeorm-extension@4.1.0(typeorm@1.1.0(pg@8.23.0)(supports-color@7.2.0)): + dependencies: + citty: 0.2.2 + envix: 1.5.0 + locter: 4.1.1 + pascal-case: 3.1.2 + reflect-metadata: 0.2.2 + smob: 1.6.2 + typeorm: 1.1.0(pg@8.23.0)(supports-color@7.2.0) + + typeorm@1.1.0(pg@8.23.0)(supports-color@7.2.0): + dependencies: + '@sqltools/formatter': 1.2.5 + ansis: 4.3.1 + dayjs: 1.11.23 + debug: 4.4.3(supports-color@7.2.0) + dedent: 1.7.2 + reflect-metadata: 0.2.2 + sql-highlight: 6.1.0 + tinyglobby: 0.2.17 + tslib: 2.8.1 + yargs: 18.1.0 + optionalDependencies: + pg: 8.23.0 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + typescript@6.0.3: {} + + uid@2.0.2: + dependencies: + '@lukeed/csprng': 1.1.0 + + uint8array-extras@1.5.0: {} + + undici-types@5.26.5: {} + + undici-types@7.18.2: {} + + undici@6.20.1: {} + + unicode-emoji-modifier-base@1.0.0: {} + + unpipe@1.0.0: {} + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + uuid@14.0.2: {} + + validator@13.15.35: {} + + vary@1.1.2: {} + + vite-tsconfig-paths@5.1.4(supports-color@7.2.0)(typescript@6.0.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)): + dependencies: + debug: 4.4.3(supports-color@7.2.0) + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@6.0.3) + optionalDependencies: + vite: 8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) + transitivePeerDependencies: + - supports-color + - typescript + + vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.26 + rolldown: 1.2.6 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + esbuild: 0.28.2 + fsevents: 2.3.3 + jiti: 2.7.0 + tsx: 4.23.13 + yaml: 2.9.0 + + vitest@4.1.11(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + '@vitest/coverage-v8': 4.1.11(vitest@4.1.11) + transitivePeerDependencies: + - msw + + wasm-feature-detect@1.9.0: {} + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + web-streams-polyfill@4.0.0-beta.3: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-module@2.0.1: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + xtend@4.0.2: {} + + y18n@4.0.3: {} + + y18n@5.0.8: {} + + yaml@2.9.0: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@22.0.0: {} + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yargs@18.1.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 8.2.2 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yoctocolors@2.2.0: {} + + zlibjs@0.3.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..02995e8 --- /dev/null +++ b/pnpm-workspace.yaml @@ -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 diff --git a/spa.traineddata b/spa.traineddata new file mode 100644 index 0000000..d498473 Binary files /dev/null and b/spa.traineddata differ diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..80ceb3a --- /dev/null +++ b/src/app.module.ts @@ -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 { } diff --git a/src/database/seeds/auto-seed.ts b/src/database/seeds/auto-seed.ts new file mode 100644 index 0000000..aa11c0a --- /dev/null +++ b/src/database/seeds/auto-seed.ts @@ -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 { + const empresaRepo = app.get>(getRepositoryToken(Empresa)); + const usuarioRepo = app.get>(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.'); +} diff --git a/src/database/seeds/empresa.seeder.ts b/src/database/seeds/empresa.seeder.ts new file mode 100644 index 0000000..7ba17b2 --- /dev/null +++ b/src/database/seeds/empresa.seeder.ts @@ -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 { + 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.'); + } +} diff --git a/src/database/seeds/main.seeder.ts b/src/database/seeds/main.seeder.ts new file mode 100644 index 0000000..c650220 --- /dev/null +++ b/src/database/seeds/main.seeder.ts @@ -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 { + //await new EmpresaSeeder().run(dataSource); + await new UsuarioSeeder().run(dataSource); + + console.log('Seed completado.'); + } +} diff --git a/src/database/seeds/run.ts b/src/database/seeds/run.ts new file mode 100644 index 0000000..76cf4b8 --- /dev/null +++ b/src/database/seeds/run.ts @@ -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(); diff --git a/src/database/seeds/usuario.seeder.ts b/src/database/seeds/usuario.seeder.ts new file mode 100644 index 0000000..da88a3d --- /dev/null +++ b/src/database/seeds/usuario.seeder.ts @@ -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 { + 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.'); + } +} diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100755 index 0000000..ffde54a --- /dev/null +++ b/src/environments/environment.ts @@ -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' } + ] +}; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..b0b269d --- /dev/null +++ b/src/main.ts @@ -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(); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..ed38bca --- /dev/null +++ b/src/modules/auth/auth.controller.ts @@ -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 { + 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; + } +} diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..b4bcae6 --- /dev/null +++ b/src/modules/auth/auth.module.ts @@ -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 {} diff --git a/src/modules/auth/auth.router.ts b/src/modules/auth/auth.router.ts new file mode 100644 index 0000000..bcd7c80 --- /dev/null +++ b/src/modules/auth/auth.router.ts @@ -0,0 +1,6 @@ +export const routeAuth = { + login: { + route: '[POST].../v1/auth/login', + title: 'Login', + }, +}; diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..ef34c76 --- /dev/null +++ b/src/modules/auth/auth.service.ts @@ -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(); + private readonly MAX_ATTEMPTS = 5; + private readonly BLOCK_DURATION_MS = 60 * 60 * 1000; // 1 hora + + constructor( + @InjectRepository(Usuario) + private usuarioRepo: Repository, + 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, + }); + } + } +} diff --git a/src/modules/auth/dto/login.dto.ts b/src/modules/auth/dto/login.dto.ts new file mode 100644 index 0000000..e85af19 --- /dev/null +++ b/src/modules/auth/dto/login.dto.ts @@ -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; +} diff --git a/src/modules/categoria/categoria.controller.ts b/src/modules/categoria/categoria.controller.ts new file mode 100644 index 0000000..7019711 --- /dev/null +++ b/src/modules/categoria/categoria.controller.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/src/modules/categoria/categoria.module.ts b/src/modules/categoria/categoria.module.ts new file mode 100644 index 0000000..52e904c --- /dev/null +++ b/src/modules/categoria/categoria.module.ts @@ -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 {} diff --git a/src/modules/categoria/categoria.router.ts b/src/modules/categoria/categoria.router.ts new file mode 100644 index 0000000..4d2a22e --- /dev/null +++ b/src/modules/categoria/categoria.router.ts @@ -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', + }, +}; diff --git a/src/modules/categoria/categoria.service.ts b/src/modules/categoria/categoria.service.ts new file mode 100644 index 0000000..4749ebd --- /dev/null +++ b/src/modules/categoria/categoria.service.ts @@ -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, + ) {} + + 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); + } +} diff --git a/src/modules/categoria/dto/categoria.dto.ts b/src/modules/categoria/dto/categoria.dto.ts new file mode 100644 index 0000000..f1737ca --- /dev/null +++ b/src/modules/categoria/dto/categoria.dto.ts @@ -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; +} diff --git a/src/modules/categoria/dto/update-categoria.dto.ts b/src/modules/categoria/dto/update-categoria.dto.ts new file mode 100644 index 0000000..85feab9 --- /dev/null +++ b/src/modules/categoria/dto/update-categoria.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CategoriaDto } from './categoria.dto.js'; + +export class UpdateCategoriaDto extends PartialType(CategoriaDto) {} diff --git a/src/modules/categoria/entities/categoria.entity.ts b/src/modules/categoria/entities/categoria.entity.ts new file mode 100644 index 0000000..8ef36b2 --- /dev/null +++ b/src/modules/categoria/entities/categoria.entity.ts @@ -0,0 +1,10 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity() +export class Categoria { + @PrimaryGeneratedColumn('uuid') + idCategoria: string; + + @Column() + nombre: string; +} diff --git a/src/modules/emprendimiento/dto/emprendimiento.dto.ts b/src/modules/emprendimiento/dto/emprendimiento.dto.ts new file mode 100644 index 0000000..eb6e41c --- /dev/null +++ b/src/modules/emprendimiento/dto/emprendimiento.dto.ts @@ -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; +} diff --git a/src/modules/emprendimiento/dto/update-emprendimiento.dto.ts b/src/modules/emprendimiento/dto/update-emprendimiento.dto.ts new file mode 100644 index 0000000..44fa0b2 --- /dev/null +++ b/src/modules/emprendimiento/dto/update-emprendimiento.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { EmprendimientoDto } from './emprendimiento.dto.js'; + +export class UpdateEmprendimientoDto extends PartialType(EmprendimientoDto) {} diff --git a/src/modules/emprendimiento/emprendimiento.controller.ts b/src/modules/emprendimiento/emprendimiento.controller.ts new file mode 100644 index 0000000..7bcd852 --- /dev/null +++ b/src/modules/emprendimiento/emprendimiento.controller.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/src/modules/emprendimiento/emprendimiento.module.ts b/src/modules/emprendimiento/emprendimiento.module.ts new file mode 100644 index 0000000..da92ef0 --- /dev/null +++ b/src/modules/emprendimiento/emprendimiento.module.ts @@ -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 {} diff --git a/src/modules/emprendimiento/emprendimiento.router.ts b/src/modules/emprendimiento/emprendimiento.router.ts new file mode 100644 index 0000000..220225d --- /dev/null +++ b/src/modules/emprendimiento/emprendimiento.router.ts @@ -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', + }, +}; diff --git a/src/modules/emprendimiento/emprendimiento.service.ts b/src/modules/emprendimiento/emprendimiento.service.ts new file mode 100644 index 0000000..5995f1a --- /dev/null +++ b/src/modules/emprendimiento/emprendimiento.service.ts @@ -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, + ) { } + + async create(emprendimientoDto: EmprendimientoDto): Promise { + 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/src/modules/emprendimiento/entities/emprendimiento.entity.ts b/src/modules/emprendimiento/entities/emprendimiento.entity.ts new file mode 100644 index 0000000..a055755 --- /dev/null +++ b/src/modules/emprendimiento/entities/emprendimiento.entity.ts @@ -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; +} diff --git a/src/modules/empresa/dto/empresa.dto.ts b/src/modules/empresa/dto/empresa.dto.ts new file mode 100644 index 0000000..bd91fc1 --- /dev/null +++ b/src/modules/empresa/dto/empresa.dto.ts @@ -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; +} diff --git a/src/modules/empresa/dto/update-empresa.dto.ts b/src/modules/empresa/dto/update-empresa.dto.ts new file mode 100644 index 0000000..5be5e29 --- /dev/null +++ b/src/modules/empresa/dto/update-empresa.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { EmpresaDto } from './empresa.dto.js'; + +export class UpdateEmpresaDto extends PartialType(EmpresaDto) { } diff --git a/src/modules/empresa/empresa.controller.ts b/src/modules/empresa/empresa.controller.ts new file mode 100644 index 0000000..e91ee3b --- /dev/null +++ b/src/modules/empresa/empresa.controller.ts @@ -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 { + 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 { + 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 { + 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; + } +} diff --git a/src/modules/empresa/empresa.module.ts b/src/modules/empresa/empresa.module.ts new file mode 100644 index 0000000..31e4e1d --- /dev/null +++ b/src/modules/empresa/empresa.module.ts @@ -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 {} diff --git a/src/modules/empresa/empresa.router.ts b/src/modules/empresa/empresa.router.ts new file mode 100644 index 0000000..c85e404 --- /dev/null +++ b/src/modules/empresa/empresa.router.ts @@ -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', + }, +}; diff --git a/src/modules/empresa/empresa.service.ts b/src/modules/empresa/empresa.service.ts new file mode 100644 index 0000000..ff47b85 --- /dev/null +++ b/src/modules/empresa/empresa.service.ts @@ -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, + private configService: ConfigService, + ) { + fs.mkdirSync(this.uploadsPath, { recursive: true }); + } + + async findOne(): Promise { + 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 { + 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 { + 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 { + 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 { + 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'); + } + } +} diff --git a/src/modules/empresa/entities/empresa.entity.ts b/src/modules/empresa/entities/empresa.entity.ts new file mode 100644 index 0000000..44e98df --- /dev/null +++ b/src/modules/empresa/entities/empresa.entity.ts @@ -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; +} diff --git a/src/modules/feriado/dto/feriado.dto.ts b/src/modules/feriado/dto/feriado.dto.ts new file mode 100644 index 0000000..74fdc6c --- /dev/null +++ b/src/modules/feriado/dto/feriado.dto.ts @@ -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; +} diff --git a/src/modules/feriado/dto/update-feriado.dto.ts b/src/modules/feriado/dto/update-feriado.dto.ts new file mode 100644 index 0000000..eb514b7 --- /dev/null +++ b/src/modules/feriado/dto/update-feriado.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { FeriadoDto } from './feriado.dto.js'; + +export class UpdateFeriadoDto extends PartialType(FeriadoDto) { } diff --git a/src/modules/feriado/entities/feriado.entity.ts b/src/modules/feriado/entities/feriado.entity.ts new file mode 100644 index 0000000..721d340 --- /dev/null +++ b/src/modules/feriado/entities/feriado.entity.ts @@ -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; +} diff --git a/src/modules/feriado/feriado.controller.ts b/src/modules/feriado/feriado.controller.ts new file mode 100644 index 0000000..90875cf --- /dev/null +++ b/src/modules/feriado/feriado.controller.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/src/modules/feriado/feriado.module.ts b/src/modules/feriado/feriado.module.ts new file mode 100644 index 0000000..fcaab5f --- /dev/null +++ b/src/modules/feriado/feriado.module.ts @@ -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 {} diff --git a/src/modules/feriado/feriado.router.ts b/src/modules/feriado/feriado.router.ts new file mode 100644 index 0000000..fea54e8 --- /dev/null +++ b/src/modules/feriado/feriado.router.ts @@ -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', + }, +}; diff --git a/src/modules/feriado/feriado.service.ts b/src/modules/feriado/feriado.service.ts new file mode 100644 index 0000000..c92a766 --- /dev/null +++ b/src/modules/feriado/feriado.service.ts @@ -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, + ) { } + + async findAll(): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/src/modules/mensaje/dto/mensaje.dto.ts b/src/modules/mensaje/dto/mensaje.dto.ts new file mode 100644 index 0000000..f7ee546 --- /dev/null +++ b/src/modules/mensaje/dto/mensaje.dto.ts @@ -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; +} diff --git a/src/modules/mensaje/dto/update-estado.dto.ts b/src/modules/mensaje/dto/update-estado.dto.ts new file mode 100644 index 0000000..738628d --- /dev/null +++ b/src/modules/mensaje/dto/update-estado.dto.ts @@ -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; +} diff --git a/src/modules/mensaje/entities/mensaje.entity.ts b/src/modules/mensaje/entities/mensaje.entity.ts new file mode 100644 index 0000000..0a54f26 --- /dev/null +++ b/src/modules/mensaje/entities/mensaje.entity.ts @@ -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; +} diff --git a/src/modules/mensaje/mensaje.controller.ts b/src/modules/mensaje/mensaje.controller.ts new file mode 100644 index 0000000..3bfe41d --- /dev/null +++ b/src/modules/mensaje/mensaje.controller.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/src/modules/mensaje/mensaje.module.ts b/src/modules/mensaje/mensaje.module.ts new file mode 100644 index 0000000..b124ee4 --- /dev/null +++ b/src/modules/mensaje/mensaje.module.ts @@ -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 {} diff --git a/src/modules/mensaje/mensaje.router.ts b/src/modules/mensaje/mensaje.router.ts new file mode 100644 index 0000000..fc2eebb --- /dev/null +++ b/src/modules/mensaje/mensaje.router.ts @@ -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', + }, +}; diff --git a/src/modules/mensaje/mensaje.service.ts b/src/modules/mensaje/mensaje.service.ts new file mode 100644 index 0000000..f3f42e3 --- /dev/null +++ b/src/modules/mensaje/mensaje.service.ts @@ -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, + ) {} + + async findByPaquete(idPaquete: string): Promise { + 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/src/modules/mensaje/mensaje.templates.ts b/src/modules/mensaje/mensaje.templates.ts new file mode 100644 index 0000000..dc7619b --- /dev/null +++ b/src/modules/mensaje/mensaje.templates.ts @@ -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}*`; +} diff --git a/src/modules/paquete/dto/cambiar-ubicacion.dto.ts b/src/modules/paquete/dto/cambiar-ubicacion.dto.ts new file mode 100644 index 0000000..78272f6 --- /dev/null +++ b/src/modules/paquete/dto/cambiar-ubicacion.dto.ts @@ -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; +} diff --git a/src/modules/paquete/dto/crear-lote.dto.ts b/src/modules/paquete/dto/crear-lote.dto.ts new file mode 100644 index 0000000..d24d41f --- /dev/null +++ b/src/modules/paquete/dto/crear-lote.dto.ts @@ -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[]; +} diff --git a/src/modules/paquete/dto/entregar-paquete.dto.ts b/src/modules/paquete/dto/entregar-paquete.dto.ts new file mode 100644 index 0000000..eb04bb5 --- /dev/null +++ b/src/modules/paquete/dto/entregar-paquete.dto.ts @@ -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; +} diff --git a/src/modules/paquete/dto/paquete.dto.ts b/src/modules/paquete/dto/paquete.dto.ts new file mode 100644 index 0000000..4fa9c98 --- /dev/null +++ b/src/modules/paquete/dto/paquete.dto.ts @@ -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; +} diff --git a/src/modules/paquete/dto/update-paquete.dto.ts b/src/modules/paquete/dto/update-paquete.dto.ts new file mode 100644 index 0000000..24f6403 --- /dev/null +++ b/src/modules/paquete/dto/update-paquete.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { PaqueteDto } from './paquete.dto.js'; + +export class UpdatePaqueteDto extends PartialType(PaqueteDto) { } diff --git a/src/modules/paquete/entities/paquete.entity.ts b/src/modules/paquete/entities/paquete.entity.ts new file mode 100644 index 0000000..8ed957c --- /dev/null +++ b/src/modules/paquete/entities/paquete.entity.ts @@ -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; +} diff --git a/src/modules/paquete/paquete.controller.ts b/src/modules/paquete/paquete.controller.ts new file mode 100644 index 0000000..39861e3 --- /dev/null +++ b/src/modules/paquete/paquete.controller.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/src/modules/paquete/paquete.module.ts b/src/modules/paquete/paquete.module.ts new file mode 100644 index 0000000..70aea2d --- /dev/null +++ b/src/modules/paquete/paquete.module.ts @@ -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 {} diff --git a/src/modules/paquete/paquete.router.ts b/src/modules/paquete/paquete.router.ts new file mode 100644 index 0000000..d40aaa5 --- /dev/null +++ b/src/modules/paquete/paquete.router.ts @@ -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', + }, +}; diff --git a/src/modules/paquete/paquete.service.ts b/src/modules/paquete/paquete.service.ts new file mode 100644 index 0000000..304a35c --- /dev/null +++ b/src/modules/paquete/paquete.service.ts @@ -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, + @InjectRepository(Sucursal) + private sucursalRepo: Repository, + @InjectRepository(Feriado) + private feriadoRepo: Repository, + @InjectRepository(Categoria) + private categoriaRepo: Repository, + @InjectRepository(Emprendimiento) + private emprendimientoRepo: Repository, + 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, + }; + } +} diff --git a/src/modules/sucursal/dto/sucursal.dto.ts b/src/modules/sucursal/dto/sucursal.dto.ts new file mode 100644 index 0000000..f3fe6d7 --- /dev/null +++ b/src/modules/sucursal/dto/sucursal.dto.ts @@ -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; +} diff --git a/src/modules/sucursal/dto/update-sucursal.dto.ts b/src/modules/sucursal/dto/update-sucursal.dto.ts new file mode 100644 index 0000000..3f0b1fc --- /dev/null +++ b/src/modules/sucursal/dto/update-sucursal.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { SucursalDto } from './sucursal.dto.js'; + +export class UpdateSucursalDto extends PartialType(SucursalDto) { } diff --git a/src/modules/sucursal/entities/sucursal.entity.ts b/src/modules/sucursal/entities/sucursal.entity.ts new file mode 100644 index 0000000..10c415e --- /dev/null +++ b/src/modules/sucursal/entities/sucursal.entity.ts @@ -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; +} diff --git a/src/modules/sucursal/sucursal.controller.ts b/src/modules/sucursal/sucursal.controller.ts new file mode 100644 index 0000000..96bce3b --- /dev/null +++ b/src/modules/sucursal/sucursal.controller.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/src/modules/sucursal/sucursal.module.ts b/src/modules/sucursal/sucursal.module.ts new file mode 100644 index 0000000..1ae50c2 --- /dev/null +++ b/src/modules/sucursal/sucursal.module.ts @@ -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 {} diff --git a/src/modules/sucursal/sucursal.router.ts b/src/modules/sucursal/sucursal.router.ts new file mode 100644 index 0000000..e9acc66 --- /dev/null +++ b/src/modules/sucursal/sucursal.router.ts @@ -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', + }, +}; diff --git a/src/modules/sucursal/sucursal.service.ts b/src/modules/sucursal/sucursal.service.ts new file mode 100644 index 0000000..c43bf5a --- /dev/null +++ b/src/modules/sucursal/sucursal.service.ts @@ -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, + @InjectRepository(Empresa) + private empresaRepo: Repository, + ) { } + + async create(sucursalDto: SucursalDto): Promise { + 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/src/modules/upload/dto/generar-qr.dto.ts b/src/modules/upload/dto/generar-qr.dto.ts new file mode 100644 index 0000000..29fdd9b --- /dev/null +++ b/src/modules/upload/dto/generar-qr.dto.ts @@ -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; +} diff --git a/src/modules/upload/dto/procesar-ocr.dto.ts b/src/modules/upload/dto/procesar-ocr.dto.ts new file mode 100644 index 0000000..93b785f --- /dev/null +++ b/src/modules/upload/dto/procesar-ocr.dto.ts @@ -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; +} diff --git a/src/modules/upload/upload.controller.ts b/src/modules/upload/upload.controller.ts new file mode 100644 index 0000000..7b3b615 --- /dev/null +++ b/src/modules/upload/upload.controller.ts @@ -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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/src/modules/upload/upload.module.ts b/src/modules/upload/upload.module.ts new file mode 100644 index 0000000..8163765 --- /dev/null +++ b/src/modules/upload/upload.module.ts @@ -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 {} diff --git a/src/modules/upload/upload.router.ts b/src/modules/upload/upload.router.ts new file mode 100644 index 0000000..7c46f16 --- /dev/null +++ b/src/modules/upload/upload.router.ts @@ -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', + }, +}; diff --git a/src/modules/upload/upload.service.ts b/src/modules/upload/upload.service.ts new file mode 100644 index 0000000..2c3771c --- /dev/null +++ b/src/modules/upload/upload.service.ts @@ -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('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 { + 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(); + } +} diff --git a/src/modules/usuario/dto/update-usuario.dto.ts b/src/modules/usuario/dto/update-usuario.dto.ts new file mode 100644 index 0000000..861b8f9 --- /dev/null +++ b/src/modules/usuario/dto/update-usuario.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { UsuarioDto } from './usuario.dto.js'; + +export class UpdateUsuarioDto extends PartialType(UsuarioDto) {} diff --git a/src/modules/usuario/dto/usuario.dto.ts b/src/modules/usuario/dto/usuario.dto.ts new file mode 100644 index 0000000..728b14d --- /dev/null +++ b/src/modules/usuario/dto/usuario.dto.ts @@ -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; +} diff --git a/src/modules/usuario/entities/usuario.entity.ts b/src/modules/usuario/entities/usuario.entity.ts new file mode 100644 index 0000000..157595c --- /dev/null +++ b/src/modules/usuario/entities/usuario.entity.ts @@ -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; +} diff --git a/src/modules/usuario/usuario.controller.ts b/src/modules/usuario/usuario.controller.ts new file mode 100644 index 0000000..c1cc156 --- /dev/null +++ b/src/modules/usuario/usuario.controller.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/src/modules/usuario/usuario.module.ts b/src/modules/usuario/usuario.module.ts new file mode 100644 index 0000000..478aafb --- /dev/null +++ b/src/modules/usuario/usuario.module.ts @@ -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 {} diff --git a/src/modules/usuario/usuario.router.ts b/src/modules/usuario/usuario.router.ts new file mode 100644 index 0000000..aabc319 --- /dev/null +++ b/src/modules/usuario/usuario.router.ts @@ -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', + }, +}; diff --git a/src/modules/usuario/usuario.service.ts b/src/modules/usuario/usuario.service.ts new file mode 100644 index 0000000..29a27c3 --- /dev/null +++ b/src/modules/usuario/usuario.service.ts @@ -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, + 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); + } +} diff --git a/src/modules/zona/dto/update-zona.dto.ts b/src/modules/zona/dto/update-zona.dto.ts new file mode 100644 index 0000000..cf4240a --- /dev/null +++ b/src/modules/zona/dto/update-zona.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { ZonaAlmacenamientoDto } from './zona.dto.js'; + +export class UpdateZonaAlmacenamientoDto extends PartialType(ZonaAlmacenamientoDto) {} diff --git a/src/modules/zona/dto/zona.dto.ts b/src/modules/zona/dto/zona.dto.ts new file mode 100644 index 0000000..e994945 --- /dev/null +++ b/src/modules/zona/dto/zona.dto.ts @@ -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; +} diff --git a/src/modules/zona/entities/zona.entity.ts b/src/modules/zona/entities/zona.entity.ts new file mode 100644 index 0000000..889a009 --- /dev/null +++ b/src/modules/zona/entities/zona.entity.ts @@ -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; +} diff --git a/src/modules/zona/zona.controller.ts b/src/modules/zona/zona.controller.ts new file mode 100644 index 0000000..f899e58 --- /dev/null +++ b/src/modules/zona/zona.controller.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/src/modules/zona/zona.module.ts b/src/modules/zona/zona.module.ts new file mode 100644 index 0000000..70e7d7c --- /dev/null +++ b/src/modules/zona/zona.module.ts @@ -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 {} diff --git a/src/modules/zona/zona.router.ts b/src/modules/zona/zona.router.ts new file mode 100644 index 0000000..4c7a385 --- /dev/null +++ b/src/modules/zona/zona.router.ts @@ -0,0 +1,26 @@ +export const routeZona = { + multiple: { + route: '[POST].../v1/zona/multiple', + title: 'Create Multiple Zona', + }, + create: { + route: '[POST].../v1/zona', + title: 'Create Zona', + }, + one: { + route: '[GET].../v1/zona/:idZonaAlmacenamiento', + title: 'Find One Zona', + }, + find: { + route: '[POST].../v1/zona/find', + title: 'Find Zona WhereCondition[], Attribute & Direccion', + }, + update: { + route: '[PATCH].../v1/zona/:idZonaAlmacenamiento', + title: 'Update Zona', + }, + remove: { + route: '[DELETE].../v1/zona/:idZonaAlmacenamiento', + title: 'Remove Zona', + }, +}; diff --git a/src/modules/zona/zona.service.ts b/src/modules/zona/zona.service.ts new file mode 100644 index 0000000..b19b3b4 --- /dev/null +++ b/src/modules/zona/zona.service.ts @@ -0,0 +1,45 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ZonaAlmacenamiento } from './entities/zona.entity.js'; +import { ZonaAlmacenamientoDto } from './dto/zona.dto.js'; +import { UpdateZonaAlmacenamientoDto } from './dto/update-zona.dto.js'; + +@Injectable() +export class ZonaService { + constructor( + @InjectRepository(ZonaAlmacenamiento) + private repo: Repository, + ) {} + + async findAll() { + return this.repo.find(); + } + + async findOne(id: string) { + const zona = await this.repo.findOne({ where: { idZonaAlmacenamiento: id } }); + if (!zona) throw new NotFoundException('Zona no encontrada'); + return zona; + } + + async create(dto: ZonaAlmacenamientoDto) { + const zona = this.repo.create(dto); + const saved = await this.repo.save(zona); + return saved; + } + + async update(id: string, dto: UpdateZonaAlmacenamientoDto) { + const zona = await this.repo.findOne({ where: { idZonaAlmacenamiento: id } }); + if (!zona) throw new NotFoundException('Zona no encontrada'); + + Object.assign(zona, dto); + const saved = await this.repo.save(zona); + return saved; + } + + async remove(id: string) { + const zona = await this.repo.findOne({ where: { idZonaAlmacenamiento: id } }); + if (!zona) throw new NotFoundException('Zona no encontrada'); + await this.repo.remove(zona); + } +} diff --git a/src/shared/class/find.dto.ts b/src/shared/class/find.dto.ts new file mode 100755 index 0000000..7accf36 --- /dev/null +++ b/src/shared/class/find.dto.ts @@ -0,0 +1,37 @@ +import { IsString, IsNumber, IsIn, IsArray, ValidateNested, IsOptional } from 'class-validator'; +import { Type } from 'class-transformer'; + +// Interfaz para cada condición de búsqueda +export class WhereConditionDto { + @IsString() + attribute: string; + + @IsString() + value: string; // O el tipo de dato que uses + + @IsString() + @IsOptional() + @IsIn(['=', 'like', '>', '<', '>=', '<=']) + operator?: '=' | 'like' | '>' | '<' | '>=' | '<='; +} + +// DTO principal para la solicitud +export class FindDto { + @IsArray() + @Type(() => WhereConditionDto) + whereConditions: WhereConditionDto[]; + + @IsString() + orderByAttribute: string; + + @IsString() + @IsIn(['ASC', 'DESC']) + orderByDirection: 'ASC' | 'DESC'; + + @IsNumber() + limit: number; + + @IsNumber() + offset: number; // paginacion + +} \ No newline at end of file diff --git a/src/shared/guards.module.ts b/src/shared/guards.module.ts new file mode 100644 index 0000000..e32a949 --- /dev/null +++ b/src/shared/guards.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { ApiKeyGuard } from './guards/api-key.guard.js'; + +@Global() +@Module({ + providers: [ApiKeyGuard], + exports: [ApiKeyGuard], +}) +export class GuardsModule {} diff --git a/src/shared/guards/api-key.guard.ts b/src/shared/guards/api-key.guard.ts new file mode 100644 index 0000000..d9ce48e --- /dev/null +++ b/src/shared/guards/api-key.guard.ts @@ -0,0 +1,33 @@ +import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { REQUIRE_API_KEY } from '../interfaces/api-key.interface.js'; + +@Injectable() +export class ApiKeyGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requireApiKey = this.reflector.getAllAndOverride(REQUIRE_API_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (!requireApiKey) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const apiKey = request.headers['x-api-key']; + const workerKey = request.headers['x-worker-key']; + + if (apiKey && apiKey === process.env.API_KEY) { + return true; + } + + if (workerKey && workerKey === process.env.WORKER_KEY) { + return true; + } + + throw new UnauthorizedException('API Key o Worker Key inválida'); + } +} diff --git a/src/shared/guards/jwt-auth.guard.ts b/src/shared/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..7055ccb --- /dev/null +++ b/src/shared/guards/jwt-auth.guard.ts @@ -0,0 +1,22 @@ +import { Injectable, ExecutionContext } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { Reflector } from '@nestjs/core'; +import { IS_PUBLIC_KEY } from '../interfaces/auth.interface.js'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + constructor(private reflector: Reflector) { + super(); + } + + canActivate(context: ExecutionContext) { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) { + return true; + } + return super.canActivate(context); + } +} diff --git a/src/shared/interfaces/api-key.interface.ts b/src/shared/interfaces/api-key.interface.ts new file mode 100644 index 0000000..f01236f --- /dev/null +++ b/src/shared/interfaces/api-key.interface.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const REQUIRE_API_KEY = 'requireApiKey'; +export const RequireApiKey = () => SetMetadata(REQUIRE_API_KEY, true); diff --git a/src/shared/interfaces/api.result.ts b/src/shared/interfaces/api.result.ts new file mode 100755 index 0000000..b18e44e --- /dev/null +++ b/src/shared/interfaces/api.result.ts @@ -0,0 +1,25 @@ +export interface ApiResult { + + title: string; + route: string; + status: string; + code: number; + message: string; + boolean: boolean; + rows: number; + data: any[] | null; + +} + +export function defaultApiResult(title: string, route: string): ApiResult { + return { + title, + route, + status: "", + code: 0, + message: "", + boolean: false, + rows: 0, + data: [] + } +} \ No newline at end of file diff --git a/src/shared/interfaces/auth.interface.ts b/src/shared/interfaces/auth.interface.ts new file mode 100644 index 0000000..6fc1fd5 --- /dev/null +++ b/src/shared/interfaces/auth.interface.ts @@ -0,0 +1,10 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); + +export interface JwtPayload { + sub: number; + usuario: string; + rol: string; +} diff --git a/src/shared/interfaces/service.result.ts b/src/shared/interfaces/service.result.ts new file mode 100755 index 0000000..0208572 --- /dev/null +++ b/src/shared/interfaces/service.result.ts @@ -0,0 +1,19 @@ +export interface ServiceResult { + + boolean: boolean; + message: string; + number: number; + object: any | null; + data: any[] | null; + +} + +export function defaultServiceResult (): ServiceResult{ + return { + boolean: false, + message: "", + number: 0, + object: null, + data: [] +} +} \ No newline at end of file diff --git a/src/shared/services/database.service.ts b/src/shared/services/database.service.ts new file mode 100644 index 0000000..6f8a406 --- /dev/null +++ b/src/shared/services/database.service.ts @@ -0,0 +1,28 @@ +import { Client } from 'pg'; + +export async function ensureDatabase(): Promise { + const client = new Client({ + host: process.env.DB_HOST || 'localhost', + port: Number(process.env.DB_PORT) || 5432, + user: process.env.DB_USERNAME || 'postgres', + password: process.env.DB_PASSWORD || 'postgres', + database: 'postgres', + }); + + await client.connect(); + + const dbName = process.env.DB_NAME || 'paqueteria'; + const result = await client.query( + 'SELECT 1 FROM pg_database WHERE datname = $1', + [dbName], + ); + + if (result.rowCount === 0) { + await client.query(`CREATE DATABASE "${dbName}"`); + console.log(`Database "${dbName}" creada.`); + } else { + console.log(`Database "${dbName}" ya existe.`); + } + + await client.end(); +} diff --git a/src/shared/services/ocr/google-vision.provider.ts b/src/shared/services/ocr/google-vision.provider.ts new file mode 100644 index 0000000..376067b --- /dev/null +++ b/src/shared/services/ocr/google-vision.provider.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { OcrProvider, OcrResult } from './ocr.provider.js'; + +@Injectable() +export class GoogleVisionProvider implements OcrProvider { + private apiKey: string; + + constructor(private configService: ConfigService) { + this.apiKey = this.configService.get('GOOGLE_VISION_API_KEY', ''); + } + + async reconocer(imagen: Buffer): Promise { + const base64 = imagen.toString('base64'); + + const response = await fetch( + `https://vision.googleapis.com/v1/images:annotate?key=${this.apiKey}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + requests: [ + { + image: { content: base64 }, + features: [{ type: 'TEXT_DETECTION', maxResults: 1 }], + }, + ], + }), + }, + ); + + const result = await response.json(); + const texto = result.responses?.[0]?.fullTextAnnotation?.text || ''; + + return this.parsearTexto(texto.trim()); + } + + private parsearTexto(texto: string): OcrResult { + const lineas = texto.split('\n').map((l) => l.trim()).filter(Boolean); + + let nombre = ''; + let whatsapp = ''; + + for (const linea of lineas) { + if (!nombre && /^[A-Za-zÁÉÍÓÚáéíóúñÑ\s]{3,}$/.test(linea)) { + nombre = linea; + } + const phoneMatch = linea.match(/(\d{8})/); + if (!whatsapp && phoneMatch) { + whatsapp = phoneMatch[1]; + } + } + + return { + texto, + nombre: nombre || undefined, + whatsapp: whatsapp || undefined, + codigoPais: whatsapp ? '591' : undefined, + }; + } +} diff --git a/src/shared/services/ocr/mimo.provider.ts b/src/shared/services/ocr/mimo.provider.ts new file mode 100644 index 0000000..3abc839 --- /dev/null +++ b/src/shared/services/ocr/mimo.provider.ts @@ -0,0 +1,102 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import OpenAI from 'openai'; +import { OcrProvider, OcrResult } from './ocr.provider.js'; + +@Injectable() +export class MimoProvider implements OcrProvider { + private client: OpenAI; + private readonly maxRetries = 3; + private readonly retryDelayMs = 1500; + + constructor(private configService: ConfigService) { + this.client = new OpenAI({ + apiKey: this.configService.get('MIMO_API_KEY', ''), + baseURL: 'https://token-plan-sgp.xiaomimimo.com/v1', + }); + } + + async reconocer(imagen: Buffer): Promise { + const base64 = imagen.toString('base64'); + const mimeType = this.detectarMimeType(imagen); + + for (let attempt = 1; attempt <= this.maxRetries; attempt++) { + try { + const response = await this.client.chat.completions.create({ + model: 'mimo-v2.5', + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: `Analiza esta imagen de una etiqueta de paquete manuscrita. Extrae el nombre completo del destinatario y su número de celular (WhatsApp). Responde ÚNICAMENTE con un JSON válido con este formato exacto, sin texto adicional ni markdown: +{ + "nombre": "nombre completo encontrado", + "whatsapp": "número de celular sin espacios ni guiones" +} +Si no encuentras alguno de los campos, usa cadena vacía "".`, + }, + { + type: 'image_url', + image_url: { + url: `data:${mimeType};base64,${base64}`, + }, + }, + ], + }, + ], + max_tokens: 300, + temperature: 0, + }); + + const content = response.choices[0]?.message?.content || ''; + console.log(`DEBUG MIMO response (attempt ${attempt}):`, content); + + const result = this.parsearRespuesta(content); + if (result.nombre || result.whatsapp) { + return result; + } + + console.log(`DEBUG MIMO empty result, retrying... (${attempt}/${this.maxRetries})`); + } catch (error: any) { + console.log(`DEBUG MIMO error (attempt ${attempt}):`, error.message); + } + + if (attempt < this.maxRetries) { + await this.delay(this.retryDelayMs * attempt); + } + } + + console.log('DEBUG MIMO all retries exhausted, returning empty'); + return { texto: '' }; + } + + private delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + private detectarMimeType(buffer: Buffer): string { + if (buffer[0] === 0xff && buffer[1] === 0xd8) return 'image/jpeg'; + if (buffer[0] === 0x89 && buffer[1] === 0x50) return 'image/png'; + if (buffer[0] === 0x52 && buffer[1] === 0x49) return 'image/webp'; + return 'image/jpeg'; + } + + private parsearRespuesta(content: string): OcrResult { + try { + const match = content.match(/\{[\s\S]*\}/); + if (!match) return { texto: content }; + + const parsed = JSON.parse(match[0]); + return { + texto: content, + nombre: parsed.nombre || undefined, + whatsapp: parsed.whatsapp || undefined, + codigoPais: parsed.codigoPais || undefined, + }; + } catch { + return { texto: content }; + } + } +} diff --git a/src/shared/services/ocr/ocr.factory.ts b/src/shared/services/ocr/ocr.factory.ts new file mode 100644 index 0000000..360c44c --- /dev/null +++ b/src/shared/services/ocr/ocr.factory.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { OcrProvider } from './ocr.provider.js'; +import { TesseractProvider } from './tesseract.provider.js'; +import { OcrSpaceProvider } from './ocrspace.provider.js'; +import { GoogleVisionProvider } from './google-vision.provider.js'; +import { MimoProvider } from './mimo.provider.js'; + +@Injectable() +export class OcrFactory { + constructor( + private configService: ConfigService, + private readonly tesseract: TesseractProvider, + private readonly ocrSpace: OcrSpaceProvider, + private readonly googleVision: GoogleVisionProvider, + private readonly mimo: MimoProvider, + ) {} + + getProvider(nombre?: string): OcrProvider { + const provider = (nombre || this.configService.get('OCR_PROVIDER') || 'tesseract').toLowerCase(); + + switch (provider) { + case 'ocrspace': + return this.ocrSpace; + case 'google-vision': + return this.googleVision; + case 'mimo': + return this.mimo; + case 'tesseract': + default: + return this.tesseract; + } + } +} diff --git a/src/shared/services/ocr/ocr.module.ts b/src/shared/services/ocr/ocr.module.ts new file mode 100644 index 0000000..fdceda2 --- /dev/null +++ b/src/shared/services/ocr/ocr.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { OcrFactory } from './ocr.factory.js'; +import { TesseractProvider } from './tesseract.provider.js'; +import { OcrSpaceProvider } from './ocrspace.provider.js'; +import { GoogleVisionProvider } from './google-vision.provider.js'; +import { MimoProvider } from './mimo.provider.js'; + +@Module({ + providers: [OcrFactory, TesseractProvider, OcrSpaceProvider, GoogleVisionProvider, MimoProvider], + exports: [OcrFactory], +}) +export class OcrModule {} diff --git a/src/shared/services/ocr/ocr.provider.ts b/src/shared/services/ocr/ocr.provider.ts new file mode 100644 index 0000000..76e85e3 --- /dev/null +++ b/src/shared/services/ocr/ocr.provider.ts @@ -0,0 +1,10 @@ +export interface OcrResult { + texto: string; + nombre?: string; + whatsapp?: string; + codigoPais?: string; +} + +export interface OcrProvider { + reconocer(imagen: Buffer): Promise; +} diff --git a/src/shared/services/ocr/ocrspace.provider.ts b/src/shared/services/ocr/ocrspace.provider.ts new file mode 100644 index 0000000..4d90c59 --- /dev/null +++ b/src/shared/services/ocr/ocrspace.provider.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { OcrProvider, OcrResult } from './ocr.provider.js'; + +@Injectable() +export class OcrSpaceProvider implements OcrProvider { + private apiKey: string; + + constructor(private configService: ConfigService) { + this.apiKey = this.configService.get('OCRSPACE_API_KEY', ''); + } + + async reconocer(imagen: Buffer): Promise { + const base64 = imagen.toString('base64'); + const response = await fetch('https://api.ocr.space/parse/image', { + method: 'POST', + headers: { + apikey: this.apiKey, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `base64Image=data:image/jpeg;base64,${base64}&language=spa`, + }); + + const result = await response.json(); + const texto = result.ParsedResults?.[0]?.ParsedText || ''; + + return this.parsearTexto(texto.trim()); + } + + private parsearTexto(texto: string): OcrResult { + const lineas = texto.split('\n').map((l) => l.trim()).filter(Boolean); + + let nombre = ''; + let whatsapp = ''; + + for (const linea of lineas) { + if (!nombre && /^[A-Za-zÁÉÍÓÚáéíóúñÑ\s]{3,}$/.test(linea)) { + nombre = linea; + } + const phoneMatch = linea.match(/(\d{8})/); + if (!whatsapp && phoneMatch) { + whatsapp = phoneMatch[1]; + } + } + + return { + texto, + nombre: nombre || undefined, + whatsapp: whatsapp || undefined, + codigoPais: whatsapp ? '591' : undefined, + }; + } +} diff --git a/src/shared/services/ocr/tesseract.provider.ts b/src/shared/services/ocr/tesseract.provider.ts new file mode 100644 index 0000000..0500313 --- /dev/null +++ b/src/shared/services/ocr/tesseract.provider.ts @@ -0,0 +1,80 @@ +import { Injectable } from '@nestjs/common'; +import { OcrProvider, OcrResult } from './ocr.provider.js'; + +@Injectable() +export class TesseractProvider implements OcrProvider { + async reconocer(imagen: Buffer): Promise { + const Tesseract = await import('tesseract.js'); + + const worker = await Tesseract.createWorker('spa'); + const { data } = await worker.recognize(imagen); + await worker.terminate(); + + const texto = data.text.trim(); + return this.parsearTexto(texto); + } + + private parsearTexto(texto: string): OcrResult { + console.log('DEBUG OCR texto completo:', texto); + const lineas = texto.split('\n').map((l) => l.trim()).filter(Boolean); + + let nombre = ''; + let whatsapp = ''; + + // ── Teléfono: buscar 8 dígitos en TODO el texto ── + const todosLosDigitos = texto.replace(/\D/g, ''); + // Buscar secuencias de exactamente 8 dígitos + const match8 = todosLosDigitos.match(/(\d{8})/); + if (match8) { + whatsapp = match8[1]; + } else { + // Intentar con corrección de errores OCR + const corregido = this.corregirDigitos(texto); + const matchCorregido = corregido.match(/(\d{8})/); + if (matchCorregido) { + whatsapp = matchCorregido[1]; + } + } + + // ── Nombre: buscar líneas que sean solo letras y espacios ── + // Preferir líneas más largas y con más palabras + const candidatos: string[] = []; + for (const linea of lineas) { + // Solo letras, espacios y acentos + const limpio = linea.replace(/[^A-Za-zÁÉÍÓÚáéíóúñÑ\s]/g, '').trim(); + if (limpio.length < 5) continue; + const palabras = limpio.split(/\s+/).filter(Boolean); + if (palabras.length < 2) continue; + // Verificar que cada palabra tenga al menos 2 letras + if (!palabras.every((p) => p.length >= 2)) continue; + // Verificar que no sean solo una letra repetida + if (new Set(palabras).size === 1 && palabras.length === 2) continue; + candidatos.push(limpio); + } + + // Preferir el candidato más largo (más probable que sea el nombre real) + if (candidatos.length > 0) { + candidatos.sort((a, b) => b.length - a.length); + nombre = candidatos[0]; + } + + return { + texto, + nombre: nombre || undefined, + whatsapp: whatsapp || undefined, + codigoPais: whatsapp ? '591' : undefined, + }; + } + + private corregirDigitos(texto: string): string { + return texto + .replace(/[Oo]/g, '0') + .replace(/[IiLl]/g, '1') + .replace(/[Zz]/g, '2') + .replace(/[Ss]/g, '5') + .replace(/[Bb]/g, '8') + .replace(/[Gg]/g, '6') + .replace(/[Tt]/g, '7') + .replace(/[^0-9]/g, ''); + } +} diff --git a/src/shared/services/qr/qr.module.ts b/src/shared/services/qr/qr.module.ts new file mode 100644 index 0000000..111c6aa --- /dev/null +++ b/src/shared/services/qr/qr.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { QrRenderer } from './qr.renderer.js'; + +@Module({ + providers: [QrRenderer], + exports: [QrRenderer], +}) +export class QrModule {} diff --git a/src/shared/services/qr/qr.renderer.ts b/src/shared/services/qr/qr.renderer.ts new file mode 100644 index 0000000..c10d7c2 --- /dev/null +++ b/src/shared/services/qr/qr.renderer.ts @@ -0,0 +1,69 @@ +import { Injectable } from '@nestjs/common'; +import * as path from 'path'; +import * as fs from 'fs'; +import sharp from 'sharp'; +import * as QRCode from 'qrcode'; + +@Injectable() +export class QrRenderer { + private templatePath = path.join(process.cwd(), 'assets', 'templates', 'qr-template.png'); + + async generar(tokenRetiro: string, nombreSucursal: string): Promise { + const qrBuffer = await QRCode.toBuffer(tokenRetiro, { + type: 'png', + width: 600, + margin: 1, + color: { dark: '#4B0082', light: '#FFFFFF' }, + }); + + const templateMetadata = await sharp(this.templatePath).metadata(); + const templateWidth = templateMetadata.width || 1080; + const templateHeight = templateMetadata.height || 1080; + + const nombreSvg = Buffer.from(` + + + ${nombreSucursal} + + `); + + const qrSize = Math.round(templateWidth * 0.5); + const qrResized = await sharp(qrBuffer) + .resize(qrSize, qrSize) + .toBuffer(); + + const qrX = Math.round((templateWidth - qrSize) / 2); + const qrY = Math.round(templateHeight * 0.3); + + const resultado = await sharp(this.templatePath) + .composite([ + { + input: nombreSvg, + top: 10, + left: 0, + }, + { + input: qrResized, + top: qrY, + left: qrX, + }, + ]) + .png() + .toBuffer(); + + const qrFilename = `${tokenRetiro}.png`; + const qrDir = path.join(process.cwd(), 'uploads', 'qr'); + fs.mkdirSync(qrDir, { recursive: true }); + fs.writeFileSync(path.join(qrDir, qrFilename), resultado); + + return `/uploads/qr/${qrFilename}`; + } +} diff --git a/src/shared/services/uuid.service.ts b/src/shared/services/uuid.service.ts new file mode 100644 index 0000000..ca8634c --- /dev/null +++ b/src/shared/services/uuid.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; + +@Injectable() +export class UuidService { + generate(): string { + return uuidv4(); + } +} diff --git a/src/shared/strategies/jwt.strategy.ts b/src/shared/strategies/jwt.strategy.ts new file mode 100644 index 0000000..ca98db6 --- /dev/null +++ b/src/shared/strategies/jwt.strategy.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(config: ConfigService) { + const secret = config.get('JWT_SECRET'); + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: secret!, + }); + } + + async validate(payload: any) { + return { + idUsuario: payload.sub, + usuario: payload.usuario, + rol: payload.rol, + }; + } +} diff --git a/src/shared/type/query-builder.types.ts b/src/shared/type/query-builder.types.ts new file mode 100755 index 0000000..ddb6c9f --- /dev/null +++ b/src/shared/type/query-builder.types.ts @@ -0,0 +1,5 @@ +export type WhereCondition = { + attribute: string; + value: string | number | boolean; + operator?: '=' | 'like' | '>' | '<' | '>=' | '<='; +}; \ No newline at end of file diff --git a/src/shared/utils/fecha.util.ts b/src/shared/utils/fecha.util.ts new file mode 100644 index 0000000..396ba52 --- /dev/null +++ b/src/shared/utils/fecha.util.ts @@ -0,0 +1,85 @@ +import { Repository } from 'typeorm'; +import { Feriado } from '../../modules/feriado/entities/feriado.entity.js'; + +export function esDomingo(fecha: Date): boolean { + return fecha.getDay() === 0; +} + +export function esFeriado(fecha: Date, feriados: Date[]): boolean { + const fechaStr = fecha.toISOString().split('T')[0]; + return feriados.some((f) => f.toISOString().split('T')[0] === fechaStr); +} + +export function esDiaNoLaborable(fecha: Date, feriados: Date[]): boolean { + return esDomingo(fecha) || esFeriado(fecha, feriados); +} + +export function sumarDias(fecha: Date, dias: number): Date { + const resultado = new Date(fecha); + resultado.setDate(resultado.getDate() + dias); + return resultado; +} + +export function calcularDiasTranscurridos(fechaInicio: Date, fechaFin: Date): number { + const diffTime = fechaFin.getTime() - fechaInicio.getTime(); + return Math.floor(diffTime / (1000 * 60 * 60 * 24)); +} + +export function calcularDiasAdicionales( + fechaRecepcion: Date, + fechaConsulta: Date, + diasIncluidos: number, + feriados: Date[], +): number { + let diasTranscurridos = 0; + let diasAdicionales = 0; + let fechaActual = sumarDias(fechaRecepcion, 1); // Empezar desde el día siguiente + + while (fechaActual <= fechaConsulta) { + if (!esDiaNoLaborable(fechaActual, feriados)) { + diasTranscurridos++; + if (diasTranscurridos > diasIncluidos) { + diasAdicionales++; + } + } + fechaActual = sumarDias(fechaActual, 1); + } + + return diasAdicionales; +} + +export function calcularFechaLimite( + fechaRecepcion: Date, + diasIncluidos: number, + feriados: Date[], +): Date { + let diasContados = 0; + let fechaActual = sumarDias(fechaRecepcion, 1); // Empezar desde el día siguiente + + while (diasContados < diasIncluidos) { + if (!esDiaNoLaborable(fechaActual, feriados)) { + diasContados++; + } + if (diasContados < diasIncluidos) { + fechaActual = sumarDias(fechaActual, 1); + } + } + + return fechaActual; +} + +export async function obtenerFeriadosDelRango( + feriadoRepo: Repository, + fechaInicio: Date, + fechaFin: Date, +): Promise { + const feriados = await feriadoRepo + .createQueryBuilder('f') + .where('f.fecha >= :inicio AND f.fecha <= :fin', { + inicio: fechaInicio.toISOString().split('T')[0], + fin: fechaFin.toISOString().split('T')[0], + }) + .getMany(); + + return feriados.map((f) => new Date(f.fecha)); +} diff --git a/src/shared/utils/feriados.util.ts b/src/shared/utils/feriados.util.ts new file mode 100644 index 0000000..223af1a --- /dev/null +++ b/src/shared/utils/feriados.util.ts @@ -0,0 +1,65 @@ +interface FeriadoGenerado { + fecha: string; + nombre: string; +} + +function calcularPascua(year: number): Date { + const a = year % 19; + const b = Math.floor(year / 100); + const c = year % 100; + const d = Math.floor(b / 4); + const e = b % 4; + const f = Math.floor((b + 8) / 25); + const g = Math.floor((b - f + 1) / 3); + const h = (19 * a + b - d - g + 15) % 30; + const i = Math.floor(c / 4); + const k = c % 4; + const l = (32 + 2 * e + 2 * i - h - k) % 7; + const m = Math.floor((a + 11 * h + 22 * l) / 451); + const mes = Math.floor((h + l - 7 * m + 114) / 31) - 1; + const dia = ((h + l - 7 * m + 114) % 31) + 1; + return new Date(year, mes, dia); +} + +function sumarDias(fecha: Date, dias: number): Date { + const resultado = new Date(fecha); + resultado.setDate(resultado.getDate() + dias); + return resultado; +} + +function formatoFecha(fecha: Date): string { + const year = fecha.getFullYear(); + const mes = String(fecha.getMonth() + 1).padStart(2, '0'); + const dia = String(fecha.getDate()).padStart(2, '0'); + return `${year}-${mes}-${dia}`; +} + +export function generarFeriadosBolivia(year: number): FeriadoGenerado[] { + const feriados: FeriadoGenerado[] = []; + + // Feriados fijos + feriados.push({ fecha: `${year}-01-01`, nombre: 'Año Nuevo' }); + feriados.push({ fecha: `${year}-01-22`, nombre: 'Día del Estado Plurinacional' }); + feriados.push({ fecha: `${year}-03-22`, nombre: 'Día del Mar' }); + feriados.push({ fecha: `${year}-05-01`, nombre: 'Día del Trabajo' }); + feriados.push({ fecha: `${year}-05-25`, nombre: 'Día de la Revolución Nacional' }); + feriados.push({ fecha: `${year}-06-21`, nombre: 'Año Nuevo Aymara' }); + feriados.push({ fecha: `${year}-08-06`, nombre: 'Día de la Independencia' }); + feriados.push({ fecha: `${year}-11-02`, nombre: 'Día de los Difuntos' }); + feriados.push({ fecha: `${year}-12-25`, nombre: 'Navidad' }); + + // Feriados móviles (calculados según Pascua) + const pascua = calcularPascua(year); + + // Carnaval (lunes y martes antes del miércoles de ceniza, 48 días antes de Pascua) + const carnavalLunes = sumarDias(pascua, -48); + const carnavalMartes = sumarDias(pascua, -47); + feriados.push({ fecha: formatoFecha(carnavalLunes), nombre: 'Carnaval' }); + feriados.push({ fecha: formatoFecha(carnavalMartes), nombre: 'Carnaval' }); + + // Viernes Santo (2 días antes de Pascua) + const viernesSanto = sumarDias(pascua, -2); + feriados.push({ fecha: formatoFecha(viernesSanto), nombre: 'Viernes Santo' }); + + return feriados; +} diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts new file mode 100644 index 0000000..fd20981 --- /dev/null +++ b/test/app.e2e-spec.ts @@ -0,0 +1,29 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { App } from 'supertest/types'; +import { AppModule } from './../src/app.module.js'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); + + afterEach(async () => { + await app.close(); + }); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..2eafee0 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src"], + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5ab0f26 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "resolvePackageJsonExports": true, + "esModuleInterop": true, + "isolatedModules": true, + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "incremental": true, + "skipLibCheck": true, + "strict": true, + "strictPropertyInitialization": false, + "types": ["vitest/globals", "node", "multer"] + } +} diff --git a/typeorm.config.ts b/typeorm.config.ts new file mode 100644 index 0000000..ee15936 --- /dev/null +++ b/typeorm.config.ts @@ -0,0 +1,32 @@ +import 'dotenv/config'; +import { DataSource } from 'typeorm'; +import MainSeeder from './src/database/seeds/main.seeder.js'; + +import { Empresa } from './src/modules/empresa/entities/empresa.entity.js'; +import { Usuario } from './src/modules/usuario/entities/usuario.entity.js'; +import { Sucursal } from './src/modules/sucursal/entities/sucursal.entity.js'; +import { Categoria } from './src/modules/categoria/entities/categoria.entity.js'; +import { ZonaAlmacenamiento } from './src/modules/zona/entities/zona.entity.js'; +import { Paquete } from './src/modules/paquete/entities/paquete.entity.js'; +import { Feriado } from './src/modules/feriado/entities/feriado.entity.js'; +import { MensajeWhatsApp } from './src/modules/mensaje/entities/mensaje.entity.js'; + +export default 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, + Sucursal, + Categoria, + ZonaAlmacenamiento, + Paquete, + Feriado, + MensajeWhatsApp, + ], + seeds: [MainSeeder], +} as any); diff --git a/vitest.config.e2e.ts b/vitest.config.e2e.ts new file mode 100644 index 0000000..b5b532c --- /dev/null +++ b/vitest.config.e2e.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + plugins: [tsconfigPaths()], + test: { + globals: true, + root: './', + include: ['**/*.e2e-spec.ts'], + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..9384f47 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + // Resolves the path aliases declared in tsconfig.json, including the ones + // added by `nest g library`. + plugins: [tsconfigPaths()], + test: { + globals: true, + root: './', + include: ['**/*.spec.ts'], + }, +});