Initial commit
This commit is contained in:
56
.gitignore
vendored
Executable file
56
.gitignore
vendored
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# temp directory
|
||||||
|
.temp
|
||||||
|
.tmp
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
4
.prettierrc
Executable file
4
.prettierrc
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
99
ecosystem.config.js
Normal file
99
ecosystem.config.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [{
|
||||||
|
name: 'karibeo-api',
|
||||||
|
script: 'dist/main.js',
|
||||||
|
instances: 2, // Puedes ajustar según tu servidor
|
||||||
|
exec_mode: 'cluster',
|
||||||
|
watch: false,
|
||||||
|
max_memory_restart: '1G',
|
||||||
|
restart_delay: 4000,
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'development',
|
||||||
|
// Database
|
||||||
|
DB_HOST: 'localhost',
|
||||||
|
DB_PORT: '5432',
|
||||||
|
DB_USERNAME: 'karibeo',
|
||||||
|
DB_PASSWORD: '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',
|
||||||
|
DB_NAME: 'karibeo_db',
|
||||||
|
// JWT
|
||||||
|
JWT_SECRET: 'karibeo_jwt_secret_key_2025_very_secure',
|
||||||
|
JWT_EXPIRES_IN: '24h',
|
||||||
|
JWT_REFRESH_SECRET: 'karibeo_refresh_secret_key_2025',
|
||||||
|
JWT_REFRESH_EXPIRES_IN: '7d',
|
||||||
|
// App
|
||||||
|
APP_PORT: '3000',
|
||||||
|
APP_NAME: 'Karibeo API',
|
||||||
|
APP_VERSION: '1.0.0',
|
||||||
|
APP_DESCRIPTION: 'Integrated Tourism Applications System API',
|
||||||
|
// Throttle
|
||||||
|
THROTTLE_TTL: '60',
|
||||||
|
THROTTLE_LIMIT: '100',
|
||||||
|
// CORS
|
||||||
|
CORS_ORIGINS: 'http://localhost:3000,http://localhost:4200,http://localhost:8080',
|
||||||
|
// Stripe Configuration
|
||||||
|
STRIPE_SECRET_KEY: 'sk_test_your_stripe_secret_key_here',
|
||||||
|
STRIPE_PUBLISHABLE_KEY: 'pk_test_your_stripe_publishable_key_here',
|
||||||
|
STRIPE_WEBHOOK_SECRET: 'whsec_your_webhook_secret_here',
|
||||||
|
// AWS S3 Configuration
|
||||||
|
AWS_ACCESS_KEY_ID: 'your_aws_access_key_id',
|
||||||
|
AWS_SECRET_ACCESS_KEY: 'your_aws_secret_access_key',
|
||||||
|
AWS_REGION: 'us-east-1',
|
||||||
|
AWS_S3_BUCKET: 'karibeo-assets',
|
||||||
|
AWS_CLOUDFRONT_URL: 'https://d1234567890.cloudfront.net',
|
||||||
|
// SendGrid Configuration
|
||||||
|
SENDGRID_API_KEY: 'SG.your_sendgrid_api_key_here',
|
||||||
|
SENDGRID_FROM_EMAIL: 'noreply@karibeo.com',
|
||||||
|
SENDGRID_FROM_NAME: 'Karibeo',
|
||||||
|
// WhatsApp Business API Configuration
|
||||||
|
WHATSAPP_API_URL: 'https://graph.facebook.com/v18.0/your_phone_number_id',
|
||||||
|
WHATSAPP_ACCESS_TOKEN: 'your_whatsapp_access_token',
|
||||||
|
WHATSAPP_VERIFY_TOKEN: 'your_webhook_verify_token'
|
||||||
|
},
|
||||||
|
env_production: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
// Database - Actualiza con valores de producción
|
||||||
|
DB_HOST: 'localhost', // Cambiar por tu host de producción
|
||||||
|
DB_PORT: '5432',
|
||||||
|
DB_USERNAME: 'karibeo',
|
||||||
|
DB_PASSWORD: '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',
|
||||||
|
DB_NAME: 'karibeo_db',
|
||||||
|
// JWT
|
||||||
|
JWT_SECRET: 'karibeo_jwt_secret_key_2025_very_secure',
|
||||||
|
JWT_EXPIRES_IN: '24h',
|
||||||
|
JWT_REFRESH_SECRET: 'karibeo_refresh_secret_key_2025',
|
||||||
|
JWT_REFRESH_EXPIRES_IN: '7d',
|
||||||
|
// App
|
||||||
|
APP_PORT: '3000',
|
||||||
|
APP_NAME: 'Karibeo API',
|
||||||
|
APP_VERSION: '1.0.0',
|
||||||
|
APP_DESCRIPTION: 'Integrated Tourism Applications System API',
|
||||||
|
// Throttle
|
||||||
|
THROTTLE_TTL: '60',
|
||||||
|
THROTTLE_LIMIT: '100',
|
||||||
|
// CORS - Actualizar con dominios de producción
|
||||||
|
CORS_ORIGINS: 'https://karibeo.com,https://app.karibeo.com',
|
||||||
|
// Stripe Configuration - Usar claves de producción
|
||||||
|
STRIPE_SECRET_KEY: 'sk_live_your_production_stripe_secret_key',
|
||||||
|
STRIPE_PUBLISHABLE_KEY: 'pk_live_your_production_stripe_publishable_key',
|
||||||
|
STRIPE_WEBHOOK_SECRET: 'whsec_your_production_webhook_secret',
|
||||||
|
// AWS S3 Configuration - Usar credenciales de producción
|
||||||
|
AWS_ACCESS_KEY_ID: 'your_production_aws_access_key_id',
|
||||||
|
AWS_SECRET_ACCESS_KEY: 'your_production_aws_secret_access_key',
|
||||||
|
AWS_REGION: 'us-east-1',
|
||||||
|
AWS_S3_BUCKET: 'karibeo-assets',
|
||||||
|
AWS_CLOUDFRONT_URL: 'https://d1234567890.cloudfront.net',
|
||||||
|
// SendGrid Configuration
|
||||||
|
SENDGRID_API_KEY: 'SG.your_production_sendgrid_api_key',
|
||||||
|
SENDGRID_FROM_EMAIL: 'noreply@karibeo.com',
|
||||||
|
SENDGRID_FROM_NAME: 'Karibeo',
|
||||||
|
// WhatsApp Business API Configuration
|
||||||
|
WHATSAPP_API_URL: 'https://graph.facebook.com/v18.0/your_production_phone_number_id',
|
||||||
|
WHATSAPP_ACCESS_TOKEN: 'your_production_whatsapp_access_token',
|
||||||
|
WHATSAPP_VERIFY_TOKEN: 'your_production_webhook_verify_token'
|
||||||
|
},
|
||||||
|
log_date_format: 'YYYY-MM-DD HH:mm Z',
|
||||||
|
error_file: './logs/pm2-error.log',
|
||||||
|
out_file: './logs/pm2-out.log',
|
||||||
|
log_file: './logs/pm2-combined.log'
|
||||||
|
}]
|
||||||
|
};
|
||||||
34
eslint.config.mjs
Executable file
34
eslint.config.mjs
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
// @ts-check
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['eslint.config.mjs'],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'warn'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
8
nest-cli.json
Executable file
8
nest-cli.json
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
15124
package-lock.json
generated
Executable file
15124
package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
104
package.json
Executable file
104
package.json
Executable file
@@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"name": "karibeo-api",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"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": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.835.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "^3.835.0",
|
||||||
|
"@nestjs/common": "^11.1.3",
|
||||||
|
"@nestjs/config": "^4.0.2",
|
||||||
|
"@nestjs/core": "^11.1.3",
|
||||||
|
"@nestjs/jwt": "^11.0.0",
|
||||||
|
"@nestjs/passport": "^11.0.5",
|
||||||
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/schedule": "^6.0.1",
|
||||||
|
"@nestjs/swagger": "^11.2.0",
|
||||||
|
"@nestjs/throttler": "^6.4.0",
|
||||||
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"@sendgrid/mail": "^8.1.5",
|
||||||
|
"axios": "^1.10.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.2",
|
||||||
|
"joi": "^17.13.3",
|
||||||
|
"multer": "^2.0.1",
|
||||||
|
"multer-s3": "^3.0.1",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"passport-local": "^1.0.0",
|
||||||
|
"pg": "^8.16.2",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"sharp": "^0.34.2",
|
||||||
|
"stripe": "^18.2.1",
|
||||||
|
"swagger-ui-express": "^5.0.1",
|
||||||
|
"typeorm": "^0.3.25",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@swc/cli": "^0.6.0",
|
||||||
|
"@swc/core": "^1.10.7",
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
|
"@types/multer": "^1.4.13",
|
||||||
|
"@types/node": "^22.15.33",
|
||||||
|
"@types/passport-jwt": "^4.0.1",
|
||||||
|
"@types/passport-local": "^1.0.38",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/app.controller.spec.ts
Executable file
22
src/app.controller.spec.ts
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
describe('AppController', () => {
|
||||||
|
let appController: AppController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
appController = app.get<AppController>(AppController);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('root', () => {
|
||||||
|
it('should return "Hello World!"', () => {
|
||||||
|
expect(appController.getHello()).toBe('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
12
src/app.controller.ts
Executable file
12
src/app.controller.ts
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
getHello(): string {
|
||||||
|
return this.appService.getHello();
|
||||||
|
}
|
||||||
|
}
|
||||||
232
src/app.module.ts
Executable file
232
src/app.module.ts
Executable file
@@ -0,0 +1,232 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ThrottlerModule } from '@nestjs/throttler';
|
||||||
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
|
import { ThrottlerGuard } from '@nestjs/throttler';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule'; // Importado para tareas programadas (ej. sincronización de canales)
|
||||||
|
|
||||||
|
// Config imports
|
||||||
|
import databaseConfig from './config/database.config';
|
||||||
|
import jwtConfig from './config/jwt.config';
|
||||||
|
import appConfig from './config/app.config';
|
||||||
|
import stripeConfig from './config/integrations/stripe.config';
|
||||||
|
import awsConfig from './config/integrations/aws.config';
|
||||||
|
import communicationConfig from './config/integrations/communication.config';
|
||||||
|
|
||||||
|
// Entity imports
|
||||||
|
import { User } from './entities/user.entity';
|
||||||
|
import { Country } from './entities/country.entity';
|
||||||
|
import { Language } from './entities/language.entity';
|
||||||
|
import { Role } from './entities/role.entity';
|
||||||
|
import { UserPreferences } from './entities/user-preferences.entity';
|
||||||
|
import { Destination } from './entities/destination.entity';
|
||||||
|
import { PlaceOfInterest } from './entities/place-of-interest.entity';
|
||||||
|
import { Establishment } from './entities/establishment.entity';
|
||||||
|
import { TourGuide } from './entities/tour-guide.entity';
|
||||||
|
import { TaxiDriver } from './entities/taxi-driver.entity';
|
||||||
|
import { SecurityOfficer } from './entities/security-officer.entity';
|
||||||
|
import { HotelRoom } from './entities/hotel-room.entity';
|
||||||
|
import { Product } from './entities/product.entity';
|
||||||
|
import { Reservation } from './entities/reservation.entity';
|
||||||
|
import { Transaction } from './entities/transaction.entity';
|
||||||
|
import { Notification } from './entities/notification.entity';
|
||||||
|
import { Incident } from './entities/incident.entity';
|
||||||
|
import { EmergencyAlert } from './entities/emergency-alert.entity';
|
||||||
|
import { Itinerary } from './entities/itinerary.entity';
|
||||||
|
// Restaurant entities
|
||||||
|
import { MenuItem } from './entities/menu-item.entity';
|
||||||
|
import { Table } from './entities/table.entity';
|
||||||
|
import { Order } from './entities/order.entity';
|
||||||
|
import { OrderItem } from './entities/order-item.entity';
|
||||||
|
// Hotel entities
|
||||||
|
import { HotelCheckin } from './entities/hotel-checkin.entity';
|
||||||
|
import { HotelService } from './entities/hotel-service.entity';
|
||||||
|
// AI/AR entities
|
||||||
|
import { AIGuideInteraction } from './entities/ai-guide-interaction.entity';
|
||||||
|
import { ARContent } from './entities/ar-content.entity';
|
||||||
|
// Geolocation entities
|
||||||
|
import { Geofence } from './entities/geofence.entity';
|
||||||
|
import { LocationTracking } from './entities/location-tracking.entity';
|
||||||
|
// Advanced Reviews entities
|
||||||
|
import { AdvancedReview } from './entities/advanced-review.entity';
|
||||||
|
import { ReviewHelpfulness } from './entities/review-helpfulness.entity';
|
||||||
|
// AI Generator entities
|
||||||
|
import { AIGeneratedContent } from './entities/ai-generated-content.entity';
|
||||||
|
// Personalization entities
|
||||||
|
import { UserPersonalization } from './entities/user-personalization.entity';
|
||||||
|
// Sustainability entities
|
||||||
|
import { SustainabilityTracking } from './entities/sustainability-tracking.entity';
|
||||||
|
import { EcoEstablishment } from './entities/eco-establishment.entity';
|
||||||
|
// Social Commerce entities
|
||||||
|
import { InfluencerProfile } from './entities/influencer-profile.entity';
|
||||||
|
import { CreatorCampaign } from './entities/creator-campaign.entity';
|
||||||
|
import { UGCContent } from './entities/ugc-content.entity';
|
||||||
|
// IoT Tourism entities
|
||||||
|
import { IoTDevice } from './entities/iot-device.entity';
|
||||||
|
import { SmartTourismData } from './entities/smart-tourism-data.entity';
|
||||||
|
import { WearableDevice } from './entities/wearable-device.entity';
|
||||||
|
// Finance entities
|
||||||
|
import { CommissionRate } from './entities/commission-rate.entity';
|
||||||
|
import { AdminTransaction } from './entities/admin-transaction.entity';
|
||||||
|
import { Settlement } from './entities/settlement.entity';
|
||||||
|
// NUEVAS Entidades para Channel Management, Listings, Vehicle, Flight, Availability
|
||||||
|
import { Channel } from './entities/channel.entity';
|
||||||
|
import { Listing } from './entities/listing.entity';
|
||||||
|
import { Vehicle } from './entities/vehicle.entity';
|
||||||
|
import { Flight } from './entities/flight.entity';
|
||||||
|
import { Availability } from './entities/availability.entity';
|
||||||
|
|
||||||
|
|
||||||
|
// Module imports
|
||||||
|
import { AuthModule } from './modules/auth/auth.module';
|
||||||
|
import { UsersModule } from './modules/users/users.module';
|
||||||
|
import { TourismModule } from './modules/tourism/tourism.module';
|
||||||
|
import { CommerceModule } from './modules/commerce/commerce.module';
|
||||||
|
import { SecurityModule } from './modules/security/security.module';
|
||||||
|
import { AnalyticsModule } from './modules/analytics/analytics.module';
|
||||||
|
import { NotificationsModule } from './modules/notifications/notifications.module';
|
||||||
|
import { PaymentsModule } from './modules/payments/payments.module';
|
||||||
|
import { UploadModule } from './modules/upload/upload.module';
|
||||||
|
import { CommunicationModule } from './modules/communication/communication.module';
|
||||||
|
import { RestaurantModule } from './modules/restaurant/restaurant.module';
|
||||||
|
import { HotelModule } from './modules/hotel/hotel.module';
|
||||||
|
import { AIGuideModule } from './modules/ai-guide/ai-guide.module';
|
||||||
|
import { GeolocationModule } from './modules/geolocation/geolocation.module';
|
||||||
|
import { ReviewsModule } from './modules/reviews/reviews.module';
|
||||||
|
import { AIGeneratorModule } from './modules/ai-generator/ai-generator.module';
|
||||||
|
import { PersonalizationModule } from './modules/personalization/personalization.module';
|
||||||
|
import { SustainabilityModule } from './modules/sustainability/sustainability.module';
|
||||||
|
import { SocialCommerceModule } from './modules/social-commerce/social-commerce.module';
|
||||||
|
import { IoTTourismModule } from './modules/iot-tourism/iot-tourism.module';
|
||||||
|
import { FinanceModule } from './modules/finance/finance.module';
|
||||||
|
// NUEVOS Módulos
|
||||||
|
import { ChannelManagementModule } from './modules/channel-management/channel-management.module';
|
||||||
|
import { ListingsModule } from './modules/listings/listings.module';
|
||||||
|
import { VehicleManagementModule } from './modules/vehicle-management/vehicle-management.module';
|
||||||
|
import { FlightManagementModule } from './modules/flight-management/flight-management.module';
|
||||||
|
import { AvailabilityManagementModule } from './modules/availability-management/availability-management.module';
|
||||||
|
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
// Configuration
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
load: [
|
||||||
|
databaseConfig,
|
||||||
|
jwtConfig,
|
||||||
|
appConfig,
|
||||||
|
stripeConfig,
|
||||||
|
awsConfig,
|
||||||
|
communicationConfig,
|
||||||
|
],
|
||||||
|
envFilePath: '.env',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Database
|
||||||
|
TypeOrmModule.forRootAsync({
|
||||||
|
useFactory: (configService: ConfigService) => ({
|
||||||
|
...configService.get('database'),
|
||||||
|
entities: [
|
||||||
|
User, Country, Language, Role, UserPreferences,
|
||||||
|
Destination, PlaceOfInterest, Establishment,
|
||||||
|
TourGuide, TaxiDriver, SecurityOfficer,
|
||||||
|
HotelRoom, Product, Reservation, Transaction,
|
||||||
|
Notification, Incident, EmergencyAlert, Itinerary,
|
||||||
|
// Restaurant entities
|
||||||
|
MenuItem, Table, Order, OrderItem,
|
||||||
|
// Hotel entities
|
||||||
|
HotelCheckin, HotelService,
|
||||||
|
// AI/AR entities
|
||||||
|
AIGuideInteraction, ARContent,
|
||||||
|
// Geolocation entities
|
||||||
|
Geofence, LocationTracking,
|
||||||
|
// Advanced Reviews entities
|
||||||
|
AdvancedReview, ReviewHelpfulness,
|
||||||
|
// AI Generator entities
|
||||||
|
AIGeneratedContent,
|
||||||
|
// Personalization entities
|
||||||
|
UserPersonalization,
|
||||||
|
// Sustainability entities
|
||||||
|
SustainabilityTracking, EcoEstablishment,
|
||||||
|
// Social Commerce entities
|
||||||
|
InfluencerProfile, CreatorCampaign, UGCContent,
|
||||||
|
// IoT Tourism entities
|
||||||
|
IoTDevice, SmartTourismData, WearableDevice,
|
||||||
|
// Finance entities
|
||||||
|
CommissionRate, AdminTransaction, Settlement,
|
||||||
|
// NUEVAS Entidades
|
||||||
|
Channel, Listing, Vehicle, Flight, Availability,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Rate Limiting
|
||||||
|
ThrottlerModule.forRootAsync({
|
||||||
|
useFactory: (configService: ConfigService) => ({
|
||||||
|
throttlers: [
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
ttl: configService.get<number>('app.throttle.ttl') || 60,
|
||||||
|
limit: configService.get<number>('app.throttle.limit') || 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Schedule Module para tareas cron (ej. sincronización de canales)
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// TODOS LOS MÓDULOS - AHORA 26 MÓDULOS TOTALES
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// Core modules (4)
|
||||||
|
AuthModule,
|
||||||
|
UsersModule,
|
||||||
|
AnalyticsModule,
|
||||||
|
NotificationsModule,
|
||||||
|
|
||||||
|
// Business & Operations modules (9) - Actualizado con nuevos módulos
|
||||||
|
TourismModule,
|
||||||
|
CommerceModule,
|
||||||
|
SecurityModule,
|
||||||
|
FinanceModule,
|
||||||
|
RestaurantModule,
|
||||||
|
HotelModule,
|
||||||
|
ChannelManagementModule,
|
||||||
|
ListingsModule,
|
||||||
|
AvailabilityManagementModule,
|
||||||
|
|
||||||
|
// Integration modules (3)
|
||||||
|
PaymentsModule,
|
||||||
|
UploadModule,
|
||||||
|
CommunicationModule,
|
||||||
|
|
||||||
|
// Advanced features modules (3)
|
||||||
|
AIGuideModule,
|
||||||
|
GeolocationModule,
|
||||||
|
ReviewsModule,
|
||||||
|
|
||||||
|
// Logistics & Booking Modules (2) - Nueva categoría
|
||||||
|
VehicleManagementModule,
|
||||||
|
FlightManagementModule,
|
||||||
|
|
||||||
|
// Innovation 2025 modules (5)
|
||||||
|
AIGeneratorModule,
|
||||||
|
PersonalizationModule,
|
||||||
|
SustainabilityModule,
|
||||||
|
SocialCommerceModule,
|
||||||
|
IoTTourismModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_GUARD,
|
||||||
|
useClass: ThrottlerGuard,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
8
src/app.service.ts
Executable file
8
src/app.service.ts
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
getHello(): string {
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/common/decorators/roles.decorator.ts
Executable file
4
src/common/decorators/roles.decorator.ts
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const ROLES_KEY = 'roles';
|
||||||
|
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||||
5
src/common/guards/jwt-auth.guard.ts
Executable file
5
src/common/guards/jwt-auth.guard.ts
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||||
32
src/common/guards/roles.guard.ts
Executable file
32
src/common/guards/roles.guard.ts
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { ROLES_KEY } from '../decorators/roles.decorator';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RolesGuard implements CanActivate {
|
||||||
|
constructor(private reflector: Reflector) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!requiredRoles) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
if (!user || !user.role) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Super admin tiene acceso a todo automáticamente
|
||||||
|
if (user.role.name === 'super_admin') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requiredRoles.some((role) => user.role?.name === role);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/config/app.config.ts
Executable file
16
src/config/app.config.ts
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
import stripeConfig from './integrations/stripe.config';
|
||||||
|
import awsConfig from './integrations/aws.config';
|
||||||
|
import communicationConfig from './integrations/communication.config';
|
||||||
|
|
||||||
|
export default registerAs('app', () => ({
|
||||||
|
port: parseInt(process.env.APP_PORT || '3000', 10),
|
||||||
|
name: process.env.APP_NAME || 'Karibeo API',
|
||||||
|
version: process.env.APP_VERSION || '1.0.0',
|
||||||
|
description: process.env.APP_DESCRIPTION || 'Tourism API',
|
||||||
|
corsOrigins: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3000'],
|
||||||
|
throttle: {
|
||||||
|
ttl: parseInt(process.env.THROTTLE_TTL || '60', 10),
|
||||||
|
limit: parseInt(process.env.THROTTLE_LIMIT || '100', 10),
|
||||||
|
},
|
||||||
|
}));
|
||||||
20
src/config/database.config.ts
Executable file
20
src/config/database.config.ts
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
export default registerAs('database', (): TypeOrmModuleOptions => ({
|
||||||
|
type: 'postgres',
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_PORT || '5432', 10),
|
||||||
|
username: process.env.DB_USERNAME,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
|
||||||
|
synchronize: false, // Never use in production
|
||||||
|
logging: process.env.NODE_ENV === 'development',
|
||||||
|
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
||||||
|
extra: {
|
||||||
|
max: 100, // Maximum connections
|
||||||
|
connectionTimeoutMillis: 30000,
|
||||||
|
idleTimeoutMillis: 30000,
|
||||||
|
},
|
||||||
|
}));
|
||||||
11
src/config/integrations/aws.config.ts
Executable file
11
src/config/integrations/aws.config.ts
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('aws', () => ({
|
||||||
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||||
|
region: process.env.AWS_REGION || 'us-east-1',
|
||||||
|
s3: {
|
||||||
|
bucket: process.env.AWS_S3_BUCKET,
|
||||||
|
cloudfrontUrl: process.env.AWS_CLOUDFRONT_URL,
|
||||||
|
},
|
||||||
|
}));
|
||||||
21
src/config/integrations/communication.config.ts
Executable file
21
src/config/integrations/communication.config.ts
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('communication', () => ({
|
||||||
|
sendgrid: {
|
||||||
|
apiKey: process.env.SENDGRID_API_KEY,
|
||||||
|
fromEmail: process.env.SENDGRID_FROM_EMAIL,
|
||||||
|
fromName: process.env.SENDGRID_FROM_NAME,
|
||||||
|
},
|
||||||
|
whatsapp: {
|
||||||
|
apiUrl: process.env.WHATSAPP_API_URL,
|
||||||
|
accessToken: process.env.WHATSAPP_ACCESS_TOKEN,
|
||||||
|
verifyToken: process.env.WHATSAPP_VERIFY_TOKEN,
|
||||||
|
},
|
||||||
|
googleMaps: {
|
||||||
|
apiKey: process.env.GOOGLE_MAPS_API_KEY,
|
||||||
|
},
|
||||||
|
firebase: {
|
||||||
|
serverKey: process.env.FIREBASE_SERVER_KEY,
|
||||||
|
projectId: process.env.FIREBASE_PROJECT_ID,
|
||||||
|
},
|
||||||
|
}));
|
||||||
9
src/config/integrations/stripe.config.ts
Executable file
9
src/config/integrations/stripe.config.ts
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('stripe', () => ({
|
||||||
|
secretKey: process.env.STRIPE_SECRET_KEY,
|
||||||
|
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
|
||||||
|
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
||||||
|
currency: 'usd',
|
||||||
|
apiVersion: '2023-10-16' as const,
|
||||||
|
}));
|
||||||
9
src/config/jwt.config.ts
Executable file
9
src/config/jwt.config.ts
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
import { JwtModuleOptions } from '@nestjs/jwt';
|
||||||
|
|
||||||
|
export default registerAs('jwt', (): JwtModuleOptions => ({
|
||||||
|
secret: process.env.JWT_SECRET,
|
||||||
|
signOptions: {
|
||||||
|
expiresIn: process.env.JWT_EXPIRES_IN || '24h',
|
||||||
|
},
|
||||||
|
}));
|
||||||
52
src/entities/admin-transaction.entity.ts
Normal file
52
src/entities/admin-transaction.entity.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ schema: 'finance', name: 'admin_transactions' })
|
||||||
|
export class AdminTransaction {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'original_transaction_id', type: 'uuid', nullable: true })
|
||||||
|
originalTransactionId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'merchant_id', type: 'uuid' })
|
||||||
|
merchantId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'service_type', length: 50 })
|
||||||
|
serviceType: string;
|
||||||
|
|
||||||
|
@Column({ name: 'gross_amount', type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
grossAmount: number;
|
||||||
|
|
||||||
|
@Column({ name: 'commission_rate', type: 'decimal', precision: 5, scale: 2 })
|
||||||
|
commissionRate: number;
|
||||||
|
|
||||||
|
@Column({ name: 'commission_amount', type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
commissionAmount: number;
|
||||||
|
|
||||||
|
@Column({ name: 'net_amount', type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
netAmount: number;
|
||||||
|
|
||||||
|
@Column({ length: 3, default: 'USD' })
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
@Column({ length: 20, default: 'pending' })
|
||||||
|
status: string; // pending, settled, refunded
|
||||||
|
|
||||||
|
@Column({ name: 'payment_intent_id', length: 255, nullable: true })
|
||||||
|
paymentIntentId: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'merchant_id' })
|
||||||
|
merchant: User;
|
||||||
|
}
|
||||||
112
src/entities/advanced-review.entity.ts
Normal file
112
src/entities/advanced-review.entity.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'advanced_reviews', schema: 'analytics' })
|
||||||
|
export class AdvancedReview extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id' })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Reviewable type', example: 'establishment' })
|
||||||
|
@Column({ name: 'reviewable_type', length: 30 })
|
||||||
|
reviewableType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Reviewable ID' })
|
||||||
|
@Column({ name: 'reviewable_id' })
|
||||||
|
reviewableId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Overall rating (1-5)', example: 5 })
|
||||||
|
@Column({ type: 'integer' })
|
||||||
|
overallRating: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Detailed ratings by category' })
|
||||||
|
@Column({ name: 'detailed_ratings', type: 'jsonb', nullable: true })
|
||||||
|
detailedRatings: Record<string, number> | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Review title', example: 'Amazing experience!' })
|
||||||
|
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||||
|
title: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Review comment' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
comment: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Review pros' })
|
||||||
|
@Column({ type: 'text', array: true, nullable: true })
|
||||||
|
pros: string[] | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Review cons' })
|
||||||
|
@Column({ type: 'text', array: true, nullable: true })
|
||||||
|
cons: string[] | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Review images and videos' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
media: Record<string, any>[] | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Visit date' })
|
||||||
|
@Column({ name: 'visit_date', type: 'date', nullable: true })
|
||||||
|
visitDate: Date | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Travel type', example: 'solo' })
|
||||||
|
@Column({ name: 'travel_type', type: 'varchar', length: 30, nullable: true })
|
||||||
|
travelType: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Visit purpose', example: 'leisure' })
|
||||||
|
@Column({ name: 'visit_purpose', type: 'varchar', length: 30, nullable: true })
|
||||||
|
visitPurpose: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Recommended for', example: 'couples' })
|
||||||
|
@Column({ name: 'recommended_for', type: 'text', array: true, nullable: true })
|
||||||
|
recommendedFor: string[] | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Language of review', example: 'en' })
|
||||||
|
@Column({ length: 5, default: 'en' })
|
||||||
|
language: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is verified review', example: false })
|
||||||
|
@Column({ name: 'is_verified', default: false })
|
||||||
|
isVerified: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Verification method' })
|
||||||
|
@Column({ name: 'verification_method', type: 'varchar', length: 50, nullable: true })
|
||||||
|
verificationMethod: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Helpful count', example: 15 })
|
||||||
|
@Column({ name: 'helpful_count', default: 0 })
|
||||||
|
helpfulCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Unhelpful count', example: 2 })
|
||||||
|
@Column({ name: 'unhelpful_count', default: 0 })
|
||||||
|
unhelpfulCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Sentiment analysis score (-1 to 1)' })
|
||||||
|
@Column({ name: 'sentiment_score', type: 'decimal', precision: 3, scale: 2, nullable: true })
|
||||||
|
sentimentScore: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'AI-generated tags' })
|
||||||
|
@Column({ name: 'ai_tags', type: 'text', array: true, nullable: true })
|
||||||
|
aiTags: string[] | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Response from establishment' })
|
||||||
|
@Column({ name: 'establishment_response', type: 'text', nullable: true })
|
||||||
|
establishmentResponse: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Response date' })
|
||||||
|
@Column({ name: 'response_date', type: 'timestamp', nullable: true })
|
||||||
|
responseDate: Date | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Review source', example: 'app' })
|
||||||
|
@Column({ length: 20, default: 'app' })
|
||||||
|
source: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is featured review', example: false })
|
||||||
|
@Column({ name: 'is_featured', default: false })
|
||||||
|
isFeatured: boolean;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
52
src/entities/ai-generated-content.entity.ts
Normal file
52
src/entities/ai-generated-content.entity.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'ai_generated_content', schema: 'analytics' })
|
||||||
|
export class AIGeneratedContent extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID who requested content' })
|
||||||
|
@Column({ name: 'user_id' })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Content type', example: 'itinerary' })
|
||||||
|
@Column({ name: 'content_type', length: 50 })
|
||||||
|
contentType: string; // itinerary, blog-post, guide, recommendation, description
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'User prompt/request' })
|
||||||
|
@Column({ name: 'user_prompt', type: 'text' })
|
||||||
|
userPrompt: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Generated content' })
|
||||||
|
@Column({ name: 'generated_content', type: 'text' })
|
||||||
|
generatedContent: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'AI model used' })
|
||||||
|
@Column({ name: 'ai_model', length: 100 })
|
||||||
|
aiModel: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Content language', example: 'es' })
|
||||||
|
@Column({ length: 5, default: 'es' })
|
||||||
|
language: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'User rating of content quality' })
|
||||||
|
@Column({ name: 'quality_rating', nullable: true })
|
||||||
|
qualityRating: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Content metadata and parameters' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Usage count' })
|
||||||
|
@Column({ name: 'usage_count', default: 0 })
|
||||||
|
usageCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is content approved/published' })
|
||||||
|
@Column({ name: 'is_approved', default: false })
|
||||||
|
isApproved: boolean;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
57
src/entities/ai-guide-interaction.entity.ts
Normal file
57
src/entities/ai-guide-interaction.entity.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
import { PlaceOfInterest } from './place-of-interest.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'ai_guide_interactions', schema: 'analytics' })
|
||||||
|
export class AIGuideInteraction extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id' })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Place of interest ID' })
|
||||||
|
@Column({ name: 'place_id', type: 'uuid', nullable: true })
|
||||||
|
placeId: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'User query/question' })
|
||||||
|
@Column({ name: 'user_query', type: 'text' })
|
||||||
|
userQuery: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'AI response' })
|
||||||
|
@Column({ name: 'ai_response', type: 'text' })
|
||||||
|
aiResponse: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'User location at time of interaction' })
|
||||||
|
@Column({ name: 'user_location', type: 'point', nullable: true })
|
||||||
|
userLocation: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Interaction type', example: 'monument-recognition' })
|
||||||
|
@Column({ name: 'interaction_type', length: 50 })
|
||||||
|
interactionType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Language used', example: 'en' })
|
||||||
|
@Column({ length: 5, default: 'en' })
|
||||||
|
language: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Session ID for conversation context' })
|
||||||
|
@Column({ name: 'session_id', length: 100 })
|
||||||
|
sessionId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'User satisfaction rating' })
|
||||||
|
@Column({ type: 'integer', nullable: true })
|
||||||
|
rating: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Additional metadata' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
metadata: Record<string, any> | null;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => PlaceOfInterest)
|
||||||
|
@JoinColumn({ name: 'place_id' })
|
||||||
|
place: PlaceOfInterest;
|
||||||
|
}
|
||||||
68
src/entities/ar-content.entity.ts
Normal file
68
src/entities/ar-content.entity.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { PlaceOfInterest } from './place-of-interest.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'ar_content', schema: 'tourism' })
|
||||||
|
export class ARContent extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Place of interest ID' })
|
||||||
|
@Column({ name: 'place_id' })
|
||||||
|
placeId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'AR content title', example: 'Historic Alcázar de Colón' })
|
||||||
|
@Column({ length: 255 })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Content description' })
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'AR content type', example: '3d-model' })
|
||||||
|
@Column({ name: 'content_type', length: 50 })
|
||||||
|
contentType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Content file URL' })
|
||||||
|
@Column({ name: 'content_url', type: 'text' })
|
||||||
|
contentUrl: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Thumbnail image URL' })
|
||||||
|
@Column({ name: 'thumbnail_url', type: 'text', nullable: true })
|
||||||
|
thumbnailUrl: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Trigger coordinates for AR activation' })
|
||||||
|
@Column({ name: 'trigger_coordinates', type: 'point' })
|
||||||
|
triggerCoordinates: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Trigger radius in meters', example: 50 })
|
||||||
|
@Column({ name: 'trigger_radius', type: 'decimal', precision: 8, scale: 2 })
|
||||||
|
triggerRadius: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Languages available' })
|
||||||
|
@Column({ type: 'text', array: true })
|
||||||
|
languages: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Historical period', example: '1492-1520' })
|
||||||
|
@Column({ name: 'historical_period', type: 'varchar', length: 100, nullable: true })
|
||||||
|
historicalPeriod: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'AR tracking markers' })
|
||||||
|
@Column({ name: 'tracking_data', type: 'jsonb', nullable: true })
|
||||||
|
trackingData: Record<string, any> | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Content metadata' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
metadata: Record<string, any> | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is active', example: true })
|
||||||
|
@Column({ name: 'is_active', default: true })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Views count' })
|
||||||
|
@Column({ name: 'views_count', default: 0 })
|
||||||
|
viewsCount: number;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => PlaceOfInterest)
|
||||||
|
@JoinColumn({ name: 'place_id' })
|
||||||
|
place: PlaceOfInterest;
|
||||||
|
}
|
||||||
68
src/entities/availability.entity.ts
Normal file
68
src/entities/availability.entity.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { Entity, Column, Index, Unique } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'availability', schema: 'commerce' })
|
||||||
|
@Unique(['resourceType', 'resourceId', 'date'])
|
||||||
|
@Index(['resourceType', 'resourceId', 'date'])
|
||||||
|
export class Availability extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Resource type', example: 'hotel' })
|
||||||
|
@Column({ name: 'resource_type', length: 50 })
|
||||||
|
resourceType: string; // hotel, restaurant, vehicle, room, table
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Resource ID (hotel, restaurant, vehicle, etc.)' })
|
||||||
|
@Column({ name: 'resource_id' })
|
||||||
|
resourceId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Available date' })
|
||||||
|
@Column({ type: 'date' })
|
||||||
|
date: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Available quantity', example: 5 })
|
||||||
|
@Column({ name: 'available_quantity' })
|
||||||
|
availableQuantity: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total quantity', example: 10 })
|
||||||
|
@Column({ name: 'total_quantity' })
|
||||||
|
totalQuantity: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Booked quantity', example: 3 })
|
||||||
|
@Column({ name: 'booked_quantity', default: 0 })
|
||||||
|
bookedQuantity: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Blocked quantity', example: 1 })
|
||||||
|
@Column({ name: 'blocked_quantity', default: 0 })
|
||||||
|
blockedQuantity: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Base price for this date' })
|
||||||
|
@Column({ name: 'base_price', type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
basePrice: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Dynamic price adjustments' })
|
||||||
|
@Column({ name: 'price_modifiers', type: 'jsonb', nullable: true })
|
||||||
|
priceModifiers: Record<string, any> | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Final calculated price' })
|
||||||
|
@Column({ name: 'final_price', type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
finalPrice: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Minimum stay requirement', example: 1 })
|
||||||
|
@Column({ name: 'min_stay', default: 1 })
|
||||||
|
minStay: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Special restrictions or notes' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
restrictions: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is available for booking', example: true })
|
||||||
|
@Column({ name: 'is_available', default: true })
|
||||||
|
isAvailable: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Availability status', example: 'open' })
|
||||||
|
@Column({ length: 20, default: 'open' })
|
||||||
|
status: string; // open, closed, limited, sold-out
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Last updated by channel sync' })
|
||||||
|
@Column({ name: 'last_synced', type: 'timestamp', nullable: true })
|
||||||
|
lastSynced: Date | null;
|
||||||
|
}
|
||||||
29
src/entities/base.entity.ts
Executable file
29
src/entities/base.entity.ts
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
import {
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn
|
||||||
|
} from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export abstract class BaseEntity {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Unique identifier',
|
||||||
|
example: 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
|
||||||
|
})
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Creation timestamp',
|
||||||
|
example: '2025-06-24T17:30:00Z'
|
||||||
|
})
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Last update timestamp',
|
||||||
|
example: '2025-06-24T17:35:00Z'
|
||||||
|
})
|
||||||
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
54
src/entities/channel.entity.ts
Normal file
54
src/entities/channel.entity.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { Entity, Column } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'channels', schema: 'commerce' })
|
||||||
|
export class Channel extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Channel name', example: 'Booking.com' })
|
||||||
|
@Column({ length: 100 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Channel type', example: 'hotel' })
|
||||||
|
@Column({ length: 50 })
|
||||||
|
type: string; // hotel, restaurant, vehicle, flight
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Channel provider', example: 'booking' })
|
||||||
|
@Column({ length: 50 })
|
||||||
|
provider: string; // booking, expedia, airbnb, opentable, etc.
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Connection status', example: 'connected' })
|
||||||
|
@Column({ length: 20, default: 'disconnected' })
|
||||||
|
status: string; // connected, disconnected, error, syncing
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'API credentials (encrypted)' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
credentials: Record<string, any> | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Channel configuration' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
config: Record<string, any> | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Last sync timestamp' })
|
||||||
|
@Column({ name: 'last_sync', type: 'timestamp', nullable: true })
|
||||||
|
lastSync: Date | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Sync frequency in hours', example: 24 })
|
||||||
|
@Column({ name: 'sync_frequency', default: 24 })
|
||||||
|
syncFrequency: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Auto sync enabled', example: true })
|
||||||
|
@Column({ name: 'auto_sync', default: true })
|
||||||
|
autoSync: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Properties synchronized' })
|
||||||
|
@Column({ name: 'properties_synced', default: 0 })
|
||||||
|
propertiesSynced: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Last error message' })
|
||||||
|
@Column({ name: 'last_error', type: 'text', nullable: true })
|
||||||
|
lastError: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is active', example: true })
|
||||||
|
@Column({ name: 'is_active', default: true })
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
38
src/entities/commission-rate.entity.ts
Normal file
38
src/entities/commission-rate.entity.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ schema: 'finance', name: 'commission_rates' })
|
||||||
|
export class CommissionRate {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ name: 'service_type', length: 50 })
|
||||||
|
serviceType: string;
|
||||||
|
|
||||||
|
@Column({ name: 'commission_percentage', type: 'decimal', precision: 5, scale: 2 })
|
||||||
|
commissionPercentage: number;
|
||||||
|
|
||||||
|
@Column({ default: true })
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
|
||||||
|
updatedBy: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, { nullable: true })
|
||||||
|
@JoinColumn({ name: 'updated_by' })
|
||||||
|
updatedByUser: User;
|
||||||
|
}
|
||||||
37
src/entities/country.entity.ts
Executable file
37
src/entities/country.entity.ts
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'countries', schema: 'auth' })
|
||||||
|
export class Country {
|
||||||
|
@ApiProperty({ description: 'Country ID', example: 1 })
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Country name', example: 'Dominican Republic' })
|
||||||
|
@Column({ length: 100 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Country code', example: 'DOM' })
|
||||||
|
@Column({ length: 3, unique: true })
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Phone code', example: '+1809' })
|
||||||
|
@Column({ name: 'phone_code', length: 10, nullable: true })
|
||||||
|
phoneCode: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Currency code', example: 'DOP' })
|
||||||
|
@Column({ length: 3, nullable: true })
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Active status', example: true })
|
||||||
|
@Column({ default: true })
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Creation date' })
|
||||||
|
@Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@OneToMany(() => User, user => user.country)
|
||||||
|
users: User[];
|
||||||
|
}
|
||||||
117
src/entities/creator-campaign.entity.ts
Normal file
117
src/entities/creator-campaign.entity.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
import { InfluencerProfile } from './influencer-profile.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'creator_campaigns', schema: 'social_commerce' })
|
||||||
|
export class CreatorCampaign extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Campaign title' })
|
||||||
|
@Column({ length: 200 })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Campaign description' })
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Client/Brand user ID' })
|
||||||
|
@Column({ name: 'client_id' })
|
||||||
|
clientId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Selected influencer ID' })
|
||||||
|
@Column({ name: 'influencer_id', nullable: true })
|
||||||
|
influencerId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Campaign type' })
|
||||||
|
@Column({ name: 'campaign_type', length: 50 })
|
||||||
|
campaignType: string; // sponsored-post, story, reel, live-stream, tour-guide, ugc
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Campaign status' })
|
||||||
|
@Column({ length: 20, default: 'draft' })
|
||||||
|
status: string; // draft, open, in-progress, completed, cancelled
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Budget and compensation' })
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
budget: {
|
||||||
|
totalBudget: number;
|
||||||
|
influencerFee: number;
|
||||||
|
platformFee: number;
|
||||||
|
additionalCosts: number;
|
||||||
|
currency: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Campaign requirements' })
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
requirements: {
|
||||||
|
platforms: string[];
|
||||||
|
contentTypes: string[];
|
||||||
|
minimumFollowers: number;
|
||||||
|
minimumEngagement: number;
|
||||||
|
demographics: Record<string, any>;
|
||||||
|
deliverables: Array<{
|
||||||
|
type: string;
|
||||||
|
quantity: number;
|
||||||
|
deadline: string;
|
||||||
|
specifications: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Target audience and goals' })
|
||||||
|
@Column({ name: 'target_audience', type: 'jsonb' })
|
||||||
|
targetAudience: {
|
||||||
|
ageRange: { min: number; max: number };
|
||||||
|
gender: string[];
|
||||||
|
location: string[];
|
||||||
|
interests: string[];
|
||||||
|
languages: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Campaign timeline' })
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
timeline: {
|
||||||
|
applicationDeadline: Date;
|
||||||
|
campaignStart: Date;
|
||||||
|
campaignEnd: Date;
|
||||||
|
contentDeadlines: Array<{
|
||||||
|
deliverable: string;
|
||||||
|
deadline: Date;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Content guidelines and brand requirements' })
|
||||||
|
@Column({ name: 'content_guidelines', type: 'jsonb' })
|
||||||
|
contentGuidelines: {
|
||||||
|
brandGuidelines: string;
|
||||||
|
hashtags: string[];
|
||||||
|
mentions: string[];
|
||||||
|
doNotUse: string[];
|
||||||
|
contentStyle: string;
|
||||||
|
brandValues: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Performance tracking' })
|
||||||
|
@Column({ name: 'performance_tracking', type: 'jsonb', nullable: true })
|
||||||
|
performanceTracking: {
|
||||||
|
expectedMetrics: Record<string, number>;
|
||||||
|
actualMetrics: Record<string, number>;
|
||||||
|
roi: number;
|
||||||
|
contentUrls: string[];
|
||||||
|
engagementData: Array<{
|
||||||
|
platform: string;
|
||||||
|
postUrl: string;
|
||||||
|
likes: number;
|
||||||
|
comments: number;
|
||||||
|
shares: number;
|
||||||
|
views: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'client_id' })
|
||||||
|
client: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => InfluencerProfile)
|
||||||
|
@JoinColumn({ name: 'influencer_id' })
|
||||||
|
influencer: InfluencerProfile;
|
||||||
|
}
|
||||||
47
src/entities/destination.entity.ts
Executable file
47
src/entities/destination.entity.ts
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Country } from './country.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'destinations', schema: 'tourism' })
|
||||||
|
export class Destination {
|
||||||
|
@ApiProperty({ description: 'Destination ID', example: 1 })
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Country ID', example: 1 })
|
||||||
|
@Column({ name: 'country_id', nullable: true })
|
||||||
|
countryId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Destination name', example: 'Santo Domingo' })
|
||||||
|
@Column({ length: 255 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Destination description' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Category', example: 'city' })
|
||||||
|
@Column({ length: 50, nullable: true })
|
||||||
|
category: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Coordinates (lat, lng)' })
|
||||||
|
@Column({ type: 'point', nullable: true })
|
||||||
|
coordinates: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Destination images' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
images: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Active status', example: true })
|
||||||
|
@Column({ default: true })
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Creation date' })
|
||||||
|
@Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => Country)
|
||||||
|
@JoinColumn({ name: 'country_id' })
|
||||||
|
country: Country;
|
||||||
|
}
|
||||||
110
src/entities/eco-establishment.entity.ts
Normal file
110
src/entities/eco-establishment.entity.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Entity, Column, OneToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { Establishment } from './establishment.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'eco_establishments', schema: 'sustainability' })
|
||||||
|
export class EcoEstablishment extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Establishment ID' })
|
||||||
|
@Column({ name: 'establishment_id', unique: true })
|
||||||
|
establishmentId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Overall sustainability rating (0-100)' })
|
||||||
|
@Column({ name: 'sustainability_rating', type: 'decimal', precision: 5, scale: 2 })
|
||||||
|
sustainabilityRating: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Green certifications' })
|
||||||
|
@Column({ name: 'green_certifications', type: 'text', array: true })
|
||||||
|
greenCertifications: string[]; // LEED, Green Key, ISO 14001, etc.
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Energy efficiency measures' })
|
||||||
|
@Column({ name: 'energy_measures', type: 'jsonb' })
|
||||||
|
energyMeasures: {
|
||||||
|
solarPanels: boolean;
|
||||||
|
ledLighting: boolean;
|
||||||
|
energyEfficientAppliances: boolean;
|
||||||
|
smartThermostat: boolean;
|
||||||
|
renewableEnergy: boolean;
|
||||||
|
energySavingPercentage: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Water conservation practices' })
|
||||||
|
@Column({ name: 'water_conservation', type: 'jsonb' })
|
||||||
|
waterConservation: {
|
||||||
|
lowFlowFixtures: boolean;
|
||||||
|
rainwaterHarvesting: boolean;
|
||||||
|
grayWaterRecycling: boolean;
|
||||||
|
droughtResistantLandscaping: boolean;
|
||||||
|
waterSavingPercentage: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Waste management practices' })
|
||||||
|
@Column({ name: 'waste_management', type: 'jsonb' })
|
||||||
|
wasteManagement: {
|
||||||
|
recyclingProgram: boolean;
|
||||||
|
composting: boolean;
|
||||||
|
wasteReduction: boolean;
|
||||||
|
singleUsePlasticElimination: boolean;
|
||||||
|
wasteReductionPercentage: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Local community support' })
|
||||||
|
@Column({ name: 'community_support', type: 'jsonb' })
|
||||||
|
communitySupport: {
|
||||||
|
localEmployment: boolean;
|
||||||
|
localSourcing: boolean;
|
||||||
|
communityProjects: boolean;
|
||||||
|
culturalPreservation: boolean;
|
||||||
|
localEmploymentPercentage: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Biodiversity and wildlife protection' })
|
||||||
|
@Column({ name: 'biodiversity_protection', type: 'jsonb' })
|
||||||
|
biodiversityProtection: {
|
||||||
|
wildlifeConservation: boolean;
|
||||||
|
nativePlantLandscaping: boolean;
|
||||||
|
habitatProtection: boolean;
|
||||||
|
marineCare: boolean;
|
||||||
|
conservationPartnerships: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Carbon footprint data' })
|
||||||
|
@Column({ name: 'carbon_footprint', type: 'jsonb' })
|
||||||
|
carbonFootprint: {
|
||||||
|
annualEmissionsTons: number;
|
||||||
|
emissionsPerGuest: number;
|
||||||
|
carbonNeutralGoal: string;
|
||||||
|
offsetPrograms: string[];
|
||||||
|
reductionTargets: { year: number; targetReduction: number }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Sustainable practices description' })
|
||||||
|
@Column({ name: 'practices_description', type: 'text' })
|
||||||
|
practicesDescription: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Last sustainability audit date' })
|
||||||
|
@Column({ name: 'last_audit_date', type: 'date' })
|
||||||
|
lastAuditDate: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Next audit scheduled date' })
|
||||||
|
@Column({ name: 'next_audit_date', type: 'date' })
|
||||||
|
nextAuditDate: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Sustainability goals and commitments' })
|
||||||
|
@Column({ name: 'sustainability_goals', type: 'jsonb' })
|
||||||
|
sustainabilityGoals: {
|
||||||
|
shortTerm: string[];
|
||||||
|
longTerm: string[];
|
||||||
|
measurableTargets: Array<{
|
||||||
|
metric: string;
|
||||||
|
currentValue: number;
|
||||||
|
targetValue: number;
|
||||||
|
targetDate: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@OneToOne(() => Establishment)
|
||||||
|
@JoinColumn({ name: 'establishment_id' })
|
||||||
|
establishment: Establishment;
|
||||||
|
}
|
||||||
36
src/entities/emergency-alert.entity.ts
Executable file
36
src/entities/emergency-alert.entity.ts
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'emergency_alerts', schema: 'security' })
|
||||||
|
export class EmergencyAlert extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id', nullable: true })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Alert location' })
|
||||||
|
@Column({ type: 'point' })
|
||||||
|
location: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Address' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
address: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Alert type', example: 'medical' })
|
||||||
|
@Column({ length: 30, default: 'general' })
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Alert message' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is active', example: true })
|
||||||
|
@Column({ name: 'is_active', default: true })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
80
src/entities/establishment.entity.ts
Executable file
80
src/entities/establishment.entity.ts
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'establishments', schema: 'commerce' })
|
||||||
|
export class Establishment extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Owner user ID' })
|
||||||
|
@Column({ name: 'user_id', nullable: true })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Establishment type', example: 'restaurant' })
|
||||||
|
@Column({ length: 20 })
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Establishment name', example: 'La Casita Restaurant' })
|
||||||
|
@Column({ length: 255 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Description' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Category', example: 'caribbean-cuisine' })
|
||||||
|
@Column({ length: 50, nullable: true })
|
||||||
|
category: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Address' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
address: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Coordinates (lat, lng)' })
|
||||||
|
@Column({ type: 'point', nullable: true })
|
||||||
|
coordinates: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Phone number' })
|
||||||
|
@Column({ length: 20, nullable: true })
|
||||||
|
phone: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Email' })
|
||||||
|
@Column({ length: 255, nullable: true })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Website' })
|
||||||
|
@Column({ length: 255, nullable: true })
|
||||||
|
website: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Business hours' })
|
||||||
|
@Column({ name: 'business_hours', type: 'jsonb', nullable: true })
|
||||||
|
businessHours: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Images' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
images: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Amenities' })
|
||||||
|
@Column({ type: 'text', array: true, nullable: true })
|
||||||
|
amenities: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Average rating', example: 4.3 })
|
||||||
|
@Column({ type: 'decimal', precision: 3, scale: 2, nullable: true })
|
||||||
|
rating: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total reviews' })
|
||||||
|
@Column({ name: 'total_reviews', default: 0 })
|
||||||
|
totalReviews: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is verified', example: false })
|
||||||
|
@Column({ name: 'is_verified', default: false })
|
||||||
|
isVerified: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is active', example: true })
|
||||||
|
@Column({ name: 'is_active', default: true })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
owner: User;
|
||||||
|
}
|
||||||
98
src/entities/flight.entity.ts
Normal file
98
src/entities/flight.entity.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { Entity, Column } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'flights', schema: 'tourism' })
|
||||||
|
export class Flight extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Airline code', example: 'AA' })
|
||||||
|
@Column({ name: 'airline_code', length: 3 })
|
||||||
|
airlineCode: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Airline name', example: 'American Airlines' })
|
||||||
|
@Column({ name: 'airline_name', length: 100 })
|
||||||
|
airlineName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Flight number', example: 'AA1234' })
|
||||||
|
@Column({ name: 'flight_number', length: 10 })
|
||||||
|
flightNumber: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Origin airport code', example: 'JFK' })
|
||||||
|
@Column({ name: 'origin_code', length: 3 })
|
||||||
|
originCode: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Origin airport name', example: 'John F. Kennedy International Airport' })
|
||||||
|
@Column({ name: 'origin_name', length: 255 })
|
||||||
|
originName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Origin city', example: 'New York' })
|
||||||
|
@Column({ name: 'origin_city', length: 100 })
|
||||||
|
originCity: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Destination airport code', example: 'SDQ' })
|
||||||
|
@Column({ name: 'destination_code', length: 3 })
|
||||||
|
destinationCode: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Destination airport name', example: 'Las Américas International Airport' })
|
||||||
|
@Column({ name: 'destination_name', length: 255 })
|
||||||
|
destinationName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Destination city', example: 'Santo Domingo' })
|
||||||
|
@Column({ name: 'destination_city', length: 100 })
|
||||||
|
destinationCity: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Departure date and time' })
|
||||||
|
@Column({ name: 'departure_time', type: 'timestamp' })
|
||||||
|
departureTime: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Arrival date and time' })
|
||||||
|
@Column({ name: 'arrival_time', type: 'timestamp' })
|
||||||
|
arrivalTime: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Flight duration in minutes', example: 240 })
|
||||||
|
@Column({ name: 'duration_minutes' })
|
||||||
|
durationMinutes: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Aircraft type', example: 'Boeing 737' })
|
||||||
|
@Column({ name: 'aircraft_type', length: 50 })
|
||||||
|
aircraftType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Available seat classes' })
|
||||||
|
@Column({ name: 'seat_classes', type: 'jsonb' })
|
||||||
|
seatClasses: Record<string, any>; // { economy: { available: 150, price: 450 }, business: { available: 20, price: 1200 } }
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Flight status', example: 'scheduled' })
|
||||||
|
@Column({ length: 20, default: 'scheduled' })
|
||||||
|
status: string; // scheduled, delayed, cancelled, boarding, departed, arrived
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Stops/layovers' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
stops: Record<string, any>[] | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Baggage policy' })
|
||||||
|
@Column({ name: 'baggage_policy', type: 'jsonb', nullable: true })
|
||||||
|
baggagePolicy: Record<string, any> | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Amenities offered' })
|
||||||
|
@Column({ type: 'text', array: true, nullable: true })
|
||||||
|
amenities: string[] | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Base price in USD' })
|
||||||
|
@Column({ name: 'base_price', type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
basePrice: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Currency', example: 'USD' })
|
||||||
|
@Column({ length: 3, default: 'USD' })
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total seats available' })
|
||||||
|
@Column({ name: 'total_seats' })
|
||||||
|
totalSeats: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Seats booked' })
|
||||||
|
@Column({ name: 'seats_booked', default: 0 })
|
||||||
|
seatsBooked: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is direct flight', example: true })
|
||||||
|
@Column({ name: 'is_direct', default: true })
|
||||||
|
isDirect: boolean;
|
||||||
|
}
|
||||||
46
src/entities/geofence.entity.ts
Normal file
46
src/entities/geofence.entity.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Entity, Column } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'geofences', schema: 'tourism' })
|
||||||
|
export class Geofence extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Geofence name', example: 'Zona Colonial Safety Zone' })
|
||||||
|
@Column({ length: 255 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Center coordinates' })
|
||||||
|
@Column({ name: 'center_coordinates', type: 'point' })
|
||||||
|
centerCoordinates: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Radius in meters', example: 500 })
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
radius: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Geofence type', example: 'safety-alert' })
|
||||||
|
@Column({ length: 50 })
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Description' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Entry alert message' })
|
||||||
|
@Column({ name: 'entry_message', type: 'text', nullable: true })
|
||||||
|
entryMessage: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Exit alert message' })
|
||||||
|
@Column({ name: 'exit_message', type: 'text', nullable: true })
|
||||||
|
exitMessage: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Additional metadata' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
metadata: Record<string, any> | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is active', example: true })
|
||||||
|
@Column({ name: 'is_active', default: true })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Entry count' })
|
||||||
|
@Column({ name: 'entry_count', default: 0 })
|
||||||
|
entryCount: number;
|
||||||
|
}
|
||||||
69
src/entities/hotel-checkin.entity.ts
Executable file
69
src/entities/hotel-checkin.entity.ts
Executable file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { HotelRoom } from './hotel-room.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'hotel_checkins', schema: 'commerce' })
|
||||||
|
export class HotelCheckin extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Room ID' })
|
||||||
|
@Column({ name: 'room_id' })
|
||||||
|
roomId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Guest user ID' })
|
||||||
|
@Column({ name: 'guest_id' })
|
||||||
|
guestId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Reservation ID' })
|
||||||
|
@Column({ name: 'reservation_id' })
|
||||||
|
reservationId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Check-in date' })
|
||||||
|
@Column({ name: 'checkin_date', type: 'date' })
|
||||||
|
checkinDate: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Check-out date' })
|
||||||
|
@Column({ name: 'checkout_date', type: 'date' })
|
||||||
|
checkoutDate: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Actual check-in time' })
|
||||||
|
@Column({ name: 'actual_checkin_time', type: 'timestamp', nullable: true })
|
||||||
|
actualCheckinTime: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Actual check-out time' })
|
||||||
|
@Column({ name: 'actual_checkout_time', type: 'timestamp', nullable: true })
|
||||||
|
actualCheckoutTime: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Number of guests', example: 2 })
|
||||||
|
@Column({ name: 'guest_count' })
|
||||||
|
guestCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Digital key code' })
|
||||||
|
@Column({ name: 'digital_key', type: 'text', nullable: true })
|
||||||
|
digitalKey: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Check-in status', example: 'pending' })
|
||||||
|
@Column({ length: 20, default: 'pending' }) // pending, checked-in, checked-out, no-show
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Special requests' })
|
||||||
|
@Column({ name: 'special_requests', type: 'text', nullable: true })
|
||||||
|
specialRequests: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Guest preferences' })
|
||||||
|
@Column({ name: 'guest_preferences', type: 'jsonb', nullable: true })
|
||||||
|
guestPreferences: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Room access log' })
|
||||||
|
@Column({ name: 'access_log', type: 'jsonb', nullable: true })
|
||||||
|
accessLog: Record<string, any>[];
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => HotelRoom)
|
||||||
|
@JoinColumn({ name: 'room_id' })
|
||||||
|
room: HotelRoom;
|
||||||
|
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'guest_id' })
|
||||||
|
guest: User;
|
||||||
|
}
|
||||||
44
src/entities/hotel-room.entity.ts
Executable file
44
src/entities/hotel-room.entity.ts
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { Establishment } from './establishment.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'hotel_rooms', schema: 'commerce' })
|
||||||
|
export class HotelRoom extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Establishment ID' })
|
||||||
|
@Column({ name: 'establishment_id', nullable: true })
|
||||||
|
establishmentId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Room number', example: '101' })
|
||||||
|
@Column({ name: 'room_number', length: 10, nullable: true })
|
||||||
|
roomNumber: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Room type', example: 'deluxe' })
|
||||||
|
@Column({ name: 'room_type', length: 50, nullable: true })
|
||||||
|
roomType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Room capacity', example: 2 })
|
||||||
|
@Column({ nullable: true })
|
||||||
|
capacity: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Price per night', example: 120.00 })
|
||||||
|
@Column({ name: 'price_per_night', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||||
|
pricePerNight: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Room amenities' })
|
||||||
|
@Column({ type: 'text', array: true, nullable: true })
|
||||||
|
amenities: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Room images' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
images: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is available', example: true })
|
||||||
|
@Column({ name: 'is_available', default: true })
|
||||||
|
isAvailable: boolean;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => Establishment)
|
||||||
|
@JoinColumn({ name: 'establishment_id' })
|
||||||
|
establishment: Establishment;
|
||||||
|
}
|
||||||
69
src/entities/hotel-service.entity.ts
Executable file
69
src/entities/hotel-service.entity.ts
Executable file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { HotelRoom } from './hotel-room.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'hotel_services', schema: 'commerce' })
|
||||||
|
export class HotelService extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Room ID' })
|
||||||
|
@Column({ name: 'room_id' })
|
||||||
|
roomId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Guest ID' })
|
||||||
|
@Column({ name: 'guest_id' })
|
||||||
|
guestId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Service type', example: 'room-service' })
|
||||||
|
@Column({ name: 'service_type', length: 50 })
|
||||||
|
serviceType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Service items requested' })
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
items: Record<string, any>[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Priority level', example: 'normal' })
|
||||||
|
@Column({ length: 20, default: 'normal' })
|
||||||
|
priority: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Preferred time' })
|
||||||
|
@Column({ name: 'preferred_time', nullable: true })
|
||||||
|
preferredTime: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Special instructions' })
|
||||||
|
@Column({ name: 'special_instructions', type: 'text', nullable: true })
|
||||||
|
specialInstructions: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Service status', example: 'pending' })
|
||||||
|
@Column({ length: 20, default: 'pending' }) // pending, assigned, in-progress, completed, cancelled
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Assigned staff ID' })
|
||||||
|
@Column({ name: 'assigned_staff_id', nullable: true })
|
||||||
|
assignedStaffId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Estimated completion time' })
|
||||||
|
@Column({ name: 'estimated_completion', type: 'timestamp', nullable: true })
|
||||||
|
estimatedCompletion: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Actual completion time' })
|
||||||
|
@Column({ name: 'completed_at', type: 'timestamp', nullable: true })
|
||||||
|
completedAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Service notes from staff' })
|
||||||
|
@Column({ name: 'service_notes', type: 'text', nullable: true })
|
||||||
|
serviceNotes: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total cost' })
|
||||||
|
@Column({ name: 'total_cost', type: 'decimal', precision: 8, scale: 2, nullable: true })
|
||||||
|
totalCost: number;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => HotelRoom)
|
||||||
|
@JoinColumn({ name: 'room_id' })
|
||||||
|
room: HotelRoom;
|
||||||
|
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'guest_id' })
|
||||||
|
guest: User;
|
||||||
|
}
|
||||||
65
src/entities/incident.entity.ts
Executable file
65
src/entities/incident.entity.ts
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
import { SecurityOfficer } from './security-officer.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'incidents', schema: 'security' })
|
||||||
|
export class Incident extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Reporter user ID' })
|
||||||
|
@Column({ name: 'reporter_id', nullable: true })
|
||||||
|
reporterId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Assigned officer ID' })
|
||||||
|
@Column({ name: 'officer_id', nullable: true })
|
||||||
|
officerId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Incident type', example: 'theft' })
|
||||||
|
@Column({ length: 50 })
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Priority level', example: 'high' })
|
||||||
|
@Column({ length: 20, default: 'medium' })
|
||||||
|
priority: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Incident description' })
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Incident location' })
|
||||||
|
@Column({ type: 'point', nullable: true })
|
||||||
|
location: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Address' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
address: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Incident status', example: 'reported' })
|
||||||
|
@Column({ length: 20, default: 'reported' })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Reported at' })
|
||||||
|
@Column({ name: 'reported_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
reportedAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Assigned at' })
|
||||||
|
@Column({ name: 'assigned_at', type: 'timestamp', nullable: true })
|
||||||
|
assignedAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Resolved at' })
|
||||||
|
@Column({ name: 'resolved_at', type: 'timestamp', nullable: true })
|
||||||
|
resolvedAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Resolution notes' })
|
||||||
|
@Column({ name: 'resolution_notes', type: 'text', nullable: true })
|
||||||
|
resolutionNotes: string;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'reporter_id' })
|
||||||
|
reporter: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => SecurityOfficer)
|
||||||
|
@JoinColumn({ name: 'officer_id' })
|
||||||
|
officer: SecurityOfficer;
|
||||||
|
}
|
||||||
121
src/entities/influencer-profile.entity.ts
Normal file
121
src/entities/influencer-profile.entity.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { Entity, Column, OneToOne, OneToMany, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'influencer_profiles', schema: 'social_commerce' })
|
||||||
|
export class InfluencerProfile extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id', unique: true })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Influencer tier level' })
|
||||||
|
@Column({ name: 'tier_level', length: 20 })
|
||||||
|
tierLevel: string; // nano, micro, macro, mega
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Verification status' })
|
||||||
|
@Column({ name: 'verification_status', length: 20 })
|
||||||
|
verificationStatus: string; // pending, verified, rejected
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'AI verification score (0-100)' })
|
||||||
|
@Column({ name: 'ai_verification_score', type: 'decimal', precision: 5, scale: 2 })
|
||||||
|
aiVerificationScore: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Social media statistics' })
|
||||||
|
@Column({ name: 'social_stats', type: 'jsonb' })
|
||||||
|
socialStats: {
|
||||||
|
instagram: { followers: number; engagement: number; verified: boolean };
|
||||||
|
tiktok: { followers: number; engagement: number; verified: boolean };
|
||||||
|
youtube: { subscribers: number; views: number; verified: boolean };
|
||||||
|
facebook: { followers: number; engagement: number; verified: boolean };
|
||||||
|
twitter: { followers: number; engagement: number; verified: boolean };
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Content specialties and niches' })
|
||||||
|
@Column({ name: 'specialties', type: 'text', array: true })
|
||||||
|
specialties: string[]; // travel, food, adventure, luxury, budget, family, solo
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Geographic coverage areas' })
|
||||||
|
@Column({ name: 'coverage_areas', type: 'text', array: true })
|
||||||
|
coverageAreas: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Languages for content creation' })
|
||||||
|
@Column({ name: 'content_languages', type: 'text', array: true })
|
||||||
|
contentLanguages: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Pricing and rate card' })
|
||||||
|
@Column({ name: 'pricing', type: 'jsonb' })
|
||||||
|
pricing: {
|
||||||
|
postRate: number;
|
||||||
|
storyRate: number;
|
||||||
|
reelRate: number;
|
||||||
|
liveStreamRate: number;
|
||||||
|
tourGuideRate: number;
|
||||||
|
packageDeals: Array<{ name: string; price: number; description: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Performance metrics' })
|
||||||
|
@Column({ name: 'performance_metrics', type: 'jsonb' })
|
||||||
|
performanceMetrics: {
|
||||||
|
averageEngagement: number;
|
||||||
|
completedCampaigns: number;
|
||||||
|
clientSatisfactionRating: number;
|
||||||
|
responseTime: number; // hours
|
||||||
|
contentQualityScore: number;
|
||||||
|
professionalismScore: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Availability and booking preferences' })
|
||||||
|
@Column({ name: 'availability', type: 'jsonb' })
|
||||||
|
availability: {
|
||||||
|
timezone: string;
|
||||||
|
workingDays: string[];
|
||||||
|
preferredNoticeTime: number; // days
|
||||||
|
maxCampaignsPerMonth: number;
|
||||||
|
travelWillingness: boolean;
|
||||||
|
remoteWorkOnly: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Portfolio and content samples' })
|
||||||
|
@Column({ name: 'portfolio', type: 'jsonb' })
|
||||||
|
portfolio: {
|
||||||
|
featuredContent: Array<{
|
||||||
|
platform: string;
|
||||||
|
url: string;
|
||||||
|
type: string;
|
||||||
|
engagement: number;
|
||||||
|
description: string;
|
||||||
|
}>;
|
||||||
|
testimonials: Array<{
|
||||||
|
clientName: string;
|
||||||
|
rating: number;
|
||||||
|
comment: string;
|
||||||
|
campaignType: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'AI-generated insights' })
|
||||||
|
@Column({ name: 'ai_insights', type: 'jsonb' })
|
||||||
|
aiInsights: {
|
||||||
|
audienceAnalysis: {
|
||||||
|
demographics: Record<string, number>;
|
||||||
|
interests: string[];
|
||||||
|
peakEngagementTimes: string[];
|
||||||
|
};
|
||||||
|
contentAnalysis: {
|
||||||
|
topPerformingContentTypes: string[];
|
||||||
|
averageEngagementByType: Record<string, number>;
|
||||||
|
sentimentAnalysis: { positive: number; neutral: number; negative: number };
|
||||||
|
};
|
||||||
|
marketValue: {
|
||||||
|
estimatedReach: number;
|
||||||
|
estimatedValue: number;
|
||||||
|
growthPotential: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@OneToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
92
src/entities/iot-device.entity.ts
Normal file
92
src/entities/iot-device.entity.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { PlaceOfInterest } from './place-of-interest.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'iot_devices', schema: 'smart_tourism' })
|
||||||
|
export class IoTDevice extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Device unique identifier' })
|
||||||
|
@Column({ name: 'device_id', unique: true })
|
||||||
|
deviceId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Device name/label' })
|
||||||
|
@Column({ name: 'device_name', length: 100 })
|
||||||
|
deviceName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Device type' })
|
||||||
|
@Column({ name: 'device_type', length: 50 })
|
||||||
|
deviceType: string; // crowd-sensor, air-quality, noise, temperature, parking, wifi-beacon
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Device status' })
|
||||||
|
@Column({ length: 20, default: 'active' })
|
||||||
|
status: string; // active, inactive, maintenance, error
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Device location coordinates' })
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
location: {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
altitude: number;
|
||||||
|
address: string;
|
||||||
|
zone: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Device specifications and capabilities' })
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
specifications: {
|
||||||
|
manufacturer: string;
|
||||||
|
model: string;
|
||||||
|
firmwareVersion: string;
|
||||||
|
connectivity: string[]; // 5G, WiFi, LoRaWAN, NB-IoT
|
||||||
|
sensors: string[];
|
||||||
|
dataTypes: string[];
|
||||||
|
batteryLevel: number;
|
||||||
|
transmissionRange: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Current sensor readings' })
|
||||||
|
@Column({ name: 'current_readings', type: 'jsonb' })
|
||||||
|
currentReadings: {
|
||||||
|
timestamp: Date;
|
||||||
|
crowdDensity?: number; // people per m²
|
||||||
|
airQualityIndex?: number; // 0-500 AQI
|
||||||
|
noiseLevel?: number; // decibels
|
||||||
|
temperature?: number; // celsius
|
||||||
|
humidity?: number; // percentage
|
||||||
|
parkingOccupancy?: number; // percentage
|
||||||
|
wifiConnections?: number;
|
||||||
|
energyConsumption?: number; // kWh
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Device configuration and settings' })
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
configuration: {
|
||||||
|
sampleRate: number; // seconds between readings
|
||||||
|
alertThresholds: Record<string, { min: number; max: number }>;
|
||||||
|
dataRetentionPeriod: number; // days
|
||||||
|
transmissionFrequency: number; // minutes
|
||||||
|
lowPowerMode: boolean;
|
||||||
|
geofenceRadius: number; // meters
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Connected place of interest ID' })
|
||||||
|
@Column({ name: 'place_id', nullable: true })
|
||||||
|
placeId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Last maintenance date' })
|
||||||
|
@Column({ name: 'last_maintenance', type: 'timestamp', nullable: true })
|
||||||
|
lastMaintenance: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Next scheduled maintenance' })
|
||||||
|
@Column({ name: 'next_maintenance', type: 'timestamp', nullable: true })
|
||||||
|
nextMaintenance: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Device health score (0-100)' })
|
||||||
|
@Column({ name: 'health_score', type: 'decimal', precision: 5, scale: 2, default: 100 })
|
||||||
|
healthScore: number;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => PlaceOfInterest)
|
||||||
|
@JoinColumn({ name: 'place_id' })
|
||||||
|
place: PlaceOfInterest;
|
||||||
|
}
|
||||||
56
src/entities/itinerary.entity.ts
Executable file
56
src/entities/itinerary.entity.ts
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { TourGuide } from './tour-guide.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'itineraries', schema: 'tourism' })
|
||||||
|
export class Itinerary extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Tour guide ID' })
|
||||||
|
@Column({ name: 'guide_id', nullable: true })
|
||||||
|
guideId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Itinerary name', example: 'Colonial Santo Domingo Tour' })
|
||||||
|
@Column({ length: 255 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Itinerary description' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Duration in hours', example: 4 })
|
||||||
|
@Column({ name: 'duration_hours', nullable: true })
|
||||||
|
durationHours: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Maximum participants', example: 8 })
|
||||||
|
@Column({ name: 'max_participants', nullable: true })
|
||||||
|
maxParticipants: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Price per person', example: 75.00 })
|
||||||
|
@Column({ name: 'price_per_person', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||||
|
pricePerPerson: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Included services' })
|
||||||
|
@Column({ name: 'included_services', type: 'text', array: true, nullable: true })
|
||||||
|
includedServices: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Places to visit' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
places: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Difficulty level', example: 'easy' })
|
||||||
|
@Column({ name: 'difficulty_level', length: 20, nullable: true })
|
||||||
|
difficultyLevel: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is template', example: false })
|
||||||
|
@Column({ name: 'is_template', default: false })
|
||||||
|
isTemplate: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is active', example: true })
|
||||||
|
@Column({ default: true })
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => TourGuide)
|
||||||
|
@JoinColumn({ name: 'guide_id' })
|
||||||
|
guide: TourGuide;
|
||||||
|
}
|
||||||
37
src/entities/language.entity.ts
Executable file
37
src/entities/language.entity.ts
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'languages', schema: 'auth' })
|
||||||
|
export class Language {
|
||||||
|
@ApiProperty({ description: 'Language ID', example: 1 })
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Language code', example: 'en' })
|
||||||
|
@Column({ length: 5, unique: true })
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Language name', example: 'English' })
|
||||||
|
@Column({ length: 50 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Native language name', example: 'English' })
|
||||||
|
@Column({ name: 'native_name', length: 50 })
|
||||||
|
nativeName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is default language', example: true })
|
||||||
|
@Column({ name: 'is_default', default: false })
|
||||||
|
isDefault: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Active status', example: true })
|
||||||
|
@Column({ default: true })
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Creation date' })
|
||||||
|
@Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@OneToMany(() => User, user => user.preferredLanguageEntity)
|
||||||
|
users: User[];
|
||||||
|
}
|
||||||
109
src/entities/listing.entity.ts
Normal file
109
src/entities/listing.entity.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { Establishment } from './establishment.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'listings', schema: 'commerce' })
|
||||||
|
export class Listing extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Property owner user ID' })
|
||||||
|
@Column({ name: 'owner_id' })
|
||||||
|
ownerId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Associated establishment ID' })
|
||||||
|
@Column({ name: 'establishment_id', type: 'uuid', nullable: true })
|
||||||
|
establishmentId: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Listing type', example: 'hotel' })
|
||||||
|
@Column({ name: 'listing_type', length: 50 })
|
||||||
|
listingType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Listing title', example: 'Luxury Beach Resort in Punta Cana' })
|
||||||
|
@Column({ length: 255 })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Detailed description' })
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Property location' })
|
||||||
|
@Column({ type: 'point', nullable: true })
|
||||||
|
coordinates: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Address' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
address: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Base price per night/hour/day' })
|
||||||
|
@Column({ name: 'base_price', type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
basePrice: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Currency', example: 'USD' })
|
||||||
|
@Column({ length: 3, default: 'USD' })
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Maximum capacity', example: 4 })
|
||||||
|
@Column({ type: 'integer', nullable: true })
|
||||||
|
capacity: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Amenities list' })
|
||||||
|
@Column({ type: 'text', array: true, nullable: true })
|
||||||
|
amenities: string[] | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Property images' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
images: Record<string, any>[] | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Property rules and policies' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
policies: Record<string, any> | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Check-in time', example: '15:00' })
|
||||||
|
@Column({ name: 'checkin_time', type: 'varchar', nullable: true })
|
||||||
|
checkinTime: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Check-out time', example: '11:00' })
|
||||||
|
@Column({ name: 'checkout_time', type: 'varchar', nullable: true })
|
||||||
|
checkoutTime: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Minimum stay nights', example: 2 })
|
||||||
|
@Column({ name: 'min_stay', type: 'integer', nullable: true })
|
||||||
|
minStay: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Maximum stay nights', example: 30 })
|
||||||
|
@Column({ name: 'max_stay', type: 'integer', nullable: true })
|
||||||
|
maxStay: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Channel distribution settings' })
|
||||||
|
@Column({ name: 'channel_settings', type: 'jsonb', nullable: true })
|
||||||
|
channelSettings: Record<string, any> | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Listing status', example: 'published' })
|
||||||
|
@Column({ length: 20, default: 'draft' })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Average rating', example: 4.5 })
|
||||||
|
@Column({ type: 'decimal', precision: 3, scale: 2, nullable: true })
|
||||||
|
rating: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total reviews count' })
|
||||||
|
@Column({ name: 'reviews_count', default: 0 })
|
||||||
|
reviewsCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Booking count' })
|
||||||
|
@Column({ name: 'bookings_count', default: 0 })
|
||||||
|
bookingsCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Last updated' })
|
||||||
|
@Column({ name: 'last_updated', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
lastUpdated: Date;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'owner_id' })
|
||||||
|
owner: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => Establishment)
|
||||||
|
@JoinColumn({ name: 'establishment_id' })
|
||||||
|
establishment: Establishment;
|
||||||
|
}
|
||||||
44
src/entities/location-tracking.entity.ts
Normal file
44
src/entities/location-tracking.entity.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'location_tracking', schema: 'analytics' })
|
||||||
|
export class LocationTracking extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id' })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'User location coordinates' })
|
||||||
|
@Column({ type: 'point' })
|
||||||
|
coordinates: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Location accuracy in meters' })
|
||||||
|
@Column({ type: 'decimal', precision: 8, scale: 2, nullable: true })
|
||||||
|
accuracy: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Speed in km/h' })
|
||||||
|
@Column({ type: 'decimal', precision: 8, scale: 2, nullable: true })
|
||||||
|
speed: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Heading in degrees' })
|
||||||
|
@Column({ type: 'decimal', precision: 8, scale: 2, nullable: true })
|
||||||
|
heading: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Activity type', example: 'walking' })
|
||||||
|
@Column({ type: 'varchar', length: 50, nullable: true })
|
||||||
|
activity: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Device info' })
|
||||||
|
@Column({ name: 'device_info', type: 'jsonb', nullable: true })
|
||||||
|
deviceInfo: Record<string, any> | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Geofences triggered' })
|
||||||
|
@Column({ name: 'geofences_triggered', type: 'text', array: true, nullable: true })
|
||||||
|
geofencesTriggered: string[] | null;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
72
src/entities/menu-item.entity.ts
Executable file
72
src/entities/menu-item.entity.ts
Executable file
@@ -0,0 +1,72 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { Establishment } from './establishment.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'menu_items', schema: 'commerce' })
|
||||||
|
export class MenuItem extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Establishment ID' })
|
||||||
|
@Column({ name: 'establishment_id' })
|
||||||
|
establishmentId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Item name', example: 'Mofongo with Shrimp' })
|
||||||
|
@Column({ length: 255 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Item description' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Category', example: 'main-course' })
|
||||||
|
@Column({ length: 100 })
|
||||||
|
category: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Price', example: 18.50 })
|
||||||
|
@Column({ type: 'decimal', precision: 8, scale: 2 })
|
||||||
|
price: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Currency', example: 'USD' })
|
||||||
|
@Column({ length: 3, default: 'USD' })
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Preparation time in minutes', example: 15 })
|
||||||
|
@Column({ name: 'prep_time_minutes', nullable: true })
|
||||||
|
prepTimeMinutes: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Item images' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
images: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Nutritional info' })
|
||||||
|
@Column({ name: 'nutritional_info', type: 'jsonb', nullable: true })
|
||||||
|
nutritionalInfo: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Allergens list' })
|
||||||
|
@Column({ type: 'text', array: true, nullable: true })
|
||||||
|
allergens: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is vegetarian', example: false })
|
||||||
|
@Column({ name: 'is_vegetarian', default: false })
|
||||||
|
isVegetarian: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is vegan', example: false })
|
||||||
|
@Column({ name: 'is_vegan', default: false })
|
||||||
|
isVegan: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is gluten free', example: false })
|
||||||
|
@Column({ name: 'is_gluten_free', default: false })
|
||||||
|
isGlutenFree: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is available', example: true })
|
||||||
|
@Column({ name: 'is_available', default: true })
|
||||||
|
isAvailable: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is active', example: true })
|
||||||
|
@Column({ name: 'is_active', default: true })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => Establishment)
|
||||||
|
@JoinColumn({ name: 'establishment_id' })
|
||||||
|
establishment: Establishment;
|
||||||
|
}
|
||||||
48
src/entities/notification.entity.ts
Executable file
48
src/entities/notification.entity.ts
Executable file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'notifications', schema: 'notifications' })
|
||||||
|
export class Notification extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id', nullable: true })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Notification type', example: 'push' })
|
||||||
|
@Column({ length: 30 })
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Notification category', example: 'booking' })
|
||||||
|
@Column({ length: 50, nullable: true })
|
||||||
|
category: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Notification title', example: 'Booking Confirmed' })
|
||||||
|
@Column({ length: 255 })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Notification message' })
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Additional data' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
data: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is read', example: false })
|
||||||
|
@Column({ name: 'is_read', default: false })
|
||||||
|
isRead: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Sent at' })
|
||||||
|
@Column({ name: 'sent_at', type: 'timestamp', nullable: true })
|
||||||
|
sentAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Read at' })
|
||||||
|
@Column({ name: 'read_at', type: 'timestamp', nullable: true })
|
||||||
|
readAt: Date;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
45
src/entities/order-item.entity.ts
Executable file
45
src/entities/order-item.entity.ts
Executable file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { Order } from './order.entity';
|
||||||
|
import { MenuItem } from './menu-item.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'order_items', schema: 'commerce' })
|
||||||
|
export class OrderItem extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Order ID' })
|
||||||
|
@Column({ name: 'order_id' })
|
||||||
|
orderId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Menu item ID' })
|
||||||
|
@Column({ name: 'menu_item_id' })
|
||||||
|
menuItemId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Quantity', example: 2 })
|
||||||
|
@Column()
|
||||||
|
quantity: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Unit price at time of order', example: 18.50 })
|
||||||
|
@Column({ name: 'unit_price', type: 'decimal', precision: 8, scale: 2 })
|
||||||
|
unitPrice: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total price for this item', example: 37.00 })
|
||||||
|
@Column({ name: 'total_price', type: 'decimal', precision: 8, scale: 2 })
|
||||||
|
totalPrice: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Special requests for this item' })
|
||||||
|
@Column({ name: 'special_requests', type: 'text', nullable: true })
|
||||||
|
specialRequests: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Item status', example: 'pending' })
|
||||||
|
@Column({ length: 20, default: 'pending' }) // pending, preparing, ready, served
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => Order)
|
||||||
|
@JoinColumn({ name: 'order_id' })
|
||||||
|
order: Order;
|
||||||
|
|
||||||
|
@ManyToOne(() => MenuItem)
|
||||||
|
@JoinColumn({ name: 'menu_item_id' })
|
||||||
|
menuItem: MenuItem;
|
||||||
|
}
|
||||||
94
src/entities/order.entity.ts
Executable file
94
src/entities/order.entity.ts
Executable file
@@ -0,0 +1,94 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { Establishment } from './establishment.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
import { Table } from './table.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'orders', schema: 'commerce' })
|
||||||
|
export class Order extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Establishment ID' })
|
||||||
|
@Column({ name: 'establishment_id' })
|
||||||
|
establishmentId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Customer user ID' })
|
||||||
|
@Column({ name: 'customer_id', nullable: true })
|
||||||
|
customerId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Table ID' })
|
||||||
|
@Column({ name: 'table_id', nullable: true })
|
||||||
|
tableId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Order number', example: 'ORD-001-20250624' })
|
||||||
|
@Column({ name: 'order_number', length: 50, unique: true })
|
||||||
|
orderNumber: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Order type', example: 'dine-in' })
|
||||||
|
@Column({ name: 'order_type', length: 20 }) // dine-in, takeout, delivery
|
||||||
|
orderType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Customer name', example: 'John Doe' })
|
||||||
|
@Column({ name: 'customer_name', length: 255, nullable: true })
|
||||||
|
customerName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Customer phone', example: '+1234567890' })
|
||||||
|
@Column({ name: 'customer_phone', length: 20, nullable: true })
|
||||||
|
customerPhone: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Special instructions' })
|
||||||
|
@Column({ name: 'special_instructions', type: 'text', nullable: true })
|
||||||
|
specialInstructions: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Subtotal amount', example: 45.50 })
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
subtotal: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Tax amount', example: 4.55 })
|
||||||
|
@Column({ name: 'tax_amount', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||||
|
taxAmount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Service charge', example: 2.25 })
|
||||||
|
@Column({ name: 'service_charge', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||||
|
serviceCharge: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Discount amount', example: 5.00 })
|
||||||
|
@Column({ name: 'discount_amount', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||||
|
discountAmount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total amount', example: 47.30 })
|
||||||
|
@Column({ name: 'total_amount', type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
totalAmount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Order status', example: 'pending' })
|
||||||
|
@Column({ length: 20, default: 'pending' }) // pending, confirmed, preparing, ready, served, paid, cancelled
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Payment status', example: 'pending' })
|
||||||
|
@Column({ name: 'payment_status', length: 20, default: 'pending' }) // pending, paid, refunded
|
||||||
|
paymentStatus: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Estimated ready time' })
|
||||||
|
@Column({ name: 'estimated_ready_time', type: 'timestamp', nullable: true })
|
||||||
|
estimatedReadyTime: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Served at' })
|
||||||
|
@Column({ name: 'served_at', type: 'timestamp', nullable: true })
|
||||||
|
servedAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Paid at' })
|
||||||
|
@Column({ name: 'paid_at', type: 'timestamp', nullable: true })
|
||||||
|
paidAt: Date;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => Establishment)
|
||||||
|
@JoinColumn({ name: 'establishment_id' })
|
||||||
|
establishment: Establishment;
|
||||||
|
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'customer_id' })
|
||||||
|
customer: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => Table)
|
||||||
|
@JoinColumn({ name: 'table_id' })
|
||||||
|
table: Table;
|
||||||
|
}
|
||||||
80
src/entities/place-of-interest.entity.ts
Executable file
80
src/entities/place-of-interest.entity.ts
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { Destination } from './destination.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'places_of_interest', schema: 'tourism' })
|
||||||
|
export class PlaceOfInterest extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Destination ID', example: 1 })
|
||||||
|
@Column({ name: 'destination_id', nullable: true })
|
||||||
|
destinationId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Place name', example: 'Alcázar de Colón' })
|
||||||
|
@Column({ length: 255 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Place description' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Category', example: 'monument' })
|
||||||
|
@Column({ length: 50, nullable: true })
|
||||||
|
category: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Coordinates (lat, lng)' })
|
||||||
|
@Column({ type: 'point' })
|
||||||
|
coordinates: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Address', example: 'Plaza de Armas, Santo Domingo' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
address: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Phone number', example: '+1809555XXXX' })
|
||||||
|
@Column({ length: 20, nullable: true })
|
||||||
|
phone: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Website URL' })
|
||||||
|
@Column({ length: 255, nullable: true })
|
||||||
|
website: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Opening hours' })
|
||||||
|
@Column({ name: 'opening_hours', type: 'jsonb', nullable: true })
|
||||||
|
openingHours: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Entrance fee', example: 25.00 })
|
||||||
|
@Column({ name: 'entrance_fee', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||||
|
entranceFee: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Images' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
images: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Historical information' })
|
||||||
|
@Column({ name: 'historical_info', type: 'text', nullable: true })
|
||||||
|
historicalInfo: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'AR content' })
|
||||||
|
@Column({ name: 'ar_content', type: 'jsonb', nullable: true })
|
||||||
|
arContent: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Audio guide URL' })
|
||||||
|
@Column({ name: 'audio_guide_url', type: 'text', nullable: true })
|
||||||
|
audioGuideUrl: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Average rating', example: 4.5 })
|
||||||
|
@Column({ type: 'decimal', precision: 3, scale: 2, nullable: true })
|
||||||
|
rating: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total reviews', example: 150 })
|
||||||
|
@Column({ name: 'total_reviews', default: 0 })
|
||||||
|
totalReviews: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Active status', example: true })
|
||||||
|
@Column({ default: true })
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => Destination)
|
||||||
|
@JoinColumn({ name: 'destination_id' })
|
||||||
|
destination: Destination;
|
||||||
|
}
|
||||||
56
src/entities/product.entity.ts
Executable file
56
src/entities/product.entity.ts
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { Establishment } from './establishment.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'products', schema: 'commerce' })
|
||||||
|
export class Product extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Establishment ID' })
|
||||||
|
@Column({ name: 'establishment_id', nullable: true })
|
||||||
|
establishmentId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Product name', example: 'Dominican Coffee' })
|
||||||
|
@Column({ length: 255 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Product description' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Product category', example: 'beverages' })
|
||||||
|
@Column({ length: 100, nullable: true })
|
||||||
|
category: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Price', example: 15.99 })
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||||
|
price: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Currency', example: 'USD' })
|
||||||
|
@Column({ length: 3, default: 'USD' })
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Product images' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
images: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Product specifications' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
specifications: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Stock quantity', example: 100 })
|
||||||
|
@Column({ name: 'stock_quantity', nullable: true })
|
||||||
|
stockQuantity: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is digital product', example: false })
|
||||||
|
@Column({ name: 'is_digital', default: false })
|
||||||
|
isDigital: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is active', example: true })
|
||||||
|
@Column({ name: 'is_active', default: true })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => Establishment)
|
||||||
|
@JoinColumn({ name: 'establishment_id' })
|
||||||
|
establishment: Establishment;
|
||||||
|
}
|
||||||
61
src/entities/reservation.entity.ts
Executable file
61
src/entities/reservation.entity.ts
Executable file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { Establishment } from './establishment.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'reservations', schema: 'commerce' })
|
||||||
|
export class Reservation extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Establishment ID' })
|
||||||
|
@Column({ name: 'establishment_id', nullable: true })
|
||||||
|
establishmentId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id', nullable: true })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Reservation type', example: 'room' })
|
||||||
|
@Column({ length: 20 })
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Reference ID (room, table, etc.)' })
|
||||||
|
@Column({ name: 'reference_id', nullable: true })
|
||||||
|
referenceId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Check-in date' })
|
||||||
|
@Column({ name: 'check_in_date', type: 'date', nullable: true })
|
||||||
|
checkInDate: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Check-out date' })
|
||||||
|
@Column({ name: 'check_out_date', type: 'date', nullable: true })
|
||||||
|
checkOutDate: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Check-in time' })
|
||||||
|
@Column({ name: 'check_in_time', type: 'time', nullable: true })
|
||||||
|
checkInTime: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Number of guests', example: 2 })
|
||||||
|
@Column({ name: 'guests_count', nullable: true })
|
||||||
|
guestsCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Special requests' })
|
||||||
|
@Column({ name: 'special_requests', type: 'text', nullable: true })
|
||||||
|
specialRequests: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total amount', example: 240.00 })
|
||||||
|
@Column({ name: 'total_amount', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||||
|
totalAmount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Reservation status', example: 'confirmed' })
|
||||||
|
@Column({ length: 20, default: 'pending' })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => Establishment)
|
||||||
|
@JoinColumn({ name: 'establishment_id' })
|
||||||
|
establishment: Establishment;
|
||||||
|
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
30
src/entities/review-helpfulness.entity.ts
Normal file
30
src/entities/review-helpfulness.entity.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn, Unique } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
import { AdvancedReview } from './advanced-review.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'review_helpfulness', schema: 'analytics' })
|
||||||
|
@Unique(['userId', 'reviewId'])
|
||||||
|
export class ReviewHelpfulness extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id' })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Review ID' })
|
||||||
|
@Column({ name: 'review_id' })
|
||||||
|
reviewId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is helpful vote', example: true })
|
||||||
|
@Column({ name: 'is_helpful' })
|
||||||
|
isHelpful: boolean;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => AdvancedReview)
|
||||||
|
@JoinColumn({ name: 'review_id' })
|
||||||
|
review: AdvancedReview;
|
||||||
|
}
|
||||||
49
src/entities/review.entity.ts
Executable file
49
src/entities/review.entity.ts
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn, Check } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'reviews', schema: 'analytics' })
|
||||||
|
@Check(`rating >= 1 AND rating <= 5`)
|
||||||
|
export class Review extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id', nullable: true })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Reviewable type', example: 'establishment' })
|
||||||
|
@Column({ name: 'reviewable_type', length: 30 })
|
||||||
|
reviewableType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Reviewable ID' })
|
||||||
|
@Column({ name: 'reviewable_id' })
|
||||||
|
reviewableId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Rating (1-5)', example: 5 })
|
||||||
|
@Column()
|
||||||
|
rating: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Review title', example: 'Amazing experience!' })
|
||||||
|
@Column({ length: 255, nullable: true })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Review comment' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
comment: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Review images' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
images: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is verified review', example: false })
|
||||||
|
@Column({ name: 'is_verified', default: false })
|
||||||
|
isVerified: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Helpful count', example: 15 })
|
||||||
|
@Column({ name: 'helpful_count', default: 0 })
|
||||||
|
helpfulCount: number;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
33
src/entities/role.entity.ts
Executable file
33
src/entities/role.entity.ts
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'roles', schema: 'auth' })
|
||||||
|
export class Role {
|
||||||
|
@ApiProperty({ description: 'Role ID', example: 1 })
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Role name', example: 'tourist' })
|
||||||
|
@Column({ length: 50, unique: true })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Role description', example: 'Tourist user with booking capabilities' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Role permissions', example: { read: ['places'], create: ['reviews'] } })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
permissions: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Active status', example: true })
|
||||||
|
@Column({ default: true })
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Creation date' })
|
||||||
|
@Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@OneToMany(() => User, user => user.role)
|
||||||
|
users: User[];
|
||||||
|
}
|
||||||
40
src/entities/security-officer.entity.ts
Executable file
40
src/entities/security-officer.entity.ts
Executable file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'officers', schema: 'security' })
|
||||||
|
export class SecurityOfficer extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id', nullable: true })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Badge number', example: 'POL-001' })
|
||||||
|
@Column({ name: 'badge_number', length: 20, unique: true })
|
||||||
|
badgeNumber: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Rank', example: 'Lieutenant' })
|
||||||
|
@Column({ length: 50, nullable: true })
|
||||||
|
rank: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Department', example: 'POLITUR Santo Domingo' })
|
||||||
|
@Column({ length: 100, nullable: true })
|
||||||
|
department: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Zone assignment', example: 'Zona Colonial' })
|
||||||
|
@Column({ name: 'zone_assignment', length: 100, nullable: true })
|
||||||
|
zoneAssignment: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is on duty', example: false })
|
||||||
|
@Column({ name: 'is_on_duty', default: false })
|
||||||
|
isOnDuty: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Current location' })
|
||||||
|
@Column({ name: 'current_location', type: 'point', nullable: true })
|
||||||
|
currentLocation: string;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
55
src/entities/settlement.entity.ts
Normal file
55
src/entities/settlement.entity.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ schema: 'finance', name: 'settlements' })
|
||||||
|
export class Settlement {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'merchant_id', type: 'uuid' })
|
||||||
|
merchantId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'settlement_period', length: 20 })
|
||||||
|
settlementPeriod: string; // weekly, biweekly, monthly
|
||||||
|
|
||||||
|
@Column({ name: 'period_start', type: 'date' })
|
||||||
|
periodStart: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'period_end', type: 'date' })
|
||||||
|
periodEnd: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'total_gross', type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
totalGross: number;
|
||||||
|
|
||||||
|
@Column({ name: 'total_commission', type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
totalCommission: number;
|
||||||
|
|
||||||
|
@Column({ name: 'total_net', type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
totalNet: number;
|
||||||
|
|
||||||
|
@Column({ name: 'transaction_count', type: 'integer' })
|
||||||
|
transactionCount: number;
|
||||||
|
|
||||||
|
@Column({ length: 20, default: 'pending' })
|
||||||
|
status: string; // pending, processing, completed, failed
|
||||||
|
|
||||||
|
@Column({ name: 'stripe_transfer_id', length: 255, nullable: true })
|
||||||
|
stripeTransferId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'processed_at', type: 'timestamp', nullable: true })
|
||||||
|
processedAt: Date;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'merchant_id' })
|
||||||
|
merchant: User;
|
||||||
|
}
|
||||||
64
src/entities/smart-tourism-data.entity.ts
Normal file
64
src/entities/smart-tourism-data.entity.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { IoTDevice } from './iot-device.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'smart_tourism_data', schema: 'smart_tourism' })
|
||||||
|
export class SmartTourismData extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'IoT Device ID' })
|
||||||
|
@Column({ name: 'device_id' })
|
||||||
|
deviceId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Data timestamp' })
|
||||||
|
@Column({ type: 'timestamp' })
|
||||||
|
timestamp: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Sensor data readings' })
|
||||||
|
@Column({ name: 'sensor_data', type: 'jsonb' })
|
||||||
|
sensorData: {
|
||||||
|
crowdDensity?: number;
|
||||||
|
airQualityIndex?: number;
|
||||||
|
noiseLevel?: number;
|
||||||
|
temperature?: number;
|
||||||
|
humidity?: number;
|
||||||
|
parkingOccupancy?: number;
|
||||||
|
wifiConnections?: number;
|
||||||
|
energyConsumption?: number;
|
||||||
|
visitorFlow?: { in: number; out: number };
|
||||||
|
weatherConditions?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Processed insights and analytics' })
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
insights: {
|
||||||
|
crowdLevel: string; // low, moderate, high, very-high
|
||||||
|
comfortIndex: number; // 0-100
|
||||||
|
recommendations: string[];
|
||||||
|
alerts: Array<{ type: string; severity: string; message: string }>;
|
||||||
|
predictedTrends: Array<{ metric: string; direction: string; confidence: number }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Data quality score' })
|
||||||
|
@Column({ name: 'data_quality', type: 'decimal', precision: 5, scale: 2 })
|
||||||
|
dataQuality: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is anomaly detected' })
|
||||||
|
@Column({ name: 'is_anomaly', default: false })
|
||||||
|
isAnomaly: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Anomaly details if detected' })
|
||||||
|
@Column({ name: 'anomaly_details', type: 'jsonb', nullable: true })
|
||||||
|
anomalyDetails: {
|
||||||
|
type: string;
|
||||||
|
severity: string;
|
||||||
|
description: string;
|
||||||
|
expectedValue: number;
|
||||||
|
actualValue: number;
|
||||||
|
confidence: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => IoTDevice)
|
||||||
|
@JoinColumn({ name: 'device_id' })
|
||||||
|
device: IoTDevice;
|
||||||
|
}
|
||||||
70
src/entities/sustainability-tracking.entity.ts
Normal file
70
src/entities/sustainability-tracking.entity.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'sustainability_tracking', schema: 'analytics' })
|
||||||
|
export class SustainabilityTracking extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id' })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Activity type that generated carbon footprint' })
|
||||||
|
@Column({ name: 'activity_type', length: 50 })
|
||||||
|
activityType: string; // transportation, accommodation, dining, activities
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Activity details' })
|
||||||
|
@Column({ name: 'activity_details', type: 'jsonb' })
|
||||||
|
activityDetails: {
|
||||||
|
description: string;
|
||||||
|
location?: string;
|
||||||
|
duration?: number;
|
||||||
|
distance?: number;
|
||||||
|
participants?: number;
|
||||||
|
provider?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Carbon footprint in kg CO2' })
|
||||||
|
@Column({ name: 'carbon_footprint_kg', type: 'decimal', precision: 10, scale: 3 })
|
||||||
|
carbonFootprintKg: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Water usage in liters' })
|
||||||
|
@Column({ name: 'water_usage_liters', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||||
|
waterUsageLiters: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Waste generated in kg' })
|
||||||
|
@Column({ name: 'waste_generated_kg', type: 'decimal', precision: 8, scale: 3, nullable: true })
|
||||||
|
wasteGeneratedKg: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Energy consumption in kWh' })
|
||||||
|
@Column({ name: 'energy_consumption_kwh', type: 'decimal', precision: 10, scale: 3, nullable: true })
|
||||||
|
energyConsumptionKwh: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Sustainability score (0-100)' })
|
||||||
|
@Column({ name: 'sustainability_score', type: 'decimal', precision: 5, scale: 2 })
|
||||||
|
sustainabilityScore: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Offset credits purchased' })
|
||||||
|
@Column({ name: 'offset_credits_kg', type: 'decimal', precision: 10, scale: 3, default: 0 })
|
||||||
|
offsetCreditsKg: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Cost of offset in USD' })
|
||||||
|
@Column({ name: 'offset_cost_usd', type: 'decimal', precision: 8, scale: 2, default: 0 })
|
||||||
|
offsetCostUsd: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Certification or verification data' })
|
||||||
|
@Column({ name: 'certifications', type: 'jsonb', nullable: true })
|
||||||
|
certifications: {
|
||||||
|
ecoFriendly: boolean;
|
||||||
|
carbonNeutral: boolean;
|
||||||
|
sustainableTourism: boolean;
|
||||||
|
localCommunitySupport: boolean;
|
||||||
|
wildlifeProtection: boolean;
|
||||||
|
certificationBodies: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
40
src/entities/table.entity.ts
Executable file
40
src/entities/table.entity.ts
Executable file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { Establishment } from './establishment.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'tables', schema: 'commerce' })
|
||||||
|
export class Table extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Establishment ID' })
|
||||||
|
@Column({ name: 'establishment_id' })
|
||||||
|
establishmentId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Table number', example: 'T-01' })
|
||||||
|
@Column({ name: 'table_number', length: 20 })
|
||||||
|
tableNumber: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Seating capacity', example: 4 })
|
||||||
|
@Column()
|
||||||
|
capacity: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Table location', example: 'Terrace' })
|
||||||
|
@Column({ length: 100, nullable: true })
|
||||||
|
location: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'QR code for digital menu' })
|
||||||
|
@Column({ name: 'qr_code', type: 'text', nullable: true })
|
||||||
|
qrCode: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Table status', example: 'available' })
|
||||||
|
@Column({ length: 20, default: 'available' }) // available, occupied, reserved, cleaning
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is active', example: true })
|
||||||
|
@Column({ name: 'is_active', default: true })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => Establishment)
|
||||||
|
@JoinColumn({ name: 'establishment_id' })
|
||||||
|
establishment: Establishment;
|
||||||
|
}
|
||||||
64
src/entities/taxi-driver.entity.ts
Executable file
64
src/entities/taxi-driver.entity.ts
Executable file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'taxi_drivers', schema: 'tourism' })
|
||||||
|
export class TaxiDriver extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id', nullable: true })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'License number', example: 'DL-123456789' })
|
||||||
|
@Column({ name: 'license_number', length: 50, unique: true })
|
||||||
|
licenseNumber: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Vehicle plate', example: 'A123456' })
|
||||||
|
@Column({ name: 'vehicle_plate', length: 20, unique: true })
|
||||||
|
vehiclePlate: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Vehicle model', example: 'Toyota Corolla 2020' })
|
||||||
|
@Column({ name: 'vehicle_model', length: 100, nullable: true })
|
||||||
|
vehicleModel: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Vehicle year', example: 2020 })
|
||||||
|
@Column({ name: 'vehicle_year', nullable: true })
|
||||||
|
vehicleYear: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Vehicle color', example: 'White' })
|
||||||
|
@Column({ name: 'vehicle_color', length: 30, nullable: true })
|
||||||
|
vehicleColor: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Vehicle capacity', example: 4 })
|
||||||
|
@Column({ name: 'vehicle_capacity', nullable: true })
|
||||||
|
vehicleCapacity: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Average rating', example: 4.6 })
|
||||||
|
@Column({ type: 'decimal', precision: 3, scale: 2, nullable: true })
|
||||||
|
rating: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total reviews' })
|
||||||
|
@Column({ name: 'total_reviews', default: 0 })
|
||||||
|
totalReviews: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total trips completed' })
|
||||||
|
@Column({ name: 'total_trips', default: 0 })
|
||||||
|
totalTrips: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is verified', example: false })
|
||||||
|
@Column({ name: 'is_verified', default: false })
|
||||||
|
isVerified: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is available', example: true })
|
||||||
|
@Column({ name: 'is_available', default: true })
|
||||||
|
isAvailable: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Current location' })
|
||||||
|
@Column({ name: 'current_location', type: 'point', nullable: true })
|
||||||
|
currentLocation: string;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
64
src/entities/tour-guide.entity.ts
Executable file
64
src/entities/tour-guide.entity.ts
Executable file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'tour_guides', schema: 'tourism' })
|
||||||
|
export class TourGuide extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id', nullable: true })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'License number', example: 'TG-2025-001' })
|
||||||
|
@Column({ name: 'license_number', length: 50, unique: true, nullable: true })
|
||||||
|
licenseNumber: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Specialties', example: ['history', 'nature', 'adventure'] })
|
||||||
|
@Column({ type: 'text', array: true, nullable: true })
|
||||||
|
specialties: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Languages spoken', example: ['en', 'es', 'fr'] })
|
||||||
|
@Column({ type: 'text', array: true, nullable: true })
|
||||||
|
languages: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Hourly rate in USD', example: 25.00 })
|
||||||
|
@Column({ name: 'hourly_rate', type: 'decimal', precision: 8, scale: 2, nullable: true })
|
||||||
|
hourlyRate: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Daily rate in USD', example: 150.00 })
|
||||||
|
@Column({ name: 'daily_rate', type: 'decimal', precision: 8, scale: 2, nullable: true })
|
||||||
|
dailyRate: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Biography' })
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
bio: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Certifications' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
certifications: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Average rating', example: 4.8 })
|
||||||
|
@Column({ type: 'decimal', precision: 3, scale: 2, nullable: true })
|
||||||
|
rating: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total reviews' })
|
||||||
|
@Column({ name: 'total_reviews', default: 0 })
|
||||||
|
totalReviews: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total tours completed' })
|
||||||
|
@Column({ name: 'total_tours', default: 0 })
|
||||||
|
totalTours: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is verified', example: false })
|
||||||
|
@Column({ name: 'is_verified', default: false })
|
||||||
|
isVerified: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is available', example: true })
|
||||||
|
@Column({ name: 'is_available', default: true })
|
||||||
|
isAvailable: boolean;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
73
src/entities/transaction.entity.ts
Executable file
73
src/entities/transaction.entity.ts
Executable file
@@ -0,0 +1,73 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
import { Establishment } from './establishment.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'transactions', schema: 'commerce' })
|
||||||
|
export class Transaction extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id', nullable: true })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Establishment ID' })
|
||||||
|
@Column({ name: 'establishment_id', nullable: true })
|
||||||
|
establishmentId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Payment method ID' })
|
||||||
|
@Column({ name: 'payment_method_id', nullable: true })
|
||||||
|
paymentMethodId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Reference type', example: 'reservation' })
|
||||||
|
@Column({ name: 'reference_type', length: 20, nullable: true })
|
||||||
|
referenceType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Reference ID' })
|
||||||
|
@Column({ name: 'reference_id', nullable: true })
|
||||||
|
referenceId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Transaction amount', example: 240.00 })
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
amount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Currency', example: 'USD' })
|
||||||
|
@Column({ length: 3, default: 'USD' })
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Platform commission percentage', example: 0.05 })
|
||||||
|
@Column({ name: 'platform_commission', type: 'decimal', precision: 5, scale: 4, nullable: true })
|
||||||
|
platformCommission: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Commission amount', example: 12.00 })
|
||||||
|
@Column({ name: 'commission_amount', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||||
|
commissionAmount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Net amount to establishment', example: 228.00 })
|
||||||
|
@Column({ name: 'net_amount', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||||
|
netAmount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Transaction status', example: 'completed' })
|
||||||
|
@Column({ length: 20, default: 'pending' })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Gateway transaction ID' })
|
||||||
|
@Column({ name: 'gateway_transaction_id', length: 255, nullable: true })
|
||||||
|
gatewayTransactionId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Gateway response' })
|
||||||
|
@Column({ name: 'gateway_response', type: 'jsonb', nullable: true })
|
||||||
|
gatewayResponse: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Processed at' })
|
||||||
|
@Column({ name: 'processed_at', type: 'timestamp', nullable: true })
|
||||||
|
processedAt: Date;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => Establishment)
|
||||||
|
@JoinColumn({ name: 'establishment_id' })
|
||||||
|
establishment: Establishment;
|
||||||
|
}
|
||||||
117
src/entities/ugc-content.entity.ts
Normal file
117
src/entities/ugc-content.entity.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'ugc_content', schema: 'social_commerce' })
|
||||||
|
export class UGCContent extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Content creator user ID' })
|
||||||
|
@Column({ name: 'creator_id' })
|
||||||
|
creatorId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Content title' })
|
||||||
|
@Column({ length: 200 })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Content description' })
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Content type' })
|
||||||
|
@Column({ name: 'content_type', length: 50 })
|
||||||
|
contentType: string; // photo, video, story, reel, blog-post, review
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Content URLs and media' })
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
media: {
|
||||||
|
primaryUrl: string;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
additionalUrls: string[];
|
||||||
|
duration: number; // for videos
|
||||||
|
format: string;
|
||||||
|
resolution: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Location and place tags' })
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
location: {
|
||||||
|
placeName: string;
|
||||||
|
placeId: string;
|
||||||
|
coordinates: { lat: number; lng: number };
|
||||||
|
city: string;
|
||||||
|
country: string;
|
||||||
|
placeType: string; // restaurant, hotel, attraction, etc.
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Content tags and categories' })
|
||||||
|
@Column({ type: 'text', array: true })
|
||||||
|
tags: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Engagement metrics' })
|
||||||
|
@Column({ name: 'engagement_metrics', type: 'jsonb' })
|
||||||
|
engagementMetrics: {
|
||||||
|
views: number;
|
||||||
|
likes: number;
|
||||||
|
comments: number;
|
||||||
|
shares: number;
|
||||||
|
saves: number;
|
||||||
|
clickThroughs: number;
|
||||||
|
engagementRate: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Monetization and tokenization' })
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
monetization: {
|
||||||
|
isTokenized: boolean;
|
||||||
|
nftId: string;
|
||||||
|
licenseType: string; // free, paid, exclusive
|
||||||
|
price: number;
|
||||||
|
royaltyPercentage: number;
|
||||||
|
licensePurchases: number;
|
||||||
|
totalEarnings: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'AI content analysis' })
|
||||||
|
@Column({ name: 'ai_analysis', type: 'jsonb' })
|
||||||
|
aiAnalysis: {
|
||||||
|
contentQualityScore: number;
|
||||||
|
visualAppealScore: number;
|
||||||
|
authenticityScore: number;
|
||||||
|
brandSafety: boolean;
|
||||||
|
sentimentScore: number;
|
||||||
|
objectsDetected: string[];
|
||||||
|
colorsAnalysis: string[];
|
||||||
|
textAnalysis: {
|
||||||
|
language: string;
|
||||||
|
sentiment: string;
|
||||||
|
topics: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Usage rights and licensing' })
|
||||||
|
@Column({ name: 'usage_rights', type: 'jsonb' })
|
||||||
|
usageRights: {
|
||||||
|
isAvailableForLicensing: boolean;
|
||||||
|
exclusivityLevel: string; // non-exclusive, semi-exclusive, exclusive
|
||||||
|
geographicRights: string[];
|
||||||
|
durationRights: string;
|
||||||
|
usageTypes: string[]; // commercial, editorial, social-media
|
||||||
|
restrictions: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Verification and authenticity' })
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
verification: {
|
||||||
|
isVerified: boolean;
|
||||||
|
verificationMethod: string;
|
||||||
|
locationVerified: boolean;
|
||||||
|
timestampVerified: boolean;
|
||||||
|
metadataIntact: boolean;
|
||||||
|
blockchainHash: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'creator_id' })
|
||||||
|
creator: User;
|
||||||
|
}
|
||||||
82
src/entities/user-personalization.entity.ts
Normal file
82
src/entities/user-personalization.entity.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { Entity, Column, OneToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'user_personalization', schema: 'analytics' })
|
||||||
|
export class UserPersonalization extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id', unique: true })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Travel preferences and interests' })
|
||||||
|
@Column({ name: 'travel_preferences', type: 'jsonb', nullable: true })
|
||||||
|
travelPreferences: {
|
||||||
|
styles: string[]; // adventure, luxury, cultural, beach, family
|
||||||
|
activities: string[]; // hiking, dining, nightlife, museums, sports
|
||||||
|
accommodationTypes: string[]; // hotel, resort, boutique, vacation-rental
|
||||||
|
budgetRange: { min: number; max: number; currency: string };
|
||||||
|
groupSize: number;
|
||||||
|
travelFrequency: string; // frequent, occasional, rare
|
||||||
|
seasonPreferences: string[]; // dry-season, wet-season, year-round
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Behavioral patterns from app usage' })
|
||||||
|
@Column({ name: 'behavior_patterns', type: 'jsonb', nullable: true })
|
||||||
|
behaviorPatterns: {
|
||||||
|
searchHistory: Array<{ query: string; timestamp: Date; category: string }>;
|
||||||
|
clickPatterns: Array<{ itemType: string; itemId: string; frequency: number }>;
|
||||||
|
bookingHistory: Array<{ type: string; category: string; priceRange: string; rating: number }>;
|
||||||
|
timePatterns: { preferredBookingTime: string; advanceBookingDays: number };
|
||||||
|
deviceUsage: { platform: string; sessionDuration: number; featuresUsed: string[] };
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'AI-generated user insights' })
|
||||||
|
@Column({ name: 'ai_insights', type: 'jsonb', nullable: true })
|
||||||
|
aiInsights: {
|
||||||
|
personalityType: string; // explorer, luxury-seeker, budget-conscious, family-focused
|
||||||
|
predictedInterests: string[];
|
||||||
|
riskProfile: string; // conservative, moderate, adventurous
|
||||||
|
socialProfile: string; // solo, couple, group, family
|
||||||
|
valueDrivers: string[]; // price, quality, uniqueness, convenience, safety
|
||||||
|
seasonalTrends: Array<{ season: string; preferredActivities: string[] }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Location and geographic preferences' })
|
||||||
|
@Column({ name: 'location_preferences', type: 'jsonb', nullable: true })
|
||||||
|
locationPreferences: {
|
||||||
|
favoriteDestinations: string[];
|
||||||
|
avoidedLocations: string[];
|
||||||
|
preferredClimate: string[]; // tropical, temperate, humid, dry
|
||||||
|
urbanVsNature: string; // urban, nature, mixed
|
||||||
|
crowdTolerance: string; // loves-crowds, moderate, prefers-quiet
|
||||||
|
accessibilityNeeds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Dynamic scoring for recommendation engine' })
|
||||||
|
@Column({ name: 'recommendation_scores', type: 'jsonb', nullable: true })
|
||||||
|
recommendationScores: {
|
||||||
|
cuisinePreferences: Record<string, number>; // { italian: 0.8, local: 0.9, asian: 0.3 }
|
||||||
|
activityScores: Record<string, number>; // { adventure: 0.7, cultural: 0.9, nightlife: 0.2 }
|
||||||
|
priceSegments: Record<string, number>; // { budget: 0.2, mid: 0.8, luxury: 0.3 }
|
||||||
|
accommodationScores: Record<string, number>;
|
||||||
|
seasonalScores: Record<string, number>;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Last profile update timestamp' })
|
||||||
|
@Column({ name: 'last_analysis_date', type: 'timestamp', nullable: true })
|
||||||
|
lastAnalysisDate: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Profile completeness percentage' })
|
||||||
|
@Column({ name: 'profile_completeness', type: 'decimal', precision: 5, scale: 2, default: 0 })
|
||||||
|
profileCompleteness: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Number of data points used for personalization' })
|
||||||
|
@Column({ name: 'data_points_count', default: 0 })
|
||||||
|
dataPointsCount: number;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@OneToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
53
src/entities/user-preferences.entity.ts
Executable file
53
src/entities/user-preferences.entity.ts
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Entity, Column, OneToOne, JoinColumn, ManyToOne } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
import { Language } from './language.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'user_preferences', schema: 'auth' })
|
||||||
|
export class UserPreferences extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id' })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Language code', example: 'en' })
|
||||||
|
@Column({ name: 'language_code', length: 5, default: 'en' })
|
||||||
|
languageCode: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Currency', example: 'USD' })
|
||||||
|
@Column({ length: 3, default: 'USD' })
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Timezone', example: 'America/New_York' })
|
||||||
|
@Column({ length: 50, default: 'America/New_York' })
|
||||||
|
timezone: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Distance unit', example: 'miles' })
|
||||||
|
@Column({ name: 'distance_unit', length: 10, default: 'miles' })
|
||||||
|
distanceUnit: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Temperature unit', example: 'F' })
|
||||||
|
@Column({ name: 'temperature_unit', length: 1, default: 'F' })
|
||||||
|
temperatureUnit: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Date format', example: 'MM/DD/YYYY' })
|
||||||
|
@Column({ name: 'date_format', length: 20, default: 'MM/DD/YYYY' })
|
||||||
|
dateFormat: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Time format', example: '12h' })
|
||||||
|
@Column({ name: 'time_format', length: 5, default: '12h' })
|
||||||
|
timeFormat: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Accessibility features' })
|
||||||
|
@Column({ name: 'accessibility_features', type: 'jsonb', nullable: true })
|
||||||
|
accessibilityFeatures: Record<string, any>;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@OneToOne(() => User, user => user.preferences)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => Language)
|
||||||
|
@JoinColumn({ name: 'language_code', referencedColumnName: 'code' })
|
||||||
|
language: Language;
|
||||||
|
}
|
||||||
94
src/entities/user.entity.ts
Executable file
94
src/entities/user.entity.ts
Executable file
@@ -0,0 +1,94 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn, OneToMany, OneToOne } from 'typeorm';
|
||||||
|
import { ApiProperty, ApiHideProperty } from '@nestjs/swagger';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { Country } from './country.entity';
|
||||||
|
import { Language } from './language.entity';
|
||||||
|
import { Role } from './role.entity';
|
||||||
|
import { UserPreferences } from './user-preferences.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'users', schema: 'auth' })
|
||||||
|
export class User extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User email', example: 'tourist@example.com' })
|
||||||
|
@Column({ unique: true })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiHideProperty()
|
||||||
|
@Exclude()
|
||||||
|
@Column({ name: 'password_hash' })
|
||||||
|
passwordHash: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'First name', example: 'John' })
|
||||||
|
@Column({ name: 'first_name', length: 100 })
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Last name', example: 'Doe' })
|
||||||
|
@Column({ name: 'last_name', length: 100 })
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Phone number', example: '+1234567890' })
|
||||||
|
@Column({ nullable: true, length: 20 })
|
||||||
|
phone: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Country ID', example: 1 })
|
||||||
|
@Column({ name: 'country_id', nullable: true })
|
||||||
|
countryId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Preferred language', example: 'en' })
|
||||||
|
@Column({ name: 'preferred_language', length: 5, default: 'en' })
|
||||||
|
preferredLanguage: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Preferred currency', example: 'USD' })
|
||||||
|
@Column({ name: 'preferred_currency', length: 3, default: 'USD' })
|
||||||
|
preferredCurrency: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Role ID', example: 2 })
|
||||||
|
@Column({ name: 'role_id', nullable: true })
|
||||||
|
roleId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Profile image URL' })
|
||||||
|
@Column({ name: 'profile_image_url', type: 'text', nullable: true })
|
||||||
|
profileImageUrl: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Email verified status', example: false })
|
||||||
|
@Column({ name: 'is_verified', default: false })
|
||||||
|
isVerified: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Active status', example: true })
|
||||||
|
@Column({ name: 'is_active', default: true })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Last login timestamp' })
|
||||||
|
@Column({ name: 'last_login', type: 'timestamp', nullable: true })
|
||||||
|
lastLogin: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Failed login attempts', example: 0 })
|
||||||
|
@Column({ name: 'failed_login_attempts', default: 0 })
|
||||||
|
failedLoginAttempts: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Account locked until' })
|
||||||
|
@Column({ name: 'locked_until', type: 'timestamp', nullable: true })
|
||||||
|
lockedUntil: Date;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => Country, country => country.users)
|
||||||
|
@JoinColumn({ name: 'country_id' })
|
||||||
|
country: Country;
|
||||||
|
|
||||||
|
@ManyToOne(() => Language, language => language.users)
|
||||||
|
@JoinColumn({ name: 'preferred_language', referencedColumnName: 'code' })
|
||||||
|
preferredLanguageEntity: Language;
|
||||||
|
|
||||||
|
@ManyToOne(() => Role, role => role.users)
|
||||||
|
@JoinColumn({ name: 'role_id' })
|
||||||
|
role: Role;
|
||||||
|
|
||||||
|
@OneToOne(() => UserPreferences, preferences => preferences.user)
|
||||||
|
preferences: UserPreferences;
|
||||||
|
|
||||||
|
// Virtual fields
|
||||||
|
@ApiProperty({ description: 'Full name', example: 'John Doe' })
|
||||||
|
get fullName(): string {
|
||||||
|
return `${this.firstName} ${this.lastName}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
96
src/entities/vehicle.entity.ts
Normal file
96
src/entities/vehicle.entity.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'vehicles', schema: 'tourism' })
|
||||||
|
export class Vehicle extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'Vehicle owner user ID' })
|
||||||
|
@Column({ name: 'owner_id' })
|
||||||
|
ownerId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Vehicle type', example: 'car' })
|
||||||
|
@Column({ name: 'vehicle_type', length: 50 })
|
||||||
|
vehicleType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Vehicle brand', example: 'Toyota' })
|
||||||
|
@Column({ length: 50 })
|
||||||
|
brand: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Vehicle model', example: 'Corolla' })
|
||||||
|
@Column({ length: 50 })
|
||||||
|
model: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Manufacturing year', example: 2020 })
|
||||||
|
@Column()
|
||||||
|
year: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'License plate', example: 'A123456' })
|
||||||
|
@Column({ name: 'license_plate', length: 20, unique: true })
|
||||||
|
licensePlate: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Vehicle color', example: 'White' })
|
||||||
|
@Column({ length: 30 })
|
||||||
|
color: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Seating capacity', example: 5 })
|
||||||
|
@Column()
|
||||||
|
capacity: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Transmission type', example: 'automatic' })
|
||||||
|
@Column({ name: 'transmission_type', length: 20 })
|
||||||
|
transmissionType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Fuel type', example: 'gasoline' })
|
||||||
|
@Column({ name: 'fuel_type', length: 20 })
|
||||||
|
fuelType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Daily rental rate' })
|
||||||
|
@Column({ name: 'daily_rate', type: 'decimal', precision: 8, scale: 2 })
|
||||||
|
dailyRate: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Currency', example: 'USD' })
|
||||||
|
@Column({ length: 3, default: 'USD' })
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Vehicle features' })
|
||||||
|
@Column({ type: 'text', array: true, nullable: true })
|
||||||
|
features: string[] | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Vehicle images' })
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
images: Record<string, any>[] | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Current location' })
|
||||||
|
@Column({ name: 'current_location', type: 'point', nullable: true })
|
||||||
|
currentLocation: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Insurance info' })
|
||||||
|
@Column({ name: 'insurance_info', type: 'jsonb', nullable: true })
|
||||||
|
insuranceInfo: Record<string, any> | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Maintenance records' })
|
||||||
|
@Column({ name: 'maintenance_records', type: 'jsonb', nullable: true })
|
||||||
|
maintenanceRecords: Record<string, any>[] | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is available for rental', example: true })
|
||||||
|
@Column({ name: 'is_available', default: true })
|
||||||
|
isAvailable: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Is verified', example: false })
|
||||||
|
@Column({ name: 'is_verified', default: false })
|
||||||
|
isVerified: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Average rating', example: 4.2 })
|
||||||
|
@Column({ type: 'decimal', precision: 3, scale: 2, nullable: true })
|
||||||
|
rating: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total rentals' })
|
||||||
|
@Column({ name: 'total_rentals', default: 0 })
|
||||||
|
totalRentals: number;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'owner_id' })
|
||||||
|
owner: User;
|
||||||
|
}
|
||||||
99
src/entities/wearable-device.entity.ts
Normal file
99
src/entities/wearable-device.entity.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'wearable_devices', schema: 'smart_tourism' })
|
||||||
|
export class WearableDevice extends BaseEntity {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@Column({ name: 'user_id' })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Device identifier' })
|
||||||
|
@Column({ name: 'device_identifier', unique: true })
|
||||||
|
deviceIdentifier: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Device type' })
|
||||||
|
@Column({ name: 'device_type', length: 50 })
|
||||||
|
deviceType: string; // smartwatch, fitness-tracker, ar-glasses, smart-band
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Device brand and model' })
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
deviceInfo: {
|
||||||
|
brand: string;
|
||||||
|
model: string;
|
||||||
|
osVersion: string;
|
||||||
|
appVersion: string;
|
||||||
|
batteryLevel: number;
|
||||||
|
isConnected: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Current tour session data' })
|
||||||
|
@Column({ name: 'tour_session', type: 'jsonb', nullable: true })
|
||||||
|
tourSession: {
|
||||||
|
sessionId: string;
|
||||||
|
tourId: string;
|
||||||
|
startTime: Date;
|
||||||
|
currentLocation: { lat: number; lng: number };
|
||||||
|
visitedWaypoints: string[];
|
||||||
|
completionPercentage: number;
|
||||||
|
estimatedTimeRemaining: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Real-time health and activity data' })
|
||||||
|
@Column({ name: 'health_data', type: 'jsonb' })
|
||||||
|
healthData: {
|
||||||
|
heartRate: number;
|
||||||
|
stepCount: number;
|
||||||
|
caloriesBurned: number;
|
||||||
|
distanceWalked: number; // meters
|
||||||
|
activityLevel: string; // sedentary, light, moderate, vigorous
|
||||||
|
stressLevel: number; // 0-100
|
||||||
|
hydrationReminders: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Device preferences and settings' })
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
preferences: {
|
||||||
|
notificationsEnabled: boolean;
|
||||||
|
vibrationEnabled: boolean;
|
||||||
|
audioGuidance: boolean;
|
||||||
|
languagePreference: string;
|
||||||
|
hapticFeedback: boolean;
|
||||||
|
emergencyContacts: string[];
|
||||||
|
privacySettings: {
|
||||||
|
shareLocation: boolean;
|
||||||
|
shareHealthData: boolean;
|
||||||
|
shareWithTourGroup: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Smart features and capabilities' })
|
||||||
|
@Column({ name: 'smart_features', type: 'jsonb' })
|
||||||
|
smartFeatures: {
|
||||||
|
gpsTracking: boolean;
|
||||||
|
heartRateMonitoring: boolean;
|
||||||
|
fallDetection: boolean;
|
||||||
|
sosButton: boolean;
|
||||||
|
nfcPayment: boolean;
|
||||||
|
cameraControl: boolean;
|
||||||
|
voiceCommands: boolean;
|
||||||
|
augmentedReality: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Device connectivity status' })
|
||||||
|
@Column({ name: 'connectivity_status', type: 'jsonb' })
|
||||||
|
connectivityStatus: {
|
||||||
|
lastSync: Date;
|
||||||
|
connectionType: string; // bluetooth, wifi, cellular
|
||||||
|
signalStrength: number; // 0-100
|
||||||
|
dataUsage: number; // MB
|
||||||
|
isOnline: boolean;
|
||||||
|
networkQuality: string; // poor, fair, good, excellent
|
||||||
|
};
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
112
src/main.ts
Executable file
112
src/main.ts
Executable file
@@ -0,0 +1,112 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { ValidationPipe, VersioningType } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
// No SSL - nginx lo maneja
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
const configService = app.get(ConfigService);
|
||||||
|
|
||||||
|
// Enable CORS
|
||||||
|
app.enableCors({
|
||||||
|
origin: (origin, callback) => {
|
||||||
|
callback(null, true);
|
||||||
|
},
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Requested-With', 'Origin'],
|
||||||
|
credentials: true,
|
||||||
|
preflightContinue: false,
|
||||||
|
optionsSuccessStatus: 204,
|
||||||
|
exposedHeaders: ['Set-Cookie']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Global prefix
|
||||||
|
app.setGlobalPrefix('api');
|
||||||
|
|
||||||
|
// API Versioning
|
||||||
|
app.enableVersioning({
|
||||||
|
type: VersioningType.URI,
|
||||||
|
defaultVersion: '1',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Global validation pipe
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
forbidNonWhitelisted: true,
|
||||||
|
transform: true,
|
||||||
|
transformOptions: {
|
||||||
|
enableImplicitConversion: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Swagger Documentation
|
||||||
|
const config = new DocumentBuilder()
|
||||||
|
.setTitle(configService.get<string>('app.name') || 'Karibeo API')
|
||||||
|
.setDescription(configService.get<string>('app.description') || 'Tourism API')
|
||||||
|
.setVersion(configService.get<string>('app.version') || '1.0.0')
|
||||||
|
.addBearerAuth(
|
||||||
|
{
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT',
|
||||||
|
name: 'JWT',
|
||||||
|
description: 'Enter JWT token',
|
||||||
|
in: 'header',
|
||||||
|
},
|
||||||
|
'JWT-auth',
|
||||||
|
)
|
||||||
|
.addTag('Authentication', 'User authentication and authorization')
|
||||||
|
.addTag('Users', 'User management operations')
|
||||||
|
.addTag('Tourism', 'Tourism-related operations')
|
||||||
|
.addTag('Commerce', 'Commerce and booking operations')
|
||||||
|
.addTag('Security', 'Security and emergency operations')
|
||||||
|
.addTag('Analytics', 'Analytics and metrics')
|
||||||
|
.addTag('Notifications', 'Push, Email, and WhatsApp notifications')
|
||||||
|
.addTag('Payments', 'Payment processing and transactions (Stripe)')
|
||||||
|
.addTag('Upload', 'File upload to AWS S3')
|
||||||
|
.addTag('Communication', 'Email and WhatsApp messaging')
|
||||||
|
.addTag('Restaurant', 'Restaurant Point of Sale (POS) system')
|
||||||
|
.addTag('Hotel', 'Hotel management (Rooms, Check-ins, Room Service)')
|
||||||
|
.addTag('AI Guide', 'AI-powered virtual tour guide and AR content')
|
||||||
|
.addTag('Geolocation', 'Location tracking, geofencing, smart navigation')
|
||||||
|
.addTag('Channel Management', 'Management of external distribution channels (OTAs)')
|
||||||
|
.addTag('Listings Management', 'Management of properties and tourism resources (hotels, vehicles, etc.)')
|
||||||
|
.addTag('Vehicle Management', 'Management and availability of rental vehicles')
|
||||||
|
.addTag('Flight Management', 'Flight search and booking operations')
|
||||||
|
.addTag('Availability Management', 'Generic availability management for all resources')
|
||||||
|
.addTag('Reviews', 'Advanced user reviews with multimedia and sentiment analysis')
|
||||||
|
.addTag('AI Generator', 'Generative AI content creation')
|
||||||
|
.addTag('Personalization', 'User experience personalization')
|
||||||
|
.addTag('Sustainability', 'Sustainable tourism tracking and eco-certifications')
|
||||||
|
.addTag('Social Commerce', 'Influencer marketing and UGC management')
|
||||||
|
.addTag('IoT Tourism', 'IoT device integration and smart tourism data')
|
||||||
|
.addTag('Finance', 'Commission rates, admin transactions, and settlements')
|
||||||
|
.addServer('https://karibeo.lesoluciones.net:8443', 'Production HTTPS')
|
||||||
|
.addServer('http://localhost:3000', 'Local development')
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
|
SwaggerModule.setup('api/docs', app, document, {
|
||||||
|
customSiteTitle: 'Karibeo API Documentation',
|
||||||
|
customfavIcon: '/favicon.ico',
|
||||||
|
customCssUrl: '/swagger-ui.css',
|
||||||
|
swaggerOptions: {
|
||||||
|
persistAuthorization: true,
|
||||||
|
displayRequestDuration: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Siempre puerto 3000 HTTP - nginx maneja SSL
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
await app.listen(port);
|
||||||
|
|
||||||
|
console.log(`Karibeo API is running on: http://localhost:${port}`);
|
||||||
|
console.log(`API Documentation: http://localhost:${port}/api/docs`);
|
||||||
|
console.log(`External access: https://karibeo.lesoluciones.net:8443`);
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap();
|
||||||
93
src/modules/ai-generator/ai-generator.controller.ts
Normal file
93
src/modules/ai-generator/ai-generator.controller.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
Controller, Get, Post, Body, Patch, Param, Query, UseGuards, Request
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { AIGeneratorService } from './ai-generator.service';
|
||||||
|
import { GenerateContentDto } from './dto/generate-content.dto';
|
||||||
|
import { ImproveContentDto } from './dto/improve-content.dto';
|
||||||
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
|
|
||||||
|
@ApiTags('AI Content Generator')
|
||||||
|
@Controller('ai-generator')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth('JWT-auth')
|
||||||
|
export class AIGeneratorController {
|
||||||
|
constructor(private readonly aiGeneratorService: AIGeneratorService) {}
|
||||||
|
|
||||||
|
@Post('generate')
|
||||||
|
@ApiOperation({ summary: 'Generate AI-powered travel content' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Content generated successfully'
|
||||||
|
})
|
||||||
|
generateContent(@Body() generateDto: GenerateContentDto, @Request() req) {
|
||||||
|
return this.aiGeneratorService.generateContent(generateDto, req.user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('improve')
|
||||||
|
@ApiOperation({ summary: 'Improve existing content with AI' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Content improved successfully'
|
||||||
|
})
|
||||||
|
improveContent(@Body() improveDto: ImproveContentDto, @Request() req) {
|
||||||
|
return this.aiGeneratorService.improveContent(improveDto, req.user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('itinerary/smart')
|
||||||
|
@ApiOperation({ summary: 'Generate smart AI itinerary' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Smart itinerary generated'
|
||||||
|
})
|
||||||
|
generateSmartItinerary(@Body() body: {
|
||||||
|
destinations: string[];
|
||||||
|
duration: number;
|
||||||
|
travelStyle: string;
|
||||||
|
budget: string;
|
||||||
|
}, @Request() req) {
|
||||||
|
return this.aiGeneratorService.generateSmartItinerary(
|
||||||
|
body.destinations,
|
||||||
|
body.duration,
|
||||||
|
body.travelStyle as any,
|
||||||
|
body.budget,
|
||||||
|
req.user.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('destination/:id/content')
|
||||||
|
@ApiOperation({ summary: 'Generate comprehensive destination content' })
|
||||||
|
@ApiParam({ name: 'id', type: 'string', description: 'Destination ID' })
|
||||||
|
generateDestinationContent(@Param('id') id: string, @Request() req) {
|
||||||
|
return this.aiGeneratorService.generateDestinationContent(id, req.user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('templates')
|
||||||
|
@ApiOperation({ summary: 'Get available content templates' })
|
||||||
|
getContentTemplates() {
|
||||||
|
return {
|
||||||
|
templates: [
|
||||||
|
{
|
||||||
|
id: 'itinerary',
|
||||||
|
name: 'Smart Itinerary',
|
||||||
|
description: 'AI-generated day-by-day travel itinerary',
|
||||||
|
parameters: ['destinations', 'duration', 'travelStyle', 'budget'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'blog-post',
|
||||||
|
name: 'Travel Blog Post',
|
||||||
|
description: 'Engaging travel blog content',
|
||||||
|
parameters: ['topic', 'length', 'tone', 'audience'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'destination-guide',
|
||||||
|
name: 'Destination Guide',
|
||||||
|
description: 'Comprehensive destination information',
|
||||||
|
parameters: ['destination', 'highlights', 'activities'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/modules/ai-generator/ai-generator.module.ts
Normal file
21
src/modules/ai-generator/ai-generator.module.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AIGeneratorService } from './ai-generator.service';
|
||||||
|
import { AIGeneratorController } from './ai-generator.controller';
|
||||||
|
import { AIGeneratedContent } from '../../entities/ai-generated-content.entity';
|
||||||
|
import { PlaceOfInterest } from '../../entities/place-of-interest.entity';
|
||||||
|
import { Establishment } from '../../entities/establishment.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
AIGeneratedContent,
|
||||||
|
PlaceOfInterest,
|
||||||
|
Establishment,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
controllers: [AIGeneratorController],
|
||||||
|
providers: [AIGeneratorService],
|
||||||
|
exports: [AIGeneratorService],
|
||||||
|
})
|
||||||
|
export class AIGeneratorModule {}
|
||||||
596
src/modules/ai-generator/ai-generator.service.ts
Normal file
596
src/modules/ai-generator/ai-generator.service.ts
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AIGeneratedContent } from '../../entities/ai-generated-content.entity';
|
||||||
|
import { PlaceOfInterest } from '../../entities/place-of-interest.entity';
|
||||||
|
import { Establishment } from '../../entities/establishment.entity';
|
||||||
|
import { GenerateContentDto, ContentType, TravelStyle } from './dto/generate-content.dto';
|
||||||
|
import { ImproveContentDto, ImprovementType } from './dto/improve-content.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AIGeneratorService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(AIGeneratedContent)
|
||||||
|
private readonly contentRepository: Repository<AIGeneratedContent>,
|
||||||
|
@InjectRepository(PlaceOfInterest)
|
||||||
|
private readonly placeRepository: Repository<PlaceOfInterest>,
|
||||||
|
@InjectRepository(Establishment)
|
||||||
|
private readonly establishmentRepository: Repository<Establishment>,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async generateContent(
|
||||||
|
generateDto: GenerateContentDto,
|
||||||
|
userId: string,
|
||||||
|
): Promise<{
|
||||||
|
content: string;
|
||||||
|
suggestions: string[];
|
||||||
|
relatedPlaces: any[];
|
||||||
|
estimatedCost: string;
|
||||||
|
aiModel: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// Get contextual data to enrich generation
|
||||||
|
const contextData = await this.getContextualData(generateDto);
|
||||||
|
|
||||||
|
// Generate content using AI (simulated with smart templates)
|
||||||
|
const generatedContent = await this.generateAIContent(generateDto, contextData);
|
||||||
|
|
||||||
|
// Generate related suggestions
|
||||||
|
const suggestions = this.generateSuggestions(generateDto);
|
||||||
|
|
||||||
|
// Get related places
|
||||||
|
const relatedPlaces = await this.getRelatedPlaces(generateDto);
|
||||||
|
|
||||||
|
// Calculate estimated cost
|
||||||
|
const estimatedCost = this.calculateEstimatedCost(generateDto, contextData);
|
||||||
|
|
||||||
|
// Save generated content
|
||||||
|
const contentRecord = this.contentRepository.create({
|
||||||
|
userId,
|
||||||
|
contentType: generateDto.contentType,
|
||||||
|
userPrompt: generateDto.prompt,
|
||||||
|
generatedContent,
|
||||||
|
aiModel: 'karibeo-ai-v1.0',
|
||||||
|
language: generateDto.language || 'en',
|
||||||
|
metadata: {
|
||||||
|
travelStyle: generateDto.travelStyle,
|
||||||
|
duration: generateDto.duration,
|
||||||
|
budget: generateDto.budget,
|
||||||
|
destinations: generateDto.destinations,
|
||||||
|
generatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.contentRepository.save(contentRecord);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: generatedContent,
|
||||||
|
suggestions,
|
||||||
|
relatedPlaces,
|
||||||
|
estimatedCost,
|
||||||
|
aiModel: 'karibeo-ai-v1.0',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestException(`Error generating content: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async improveContent(
|
||||||
|
improveDto: ImproveContentDto,
|
||||||
|
userId: string,
|
||||||
|
): Promise<{ improvedContent: string; changes: string[] }> {
|
||||||
|
const improvedContent = await this.processContentImprovement(improveDto);
|
||||||
|
const changes = this.identifyChanges(improveDto.originalContent, improvedContent);
|
||||||
|
|
||||||
|
// Save improved version
|
||||||
|
const contentRecord = this.contentRepository.create({
|
||||||
|
userId,
|
||||||
|
contentType: 'improvement',
|
||||||
|
userPrompt: `Improve: ${improveDto.improvementType}`,
|
||||||
|
generatedContent: improvedContent,
|
||||||
|
aiModel: 'karibeo-ai-v1.0',
|
||||||
|
metadata: {
|
||||||
|
originalContent: improveDto.originalContent,
|
||||||
|
improvementType: improveDto.improvementType,
|
||||||
|
instructions: improveDto.instructions,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.contentRepository.save(contentRecord);
|
||||||
|
|
||||||
|
return { improvedContent, changes };
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateSmartItinerary(
|
||||||
|
destinations: string[],
|
||||||
|
duration: number,
|
||||||
|
travelStyle: TravelStyle,
|
||||||
|
budget: string,
|
||||||
|
userId: string,
|
||||||
|
): Promise<{
|
||||||
|
itinerary: any;
|
||||||
|
dayByDay: any[];
|
||||||
|
recommendations: string[];
|
||||||
|
totalEstimatedCost: string;
|
||||||
|
}> {
|
||||||
|
// Get real data from places and establishments
|
||||||
|
const places = await this.placeRepository.find({
|
||||||
|
where: { active: true },
|
||||||
|
order: { rating: 'DESC' },
|
||||||
|
take: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
const establishments = await this.establishmentRepository.find({
|
||||||
|
order: { rating: 'DESC' },
|
||||||
|
take: 15,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate smart day-by-day itinerary
|
||||||
|
const dayByDay = this.generateDayByDayItinerary(
|
||||||
|
places,
|
||||||
|
establishments,
|
||||||
|
duration,
|
||||||
|
travelStyle,
|
||||||
|
);
|
||||||
|
|
||||||
|
const itinerary = {
|
||||||
|
title: `${duration}-Day Itinerary: Dominican Republic & Puerto Rico`,
|
||||||
|
overview: this.generateItineraryOverview(travelStyle, duration),
|
||||||
|
highlights: this.generateHighlights(places.slice(0, 5)),
|
||||||
|
bestTimeToVisit: this.getBestTimeToVisit(),
|
||||||
|
tips: this.generateTravelTips(travelStyle),
|
||||||
|
};
|
||||||
|
|
||||||
|
const recommendations = this.generateSmartRecommendations(travelStyle, budget);
|
||||||
|
const totalEstimatedCost = this.calculateTotalItineraryCost(dayByDay, budget);
|
||||||
|
|
||||||
|
return {
|
||||||
|
itinerary,
|
||||||
|
dayByDay,
|
||||||
|
recommendations,
|
||||||
|
totalEstimatedCost,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateDestinationContent(
|
||||||
|
destinationId: string,
|
||||||
|
userId: string,
|
||||||
|
): Promise<{
|
||||||
|
description: string;
|
||||||
|
highlights: string[];
|
||||||
|
activities: string[];
|
||||||
|
bestTime: string;
|
||||||
|
budget: string;
|
||||||
|
tips: string[];
|
||||||
|
}> {
|
||||||
|
const place = await this.placeRepository.findOne({
|
||||||
|
where: { id: destinationId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!place) {
|
||||||
|
throw new BadRequestException('Destination not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate enriched destination content
|
||||||
|
const content = {
|
||||||
|
description: this.generateDestinationDescription(place),
|
||||||
|
highlights: this.generateDestinationHighlights(place),
|
||||||
|
activities: this.generateActivitiesList(place),
|
||||||
|
bestTime: this.generateBestTimeToVisit(place),
|
||||||
|
budget: this.generateBudgetInfo(place),
|
||||||
|
tips: this.generateLocalTips(place),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save generated content
|
||||||
|
const contentRecord = this.contentRepository.create({
|
||||||
|
userId,
|
||||||
|
contentType: ContentType.DESTINATION_GUIDE,
|
||||||
|
userPrompt: `Generate content for ${place.name}`,
|
||||||
|
generatedContent: JSON.stringify(content),
|
||||||
|
aiModel: 'karibeo-ai-v1.0',
|
||||||
|
metadata: { placeId: destinationId, placeName: place.name },
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.contentRepository.save(contentRecord);
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIVATE AI GENERATION METHODS
|
||||||
|
private async generateAIContent(
|
||||||
|
generateDto: GenerateContentDto,
|
||||||
|
contextData: any,
|
||||||
|
): Promise<string> {
|
||||||
|
const templates = {
|
||||||
|
[ContentType.ITINERARY]: this.generateItineraryContent,
|
||||||
|
[ContentType.BLOG_POST]: this.generateBlogContent,
|
||||||
|
[ContentType.DESTINATION_GUIDE]: this.generateGuideContent,
|
||||||
|
[ContentType.TRAVEL_TIPS]: this.generateTipsContent,
|
||||||
|
[ContentType.SOCIAL_POST]: this.generateSocialContent,
|
||||||
|
};
|
||||||
|
|
||||||
|
const generator = templates[generateDto.contentType];
|
||||||
|
if (!generator) {
|
||||||
|
throw new BadRequestException('Unsupported content type');
|
||||||
|
}
|
||||||
|
|
||||||
|
return generator.call(this, generateDto, contextData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateItineraryContent(generateDto: GenerateContentDto, contextData: any): string {
|
||||||
|
const { duration = 3, travelStyle = TravelStyle.CULTURAL } = generateDto;
|
||||||
|
|
||||||
|
return `
|
||||||
|
# ${duration}-Day Itinerary: ${generateDto.destinations?.join(', ') || 'Dominican Republic & Puerto Rico'}
|
||||||
|
|
||||||
|
## 🌟 Overview
|
||||||
|
Discover the best of the Caribbean in ${duration} days with a ${travelStyle} focus. This itinerary is designed to maximize your experience and create unforgettable memories in the Hispanic Caribbean.
|
||||||
|
|
||||||
|
${this.generateDetailedDayPlan(duration, travelStyle, contextData)}
|
||||||
|
|
||||||
|
## 💰 Estimated Budget
|
||||||
|
${generateDto.budget || 'Contact for current pricing'}
|
||||||
|
|
||||||
|
## 📋 Important Tips
|
||||||
|
- Bring sunscreen SPF 50+ and insect repellent
|
||||||
|
- Learn basic Spanish phrases (English widely spoken in tourist areas)
|
||||||
|
- Keep your documents secure
|
||||||
|
- Respect local culture and environment
|
||||||
|
- Stay hydrated and drink bottled water
|
||||||
|
|
||||||
|
## 🎯 Must-See Highlights
|
||||||
|
${contextData.topPlaces?.map(place => `- **${place.name}**: ${place.description?.substring(0, 100)}...`).join('\n') || ''}
|
||||||
|
|
||||||
|
*Generated by Karibeo AI - Your intelligent travel assistant*
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateBlogContent(generateDto: GenerateContentDto, contextData: any): string {
|
||||||
|
return `
|
||||||
|
# ${this.generateCatchyTitle(generateDto)}
|
||||||
|
|
||||||
|
Planning your next Caribbean adventure? The Dominican Republic and Puerto Rico offer unique experiences ranging from pristine beaches to rich colonial history.
|
||||||
|
|
||||||
|
## Why Choose the Hispanic Caribbean
|
||||||
|
|
||||||
|
The Hispanic Caribbean perfectly blends vibrant Latin culture with spectacular tropical landscapes. From the cobblestone streets of Santo Domingo's Colonial Zone to Puerto Rico's bioluminescent bays, every moment brings new discoveries.
|
||||||
|
|
||||||
|
## Unmissable Experiences
|
||||||
|
|
||||||
|
${contextData.experiences?.map(exp => `### ${exp.name}\n${exp.description}`).join('\n\n') || ''}
|
||||||
|
|
||||||
|
*Ready for your Caribbean adventure? Download Karibeo and start planning today.*
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateGuideContent(generateDto: GenerateContentDto, contextData: any): string {
|
||||||
|
return `
|
||||||
|
# Complete Guide: ${generateDto.destinations?.join(' & ') || 'Caribbean Destinations'}
|
||||||
|
|
||||||
|
## 🏝️ Introduction
|
||||||
|
This comprehensive guide will take you through the best destinations, activities, and experiences you cannot miss in the Hispanic Caribbean.
|
||||||
|
|
||||||
|
*Guide generated by Karibeo AI with up-to-date information*
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateTipsContent(generateDto: GenerateContentDto, contextData: any): string {
|
||||||
|
const tips = [
|
||||||
|
'🌴 **Best Time to Visit:** December to April for drier weather',
|
||||||
|
'💵 **Currency:** Dominican Peso (DOP) in DR, US Dollar in Puerto Rico',
|
||||||
|
'🗣️ **Language:** Spanish (English widely spoken in tourist areas)',
|
||||||
|
'📱 **Connectivity:** Free WiFi in most hotels and restaurants',
|
||||||
|
];
|
||||||
|
|
||||||
|
return `
|
||||||
|
# Essential Tips for Your Caribbean Journey
|
||||||
|
|
||||||
|
## 🎯 Fundamental Tips
|
||||||
|
|
||||||
|
${tips.join('\n')}
|
||||||
|
|
||||||
|
*Tips updated by the Karibeo community*
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateSocialContent(generateDto: GenerateContentDto, contextData: any): string {
|
||||||
|
const socialPosts = [
|
||||||
|
'🏝️ Discovering paradise in the Dominican Republic! #Karibeo #DominicanRepublic #Paradise',
|
||||||
|
'🌅 Magical sunrise at [Destination]... no filter can capture this beauty! #Caribbean #Sunrise #Karibeo',
|
||||||
|
'🍹 Tasting local flavors... this local dish is delicious! #FoodLover #CaribbeanFood #Karibeo',
|
||||||
|
];
|
||||||
|
|
||||||
|
return socialPosts[Math.floor(Math.random() * socialPosts.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getContextualData(generateDto: GenerateContentDto): Promise<any> {
|
||||||
|
const places = await this.placeRepository.find({
|
||||||
|
where: { active: true },
|
||||||
|
order: { rating: 'DESC' },
|
||||||
|
take: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const establishments = await this.establishmentRepository.find({
|
||||||
|
order: { rating: 'DESC' },
|
||||||
|
take: 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
topPlaces: places,
|
||||||
|
topEstablishments: establishments,
|
||||||
|
experiences: this.generateExperiencesList(),
|
||||||
|
topDestinations: places.slice(0, 5),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateExperiencesList(): any[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Colonial Zone Santo Domingo Tour',
|
||||||
|
description: 'Walk through the streets of the first city in the New World.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Saona Island Excursion',
|
||||||
|
description: 'Tropical paradise with crystal-clear waters and white sand.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateSuggestions(generateDto: GenerateContentDto): string[] {
|
||||||
|
const baseSuggestions = [
|
||||||
|
'Add local transportation information',
|
||||||
|
'Include vegetarian restaurant options',
|
||||||
|
'Add rainy day activities',
|
||||||
|
'Personalize based on traveler age',
|
||||||
|
];
|
||||||
|
|
||||||
|
return baseSuggestions.slice(0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getRelatedPlaces(generateDto: GenerateContentDto): Promise<any[]> {
|
||||||
|
return this.placeRepository.find({
|
||||||
|
where: { active: true },
|
||||||
|
order: { rating: 'DESC' },
|
||||||
|
take: 5,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateEstimatedCost(generateDto: GenerateContentDto, contextData: any): string {
|
||||||
|
const baseCosts = {
|
||||||
|
[TravelStyle.BUDGET]: { min: 80, max: 150 },
|
||||||
|
[TravelStyle.LUXURY]: { min: 300, max: 800 },
|
||||||
|
[TravelStyle.FAMILY]: { min: 120, max: 250 },
|
||||||
|
[TravelStyle.ROMANTIC]: { min: 200, max: 400 },
|
||||||
|
[TravelStyle.ADVENTURE]: { min: 100, max: 200 },
|
||||||
|
[TravelStyle.CULTURAL]: { min: 90, max: 180 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const style = generateDto.travelStyle || TravelStyle.BUDGET;
|
||||||
|
const costs = baseCosts[style] || baseCosts[TravelStyle.BUDGET];
|
||||||
|
const duration = generateDto.duration || 3;
|
||||||
|
|
||||||
|
const totalMin = costs.min * duration;
|
||||||
|
const totalMax = costs.max * duration;
|
||||||
|
|
||||||
|
return `$${totalMin} - $${totalMax} USD per person`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateDetailedDayPlan(duration: number, style: TravelStyle, contextData: any): string {
|
||||||
|
let plan = '';
|
||||||
|
for (let day = 1; day <= duration; day++) {
|
||||||
|
plan += `\n## 📅 Day ${day}\n`;
|
||||||
|
plan += this.generateDayActivities(day, style, contextData);
|
||||||
|
}
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateDayActivities(day: number, style: TravelStyle, contextData: any): string {
|
||||||
|
const activities = {
|
||||||
|
morning: this.getActivityByTime('morning', style),
|
||||||
|
afternoon: this.getActivityByTime('afternoon', style),
|
||||||
|
evening: this.getActivityByTime('evening', style),
|
||||||
|
};
|
||||||
|
|
||||||
|
return `
|
||||||
|
**🌅 Morning (9:00 AM - 12:00 PM)**
|
||||||
|
${activities.morning}
|
||||||
|
|
||||||
|
**☀️ Afternoon (1:00 PM - 6:00 PM)**
|
||||||
|
${activities.afternoon}
|
||||||
|
|
||||||
|
**🌆 Evening (7:00 PM - 11:00 PM)**
|
||||||
|
${activities.evening}
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getActivityByTime(time: string, style: TravelStyle): string {
|
||||||
|
const activities = {
|
||||||
|
morning: {
|
||||||
|
[TravelStyle.CULTURAL]: 'Visit the Cathedral Primada de America and Colonial Zone tour',
|
||||||
|
[TravelStyle.ADVENTURE]: 'Hiking in Los Haitises National Park',
|
||||||
|
[TravelStyle.BEACH]: 'Relaxation at Bavaro Beach with water sports',
|
||||||
|
[TravelStyle.LUXURY]: 'Gourmet breakfast followed by private spa session',
|
||||||
|
[TravelStyle.FAMILY]: 'Family-friendly beach time with shallow waters',
|
||||||
|
[TravelStyle.ROMANTIC]: 'Private sunrise breakfast on the beach',
|
||||||
|
},
|
||||||
|
afternoon: {
|
||||||
|
[TravelStyle.CULTURAL]: 'Museum of Royal Houses and Columbus Alcazar',
|
||||||
|
[TravelStyle.ADVENTURE]: 'Zip-lining and waterfalls in Jarabacoa',
|
||||||
|
[TravelStyle.BEACH]: 'Catamaran excursion to Saona Island',
|
||||||
|
[TravelStyle.LUXURY]: 'Private lunch with chef and exclusive tour',
|
||||||
|
[TravelStyle.FAMILY]: 'Aquarium visit and dolphin watching',
|
||||||
|
[TravelStyle.ROMANTIC]: 'Couple spa treatment and wine tasting',
|
||||||
|
},
|
||||||
|
evening: {
|
||||||
|
[TravelStyle.CULTURAL]: 'Dinner at Malecon with traditional music',
|
||||||
|
[TravelStyle.ADVENTURE]: 'Local dinner and rest for next day adventure',
|
||||||
|
[TravelStyle.BEACH]: 'Beachfront dinner with sunset views',
|
||||||
|
[TravelStyle.LUXURY]: 'Fine dining at five-star restaurant',
|
||||||
|
[TravelStyle.FAMILY]: 'Family dinner and evening entertainment',
|
||||||
|
[TravelStyle.ROMANTIC]: 'Romantic dinner under the stars',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return activities[time][style] || activities[time][TravelStyle.CULTURAL];
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateCatchyTitle(generateDto: GenerateContentDto): string {
|
||||||
|
const titles = [
|
||||||
|
'Your Caribbean Adventure Awaits: Complete 2025 Guide',
|
||||||
|
'Discover Paradise: The Best of Hispanic Caribbean',
|
||||||
|
'Dominican Republic & Puerto Rico: Your Dream Trip',
|
||||||
|
];
|
||||||
|
return titles[Math.floor(Math.random() * titles.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private processContentImprovement(improveDto: ImproveContentDto): Promise<string> {
|
||||||
|
const improvements = {
|
||||||
|
[ImprovementType.ENHANCE_DETAILS]: (content) =>
|
||||||
|
`${content}\n\n[Enhanced with specific information]`,
|
||||||
|
[ImprovementType.MAKE_SHORTER]: (content) =>
|
||||||
|
content.substring(0, Math.floor(content.length * 0.7)) + '...',
|
||||||
|
[ImprovementType.MAKE_LONGER]: (content) =>
|
||||||
|
`${content}\n\n[Expanded content with more details]`,
|
||||||
|
[ImprovementType.CHANGE_TONE]: (content) =>
|
||||||
|
`[Tone adjusted] ${content}`,
|
||||||
|
[ImprovementType.ADD_BUDGET_INFO]: (content) =>
|
||||||
|
`${content}\n\n💰 **Budget Information:** Cost details included.`,
|
||||||
|
[ImprovementType.LOCALIZE]: (content) =>
|
||||||
|
`[Content localized] ${content}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const improver = improvements[improveDto.improvementType];
|
||||||
|
return Promise.resolve(improver ? improver(improveDto.originalContent) : improveDto.originalContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private identifyChanges(original: string, improved: string): string[] {
|
||||||
|
return [
|
||||||
|
'Content enhanced with AI optimization',
|
||||||
|
'Information updated with latest data',
|
||||||
|
'Structure improved for better readability',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateDayByDayItinerary(places: any[], establishments: any[], duration: number, style: TravelStyle): any[] {
|
||||||
|
const itinerary: any[] = [];
|
||||||
|
|
||||||
|
for (let day = 1; day <= duration; day++) {
|
||||||
|
itinerary.push({
|
||||||
|
day,
|
||||||
|
title: `Day ${day}: ${this.getDayTheme(day, style)}`,
|
||||||
|
activities: this.generateDayActivitiesDetailed(day, places, establishments, style),
|
||||||
|
meals: this.generateMealSuggestions(establishments.slice(0, 3)),
|
||||||
|
estimatedCost: this.getDayCost(style),
|
||||||
|
tips: this.getDayTips(day, style),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return itinerary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDayTheme(day: number, style: TravelStyle): string {
|
||||||
|
const themes = {
|
||||||
|
1: 'Arrival and Orientation',
|
||||||
|
2: 'Cultural Exploration',
|
||||||
|
3: 'Adventure and Nature',
|
||||||
|
};
|
||||||
|
return themes[day] || 'Free Exploration';
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateDayActivitiesDetailed(day: number, places: any[], establishments: any[], style: TravelStyle): any[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
time: '09:00',
|
||||||
|
activity: places[day - 1]?.name || 'Morning exploration',
|
||||||
|
description: places[day - 1]?.description || 'Discover local attractions',
|
||||||
|
duration: '3 hours',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateMealSuggestions(establishments: any[]): any {
|
||||||
|
return {
|
||||||
|
breakfast: {
|
||||||
|
restaurant: establishments[0]?.name || 'Local breakfast spot',
|
||||||
|
recommendation: 'Try traditional Caribbean breakfast',
|
||||||
|
averageCost: '$15-25 USD',
|
||||||
|
},
|
||||||
|
lunch: {
|
||||||
|
restaurant: establishments[1]?.name || 'Local restaurant',
|
||||||
|
recommendation: 'Sample local specialties',
|
||||||
|
averageCost: '$20-35 USD',
|
||||||
|
},
|
||||||
|
dinner: {
|
||||||
|
restaurant: establishments[2]?.name || 'Dinner venue',
|
||||||
|
recommendation: 'Fresh seafood with Caribbean flavors',
|
||||||
|
averageCost: '$30-50 USD',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDayCost(style: TravelStyle): number {
|
||||||
|
const costs = {
|
||||||
|
[TravelStyle.BUDGET]: 80,
|
||||||
|
[TravelStyle.LUXURY]: 400,
|
||||||
|
[TravelStyle.FAMILY]: 150,
|
||||||
|
[TravelStyle.ROMANTIC]: 250,
|
||||||
|
[TravelStyle.ADVENTURE]: 120,
|
||||||
|
[TravelStyle.CULTURAL]: 100,
|
||||||
|
};
|
||||||
|
return costs[style] || 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDayTips(day: number, style: TravelStyle): string[] {
|
||||||
|
return ['Enjoy your day!', 'Stay hydrated', 'Bring sunscreen'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateItineraryOverview(style: TravelStyle, duration: number): string {
|
||||||
|
return `Experience ${duration} days of ${style} adventures in the Caribbean.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateHighlights(places: any[]): string[] {
|
||||||
|
return places.map(place => `${place.name} - Must visit destination`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBestTimeToVisit(): string {
|
||||||
|
return 'December to April offers the best weather.';
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateTravelTips(style: TravelStyle): string[] {
|
||||||
|
return ['Pack light clothing', 'Bring sunscreen', 'Stay hydrated'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateSmartRecommendations(style: TravelStyle, budget: string): string[] {
|
||||||
|
return ['Use Karibeo app for bookings', 'Try local cuisine', 'Respect local culture'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateTotalItineraryCost(dayByDay: any[], budget: string): string {
|
||||||
|
return '$500-800 USD total estimated cost';
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateDestinationDescription(place: any): string {
|
||||||
|
return `${place.name} is a captivating destination with rating ${place.rating}/5.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateDestinationHighlights(place: any): string[] {
|
||||||
|
return ['Rich historical significance', 'Stunning views', 'Cultural experiences'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateActivitiesList(place: any): string[] {
|
||||||
|
return ['Guided tours', 'Photography', 'Cultural workshops'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateBestTimeToVisit(place: any): string {
|
||||||
|
return 'Best visited during morning hours (8:00 AM - 11:00 AM).';
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateBudgetInfo(place: any): string {
|
||||||
|
return 'Entry fees typically range from $5-15 USD per person.';
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateLocalTips(place: any): string[] {
|
||||||
|
return ['Wear comfortable shoes', 'Bring water', 'Respect photography rules'];
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/modules/ai-generator/dto/generate-content.dto.ts
Normal file
69
src/modules/ai-generator/dto/generate-content.dto.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { IsString, IsOptional, IsEnum, IsNumber, IsArray } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export enum ContentType {
|
||||||
|
ITINERARY = 'itinerary',
|
||||||
|
BLOG_POST = 'blog-post',
|
||||||
|
DESTINATION_GUIDE = 'destination-guide',
|
||||||
|
ACTIVITY_DESCRIPTION = 'activity-description',
|
||||||
|
RESTAURANT_REVIEW = 'restaurant-review',
|
||||||
|
TRAVEL_TIPS = 'travel-tips',
|
||||||
|
SOCIAL_POST = 'social-post'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TravelStyle {
|
||||||
|
ADVENTURE = 'adventure',
|
||||||
|
LUXURY = 'luxury',
|
||||||
|
BUDGET = 'budget',
|
||||||
|
FAMILY = 'family',
|
||||||
|
ROMANTIC = 'romantic',
|
||||||
|
SOLO = 'solo',
|
||||||
|
CULTURAL = 'cultural',
|
||||||
|
BEACH = 'beach',
|
||||||
|
ECO = 'eco'
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GenerateContentDto {
|
||||||
|
@ApiProperty({ description: 'Type of content to generate', enum: ContentType })
|
||||||
|
@IsEnum(ContentType)
|
||||||
|
contentType: ContentType;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'User prompt or request', example: 'Create a 3-day itinerary for Santo Domingo' })
|
||||||
|
@IsString()
|
||||||
|
prompt: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Travel style preference', enum: TravelStyle })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(TravelStyle)
|
||||||
|
travelStyle?: TravelStyle;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Duration in days for itineraries' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
duration?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Budget range', example: '$500-1000' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
budget?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Specific destinations or places' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
destinations?: string[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Content language', example: 'en' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
language?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Target audience', example: 'young couples' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
targetAudience?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Additional context or preferences' })
|
||||||
|
@IsOptional()
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
32
src/modules/ai-generator/dto/improve-content.dto.ts
Normal file
32
src/modules/ai-generator/dto/improve-content.dto.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { IsString, IsOptional, IsEnum } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export enum ImprovementType {
|
||||||
|
ENHANCE_DETAILS = 'enhance-details',
|
||||||
|
MAKE_SHORTER = 'make-shorter',
|
||||||
|
MAKE_LONGER = 'make-longer',
|
||||||
|
CHANGE_TONE = 'change-tone',
|
||||||
|
ADD_BUDGET_INFO = 'add-budget-info',
|
||||||
|
ADD_TIME_INFO = 'add-time-info',
|
||||||
|
LOCALIZE = 'localize'
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImproveContentDto {
|
||||||
|
@ApiProperty({ description: 'Original content to improve' })
|
||||||
|
@IsString()
|
||||||
|
originalContent: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Type of improvement', enum: ImprovementType })
|
||||||
|
@IsEnum(ImprovementType)
|
||||||
|
improvementType: ImprovementType;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Specific instructions for improvement' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
instructions?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Target language for localization' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
targetLanguage?: string;
|
||||||
|
}
|
||||||
162
src/modules/ai-guide/ai-guide.controller.ts
Normal file
162
src/modules/ai-guide/ai-guide.controller.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import {
|
||||||
|
Controller, Get, Post, Body, Patch, Param, Query, UseGuards, Request
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { AIGuideService } from './ai-guide.service';
|
||||||
|
import { AIQueryDto } from './dto/ai-query.dto';
|
||||||
|
import { ARContentQueryDto } from './dto/ar-content-query.dto';
|
||||||
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
|
import { RolesGuard } from '../../common/guards/roles.guard';
|
||||||
|
import { Roles } from '../../common/decorators/roles.decorator';
|
||||||
|
import { AIGuideInteraction } from '../../entities/ai-guide-interaction.entity';
|
||||||
|
import { ARContent } from '../../entities/ar-content.entity';
|
||||||
|
|
||||||
|
@ApiTags('AI Guide')
|
||||||
|
@Controller('ai-guide')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth('JWT-auth')
|
||||||
|
export class AIGuideController {
|
||||||
|
constructor(private readonly aiGuideService: AIGuideService) {}
|
||||||
|
|
||||||
|
@Post('query')
|
||||||
|
@ApiOperation({ summary: 'Ask AI guide a question' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'AI response generated successfully',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
response: { type: 'string' },
|
||||||
|
suggestions: { type: 'array', items: { type: 'string' } },
|
||||||
|
arContent: { type: 'array' },
|
||||||
|
nearbyPlaces: { type: 'array' },
|
||||||
|
audioGuideUrl: { type: 'string' },
|
||||||
|
sessionId: { type: 'string' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
async queryAIGuide(@Body() queryDto: AIQueryDto, @Request() req) {
|
||||||
|
return this.aiGuideService.processAIQuery(queryDto, req.user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('ar-content/nearby')
|
||||||
|
@ApiOperation({ summary: 'Get nearby AR content' })
|
||||||
|
@ApiResponse({ status: 200, type: [ARContent] })
|
||||||
|
async getNearbyARContent(@Body() queryDto: ARContentQueryDto) {
|
||||||
|
return this.aiGuideService.getNearbyARContent(queryDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('ar-content/:id/view')
|
||||||
|
@ApiOperation({ summary: 'Increment AR content view count' })
|
||||||
|
@ApiParam({ name: 'id', type: 'string' })
|
||||||
|
async incrementARView(@Param('id') id: string) {
|
||||||
|
await this.aiGuideService.incrementARViewCount(id);
|
||||||
|
return { success: true, message: 'View count incremented' };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('interactions/:id/rate')
|
||||||
|
@ApiOperation({ summary: 'Rate AI interaction' })
|
||||||
|
@ApiParam({ name: 'id', type: 'string' })
|
||||||
|
async rateInteraction(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: { rating: number },
|
||||||
|
@Request() req
|
||||||
|
) {
|
||||||
|
// TODO: Implement rating functionality
|
||||||
|
return { success: true, message: 'Rating saved' };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('stats')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('admin')
|
||||||
|
@ApiOperation({ summary: 'Get AI usage statistics (Admin only)' })
|
||||||
|
getAIUsageStats() {
|
||||||
|
return this.aiGuideService.getAIUsageStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMART RECOMMENDATIONS
|
||||||
|
@Get('recommendations/personalized')
|
||||||
|
@ApiOperation({ summary: 'Get personalized recommendations' })
|
||||||
|
@ApiQuery({ name: 'latitude', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'longitude', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'category', required: false, type: String })
|
||||||
|
async getPersonalizedRecommendations(
|
||||||
|
@Request() req,
|
||||||
|
@Query('latitude') latitude?: number,
|
||||||
|
@Query('longitude') longitude?: number,
|
||||||
|
@Query('category') category?: string,
|
||||||
|
) {
|
||||||
|
const queryDto: AIQueryDto = {
|
||||||
|
query: 'Show me personalized recommendations',
|
||||||
|
interactionType: 'recommendations' as any,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
metadata: { category }
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.aiGuideService.processAIQuery(queryDto, req.user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MONUMENT RECOGNITION
|
||||||
|
@Post('recognize-monument')
|
||||||
|
@ApiOperation({ summary: 'Recognize monument from image or location' })
|
||||||
|
async recognizeMonument(@Body() body: {
|
||||||
|
imageUrl?: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
}, @Request() req) {
|
||||||
|
const queryDto: AIQueryDto = {
|
||||||
|
query: 'What is this monument?',
|
||||||
|
interactionType: 'monument-recognition' as any,
|
||||||
|
imageUrl: body.imageUrl,
|
||||||
|
latitude: body.latitude,
|
||||||
|
longitude: body.longitude,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.aiGuideService.processAIQuery(queryDto, req.user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AUDIO GUIDES
|
||||||
|
@Post('audio-guide')
|
||||||
|
@ApiOperation({ summary: 'Generate audio guide for location' })
|
||||||
|
async generateAudioGuide(@Body() body: {
|
||||||
|
placeId?: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
language?: string;
|
||||||
|
}, @Request() req) {
|
||||||
|
const queryDto: AIQueryDto = {
|
||||||
|
query: 'Generate audio guide for this location',
|
||||||
|
interactionType: 'audio-guide' as any,
|
||||||
|
placeId: body.placeId,
|
||||||
|
latitude: body.latitude,
|
||||||
|
longitude: body.longitude,
|
||||||
|
language: body.language,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.aiGuideService.processAIQuery(queryDto, req.user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMART DIRECTIONS
|
||||||
|
@Post('directions')
|
||||||
|
@ApiOperation({ summary: 'Get smart directions with points of interest' })
|
||||||
|
async getSmartDirections(@Body() body: {
|
||||||
|
destinationPlaceId?: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
travelMode?: string;
|
||||||
|
}, @Request() req) {
|
||||||
|
const queryDto: AIQueryDto = {
|
||||||
|
query: `Get directions to ${body.destinationPlaceId || 'destination'}`,
|
||||||
|
interactionType: 'directions' as any,
|
||||||
|
placeId: body.destinationPlaceId,
|
||||||
|
latitude: body.latitude,
|
||||||
|
longitude: body.longitude,
|
||||||
|
metadata: { travelMode: body.travelMode || 'walking' }
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.aiGuideService.processAIQuery(queryDto, req.user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/modules/ai-guide/ai-guide.module.ts
Normal file
21
src/modules/ai-guide/ai-guide.module.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AIGuideService } from './ai-guide.service';
|
||||||
|
import { AIGuideController } from './ai-guide.controller';
|
||||||
|
import { AIGuideInteraction } from '../../entities/ai-guide-interaction.entity';
|
||||||
|
import { ARContent } from '../../entities/ar-content.entity';
|
||||||
|
import { PlaceOfInterest } from '../../entities/place-of-interest.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
AIGuideInteraction,
|
||||||
|
ARContent,
|
||||||
|
PlaceOfInterest,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
controllers: [AIGuideController],
|
||||||
|
providers: [AIGuideService],
|
||||||
|
exports: [AIGuideService],
|
||||||
|
})
|
||||||
|
export class AIGuideModule {}
|
||||||
372
src/modules/ai-guide/ai-guide.service.ts
Normal file
372
src/modules/ai-guide/ai-guide.service.ts
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { AIGuideInteraction } from '../../entities/ai-guide-interaction.entity';
|
||||||
|
import { ARContent } from '../../entities/ar-content.entity';
|
||||||
|
import { PlaceOfInterest } from '../../entities/place-of-interest.entity';
|
||||||
|
import { AIQueryDto, InteractionType } from './dto/ai-query.dto';
|
||||||
|
import { ARContentQueryDto } from './dto/ar-content-query.dto';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AIGuideService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(AIGuideInteraction)
|
||||||
|
private readonly interactionRepository: Repository<AIGuideInteraction>,
|
||||||
|
@InjectRepository(ARContent)
|
||||||
|
private readonly arContentRepository: Repository<ARContent>,
|
||||||
|
@InjectRepository(PlaceOfInterest)
|
||||||
|
private readonly placeRepository: Repository<PlaceOfInterest>,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async processAIQuery(queryDto: AIQueryDto, userId: string): Promise<{
|
||||||
|
response: string;
|
||||||
|
suggestions: string[];
|
||||||
|
arContent?: ARContent[];
|
||||||
|
nearbyPlaces?: PlaceOfInterest[];
|
||||||
|
audioGuideUrl?: string;
|
||||||
|
sessionId: string;
|
||||||
|
}> {
|
||||||
|
const sessionId = queryDto.sessionId || uuidv4();
|
||||||
|
|
||||||
|
let aiResponse = '';
|
||||||
|
let suggestions: string[] = [];
|
||||||
|
let arContent: ARContent[] = [];
|
||||||
|
let nearbyPlaces: PlaceOfInterest[] = [];
|
||||||
|
let audioGuideUrl: string = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (queryDto.interactionType) {
|
||||||
|
case InteractionType.MONUMENT_RECOGNITION:
|
||||||
|
const recognitionResult = await this.recognizeMonument(queryDto);
|
||||||
|
aiResponse = recognitionResult.response;
|
||||||
|
arContent = recognitionResult.arContent;
|
||||||
|
suggestions = recognitionResult.suggestions;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InteractionType.GENERAL_QUESTION:
|
||||||
|
aiResponse = await this.processGeneralQuestion(queryDto, sessionId);
|
||||||
|
suggestions = await this.generateSuggestions(queryDto.query, queryDto.language);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InteractionType.AR_CONTENT:
|
||||||
|
if (queryDto.latitude && queryDto.longitude) {
|
||||||
|
arContent = await this.findNearbyARContent(queryDto.latitude, queryDto.longitude);
|
||||||
|
}
|
||||||
|
aiResponse = `Found ${arContent.length} AR experiences near your location. Touch any item to activate the augmented reality experience.`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InteractionType.AUDIO_GUIDE:
|
||||||
|
const audioResult = await this.generateAudioGuide(queryDto);
|
||||||
|
aiResponse = audioResult.transcript;
|
||||||
|
audioGuideUrl = audioResult.audioUrl;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InteractionType.DIRECTIONS:
|
||||||
|
const directionsResult = await this.getSmartDirections(queryDto);
|
||||||
|
aiResponse = directionsResult.instructions;
|
||||||
|
nearbyPlaces = directionsResult.waypoints;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InteractionType.RECOMMENDATIONS:
|
||||||
|
nearbyPlaces = await this.getPersonalizedRecommendations(userId, queryDto);
|
||||||
|
aiResponse = this.formatRecommendations(nearbyPlaces);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
aiResponse = await this.processGeneralQuestion(queryDto, sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save interaction
|
||||||
|
await this.saveInteraction({
|
||||||
|
userId,
|
||||||
|
placeId: queryDto.placeId,
|
||||||
|
userQuery: queryDto.query,
|
||||||
|
aiResponse,
|
||||||
|
userLocation: queryDto.latitude && queryDto.longitude ?
|
||||||
|
`POINT(${queryDto.longitude} ${queryDto.latitude})` : undefined,
|
||||||
|
interactionType: queryDto.interactionType,
|
||||||
|
language: queryDto.language || 'en',
|
||||||
|
sessionId,
|
||||||
|
metadata: queryDto.metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: aiResponse,
|
||||||
|
suggestions,
|
||||||
|
arContent,
|
||||||
|
nearbyPlaces,
|
||||||
|
audioGuideUrl,
|
||||||
|
sessionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestException(`AI processing failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async recognizeMonument(queryDto: AIQueryDto): Promise<{
|
||||||
|
response: string;
|
||||||
|
arContent: ARContent[];
|
||||||
|
suggestions: string[];
|
||||||
|
}> {
|
||||||
|
// Simulate monument recognition using image analysis
|
||||||
|
// In production, this would use Google Vision API, AWS Rekognition, or custom ML model
|
||||||
|
|
||||||
|
let recognizedPlace: PlaceOfInterest | null = null;
|
||||||
|
|
||||||
|
if (queryDto.latitude && queryDto.longitude) {
|
||||||
|
// Find nearby monuments
|
||||||
|
const nearbyPlaces = await this.placeRepository
|
||||||
|
.createQueryBuilder('place')
|
||||||
|
.where('place.active = :active', { active: true })
|
||||||
|
.andWhere('place.category IN (:...categories)', {
|
||||||
|
categories: ['monument', 'historic-site', 'museum', 'landmark']
|
||||||
|
})
|
||||||
|
.orderBy('place.rating', 'DESC')
|
||||||
|
.limit(1)
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
recognizedPlace = nearbyPlaces[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!recognizedPlace) {
|
||||||
|
return {
|
||||||
|
response: "I can see this is a beautiful location, but I need more information to identify it precisely. Could you tell me where you are or provide more details?",
|
||||||
|
arContent: [],
|
||||||
|
suggestions: [
|
||||||
|
"Tell me your current location",
|
||||||
|
"What type of building is this?",
|
||||||
|
"Show me nearby attractions"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get AR content for recognized place
|
||||||
|
const arContent = await this.arContentRepository.find({
|
||||||
|
where: { placeId: recognizedPlace.id, isActive: true },
|
||||||
|
order: { viewsCount: 'DESC' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = this.generateMonumentDescription(recognizedPlace);
|
||||||
|
|
||||||
|
return {
|
||||||
|
response,
|
||||||
|
arContent,
|
||||||
|
suggestions: [
|
||||||
|
"Tell me more about its history",
|
||||||
|
"What can I do here?",
|
||||||
|
"Show me AR experience",
|
||||||
|
"Find nearby restaurants"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processGeneralQuestion(queryDto: AIQueryDto, sessionId: string): Promise<string> {
|
||||||
|
// This would integrate with OpenAI GPT, Google Bard, or custom NLP model
|
||||||
|
// For now, we'll simulate responses based on common tourism questions
|
||||||
|
|
||||||
|
const query = queryDto.query.toLowerCase();
|
||||||
|
const language = queryDto.language || 'en';
|
||||||
|
|
||||||
|
// Predefined responses for common questions
|
||||||
|
const responses = {
|
||||||
|
en: {
|
||||||
|
weather: "The Dominican Republic has a tropical climate with warm temperatures year-round. The dry season (December-April) is ideal for visiting, with less humidity and minimal rainfall.",
|
||||||
|
food: "Dominican cuisine features delicious dishes like mofongo, sancocho, and fresh seafood. Don't miss trying local fruits like mangoes and passion fruit!",
|
||||||
|
safety: "Tourist areas in the DR are generally safe. Stay in well-lit areas, use official taxis, and keep your belongings secure. POLITUR officers are available to help tourists.",
|
||||||
|
currency: "The Dominican Peso (DOP) is the local currency, but US dollars are widely accepted in tourist areas. Credit cards are accepted at most hotels and restaurants.",
|
||||||
|
language: "Spanish is the official language, but English is commonly spoken in tourist areas. Learning basic Spanish phrases is always appreciated!",
|
||||||
|
default: "I'm here to help you explore the beautiful Dominican Republic and Puerto Rico! Ask me about attractions, restaurants, safety tips, or anything else you'd like to know."
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
weather: "La República Dominicana tiene un clima tropical con temperaturas cálidas todo el año. La temporada seca (diciembre-abril) es ideal para visitar.",
|
||||||
|
food: "La cocina dominicana incluye platos deliciosos como mofongo, sancocho y mariscos frescos. ¡No te pierdas las frutas tropicales!",
|
||||||
|
safety: "Las áreas turísticas en RD son generalmente seguras. Mantente en áreas bien iluminadas y usa taxis oficiales.",
|
||||||
|
currency: "El peso dominicano (DOP) es la moneda local, pero los dólares estadounidenses son ampliamente aceptados.",
|
||||||
|
language: "El español es el idioma oficial. ¡Aprender algunas frases básicas siempre es apreciado!",
|
||||||
|
default: "¡Estoy aquí para ayudarte a explorar la hermosa República Dominicana y Puerto Rico! Pregúntame sobre atracciones, restaurantes o cualquier cosa."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const langResponses = responses[language] || responses.en;
|
||||||
|
|
||||||
|
// Simple keyword matching (in production, use proper NLP)
|
||||||
|
if (query.includes('weather') || query.includes('clima')) {
|
||||||
|
return langResponses.weather;
|
||||||
|
} else if (query.includes('food') || query.includes('comida') || query.includes('restaurant')) {
|
||||||
|
return langResponses.food;
|
||||||
|
} else if (query.includes('safe') || query.includes('segur')) {
|
||||||
|
return langResponses.safety;
|
||||||
|
} else if (query.includes('money') || query.includes('currency') || query.includes('dinero')) {
|
||||||
|
return langResponses.currency;
|
||||||
|
} else if (query.includes('language') || query.includes('idioma')) {
|
||||||
|
return langResponses.language;
|
||||||
|
}
|
||||||
|
|
||||||
|
return langResponses.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateSuggestions(query: string, language: string = 'en'): Promise<string[]> {
|
||||||
|
const suggestions = {
|
||||||
|
en: [
|
||||||
|
"What are the best beaches to visit?",
|
||||||
|
"Show me historic sites nearby",
|
||||||
|
"Find restaurants with local cuisine",
|
||||||
|
"What activities can I do here?",
|
||||||
|
"Tell me about local culture",
|
||||||
|
"How do I get to Santo Domingo?"
|
||||||
|
],
|
||||||
|
es: [
|
||||||
|
"¿Cuáles son las mejores playas para visitar?",
|
||||||
|
"Muéstrame sitios históricos cercanos",
|
||||||
|
"Encuentra restaurantes con cocina local",
|
||||||
|
"¿Qué actividades puedo hacer aquí?",
|
||||||
|
"Háblame sobre la cultura local",
|
||||||
|
"¿Cómo llego a Santo Domingo?"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
return suggestions[language] || suggestions.en;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateMonumentDescription(place: PlaceOfInterest): string {
|
||||||
|
return `This is ${place.name}, ${place.description || 'a significant landmark in the Dominican Republic'}.
|
||||||
|
|
||||||
|
Built in the ${place.historicalInfo ? 'historic period' : '16th century'}, this site represents an important part of Caribbean colonial history.
|
||||||
|
|
||||||
|
Rating: ${place.rating}/5 (${place.totalReviews} reviews)
|
||||||
|
|
||||||
|
Would you like to explore AR content, hear an audio guide, or learn more about nearby attractions?`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findNearbyARContent(latitude: number, longitude: number, radius: number = 100): Promise<ARContent[]> {
|
||||||
|
// In production, use PostGIS for accurate distance calculations
|
||||||
|
return this.arContentRepository.find({
|
||||||
|
where: { isActive: true },
|
||||||
|
order: { viewsCount: 'DESC' },
|
||||||
|
take: 10,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateAudioGuide(queryDto: AIQueryDto): Promise<{ transcript: string; audioUrl: string }> {
|
||||||
|
// This would integrate with text-to-speech services like AWS Polly, Google TTS, or Azure Speech
|
||||||
|
const transcript = "Welcome to this historic location. Let me tell you about its fascinating history...";
|
||||||
|
const audioUrl = "https://karibeo-audio-guides.s3.amazonaws.com/generated-audio-guide.mp3";
|
||||||
|
|
||||||
|
return { transcript, audioUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSmartDirections(queryDto: AIQueryDto): Promise<{ instructions: string; waypoints: PlaceOfInterest[] }> {
|
||||||
|
// Integrate with Google Maps Directions API for optimal routing
|
||||||
|
const instructions = "Head north for 200 meters, then turn right at the historic plaza. You'll pass several interesting landmarks along the way.";
|
||||||
|
const waypoints = await this.placeRepository.find({
|
||||||
|
where: { active: true },
|
||||||
|
take: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { instructions, waypoints };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPersonalizedRecommendations(userId: string, queryDto: AIQueryDto): Promise<PlaceOfInterest[]> {
|
||||||
|
// This would use ML algorithms to analyze user preferences, past visits, and ratings
|
||||||
|
return this.placeRepository.find({
|
||||||
|
where: { active: true },
|
||||||
|
order: { rating: 'DESC' },
|
||||||
|
take: 5,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatRecommendations(places: PlaceOfInterest[]): string {
|
||||||
|
if (places.length === 0) {
|
||||||
|
return "I don't have specific recommendations for this area right now, but I'd be happy to help you explore what's nearby!";
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = places.map((place, index) =>
|
||||||
|
`${index + 1}. ${place.name} (${place.rating}/5) - ${place.description?.substring(0, 100)}...`
|
||||||
|
).join('\n\n');
|
||||||
|
|
||||||
|
return `Based on your location and preferences, here are my top recommendations:\n\n${formatted}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveInteraction(interactionData: Partial<AIGuideInteraction>): Promise<void> {
|
||||||
|
const interaction = this.interactionRepository.create(interactionData);
|
||||||
|
await this.interactionRepository.save(interaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AR CONTENT MANAGEMENT
|
||||||
|
async getNearbyARContent(queryDto: ARContentQueryDto): Promise<ARContent[]> {
|
||||||
|
// In production, use PostGIS for accurate geospatial queries
|
||||||
|
const query = this.arContentRepository.createQueryBuilder('ar')
|
||||||
|
.leftJoinAndSelect('ar.place', 'place')
|
||||||
|
.where('ar.isActive = :active', { active: true });
|
||||||
|
|
||||||
|
if (queryDto.contentType) {
|
||||||
|
query.andWhere('ar.contentType = :contentType', { contentType: queryDto.contentType });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryDto.language) {
|
||||||
|
query.andWhere(':language = ANY(ar.languages)', { language: queryDto.language });
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
.orderBy('ar.viewsCount', 'DESC')
|
||||||
|
.limit(10)
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
async incrementARViewCount(arContentId: string): Promise<void> {
|
||||||
|
await this.arContentRepository.increment({ id: arContentId }, 'viewsCount', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ANALYTICS
|
||||||
|
async getAIUsageStats(): Promise<{
|
||||||
|
totalInteractions: number;
|
||||||
|
byType: Array<{ type: string; count: number }>;
|
||||||
|
byLanguage: Array<{ language: string; count: number }>;
|
||||||
|
averageRating: number;
|
||||||
|
popularQueries: Array<{ query: string; count: number }>;
|
||||||
|
}> {
|
||||||
|
const [totalInteractions, byType, byLanguage, avgRating] = await Promise.all([
|
||||||
|
this.interactionRepository.count(),
|
||||||
|
this.interactionRepository
|
||||||
|
.createQueryBuilder('interaction')
|
||||||
|
.select('interaction.interactionType', 'type')
|
||||||
|
.addSelect('COUNT(*)', 'count')
|
||||||
|
.groupBy('interaction.interactionType')
|
||||||
|
.getRawMany(),
|
||||||
|
this.interactionRepository
|
||||||
|
.createQueryBuilder('interaction')
|
||||||
|
.select('interaction.language', 'language')
|
||||||
|
.addSelect('COUNT(*)', 'count')
|
||||||
|
.groupBy('interaction.language')
|
||||||
|
.getRawMany(),
|
||||||
|
this.interactionRepository
|
||||||
|
.createQueryBuilder('interaction')
|
||||||
|
.select('AVG(interaction.rating)', 'average')
|
||||||
|
.where('interaction.rating IS NOT NULL')
|
||||||
|
.getRawOne(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get popular queries (simplified version)
|
||||||
|
const popularQueries = await this.interactionRepository
|
||||||
|
.createQueryBuilder('interaction')
|
||||||
|
.select('interaction.userQuery', 'query')
|
||||||
|
.addSelect('COUNT(*)', 'count')
|
||||||
|
.groupBy('interaction.userQuery')
|
||||||
|
.orderBy('count', 'DESC')
|
||||||
|
.limit(10)
|
||||||
|
.getRawMany();
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalInteractions,
|
||||||
|
byType: byType.map(item => ({ type: item.type, count: parseInt(item.count) })),
|
||||||
|
byLanguage: byLanguage.map(item => ({ language: item.language, count: parseInt(item.count) })),
|
||||||
|
averageRating: parseFloat(avgRating?.average || '0'),
|
||||||
|
popularQueries: popularQueries.map(item => ({ query: item.query, count: parseInt(item.count) })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/modules/ai-guide/dto/ai-query.dto.ts
Normal file
55
src/modules/ai-guide/dto/ai-query.dto.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { IsString, IsOptional, IsEnum, IsNumber } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export enum InteractionType {
|
||||||
|
GENERAL_QUESTION = 'general-question',
|
||||||
|
MONUMENT_RECOGNITION = 'monument-recognition',
|
||||||
|
AR_CONTENT = 'ar-content',
|
||||||
|
AUDIO_GUIDE = 'audio-guide',
|
||||||
|
DIRECTIONS = 'directions',
|
||||||
|
RECOMMENDATIONS = 'recommendations'
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AIQueryDto {
|
||||||
|
@ApiProperty({ description: 'User query/question', example: '¿Qué puedes decirme sobre este lugar?' })
|
||||||
|
@IsString()
|
||||||
|
query: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Interaction type', enum: InteractionType })
|
||||||
|
@IsEnum(InteractionType)
|
||||||
|
interactionType: InteractionType;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Place ID if asking about specific place' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
placeId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'User latitude' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
latitude?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'User longitude' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
longitude?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Session ID for conversation context' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
sessionId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Preferred language', example: 'en' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
language?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Image URL for monument recognition' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
imageUrl?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Additional context metadata' })
|
||||||
|
@IsOptional()
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
27
src/modules/ai-guide/dto/ar-content-query.dto.ts
Normal file
27
src/modules/ai-guide/dto/ar-content-query.dto.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { IsNumber, IsOptional, IsString } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class ARContentQueryDto {
|
||||||
|
@ApiProperty({ description: 'User latitude' })
|
||||||
|
@IsNumber()
|
||||||
|
latitude: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'User longitude' })
|
||||||
|
@IsNumber()
|
||||||
|
longitude: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Search radius in meters', example: 100 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
radius?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Content type filter' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
contentType?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Language preference', example: 'en' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
language?: string;
|
||||||
|
}
|
||||||
46
src/modules/analytics/analytics.controller.ts
Executable file
46
src/modules/analytics/analytics.controller.ts
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Controller, Get, Post, Body, Query, Param, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam } from '@nestjs/swagger';
|
||||||
|
import { AnalyticsService } from './analytics.service';
|
||||||
|
import { CreateReviewDto } from './dto/create-review.dto';
|
||||||
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
|
import { RolesGuard } from '../../common/guards/roles.guard';
|
||||||
|
import { Roles } from '../../common/decorators/roles.decorator';
|
||||||
|
import { Review } from '../../entities/review.entity';
|
||||||
|
|
||||||
|
@ApiTags('Analytics')
|
||||||
|
@Controller('analytics')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth('JWT-auth')
|
||||||
|
export class AnalyticsController {
|
||||||
|
constructor(private readonly analyticsService: AnalyticsService) {}
|
||||||
|
|
||||||
|
@Post('reviews')
|
||||||
|
@ApiOperation({ summary: 'Create a review' })
|
||||||
|
@ApiResponse({ status: 201, description: 'Review created successfully', type: Review })
|
||||||
|
createReview(@Body() createReviewDto: CreateReviewDto) {
|
||||||
|
return this.analyticsService.createReview(createReviewDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('reviews/:type/:id')
|
||||||
|
@ApiOperation({ summary: 'Get reviews for a specific item' })
|
||||||
|
@ApiParam({ name: 'type', description: 'Reviewable type (establishment, guide, place, etc.)' })
|
||||||
|
@ApiParam({ name: 'id', description: 'Reviewable ID' })
|
||||||
|
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||||
|
getReviewsForItem(
|
||||||
|
@Param('type') type: string,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Query('page') page?: number,
|
||||||
|
@Query('limit') limit?: number,
|
||||||
|
) {
|
||||||
|
return this.analyticsService.findReviewsForItem(type, id, page, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('overview')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('admin')
|
||||||
|
@ApiOperation({ summary: 'Get analytics overview (Admin only)' })
|
||||||
|
getAnalyticsOverview() {
|
||||||
|
return this.analyticsService.getAnalyticsOverview();
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/modules/analytics/analytics.module.ts
Executable file
13
src/modules/analytics/analytics.module.ts
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AnalyticsService } from './analytics.service';
|
||||||
|
import { AnalyticsController } from './analytics.controller';
|
||||||
|
import { Review } from '../../entities/review.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Review])],
|
||||||
|
controllers: [AnalyticsController],
|
||||||
|
providers: [AnalyticsService],
|
||||||
|
exports: [AnalyticsService],
|
||||||
|
})
|
||||||
|
export class AnalyticsModule {}
|
||||||
126
src/modules/analytics/analytics.service.ts
Executable file
126
src/modules/analytics/analytics.service.ts
Executable file
@@ -0,0 +1,126 @@
|
|||||||
|
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Review } from '../../entities/review.entity';
|
||||||
|
import { CreateReviewDto } from './dto/create-review.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AnalyticsService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Review)
|
||||||
|
private readonly reviewRepository: Repository<Review>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createReview(createReviewDto: CreateReviewDto): Promise<Review> {
|
||||||
|
// Check if user already reviewed this item
|
||||||
|
const existingReview = await this.reviewRepository.findOne({
|
||||||
|
where: {
|
||||||
|
userId: createReviewDto.userId,
|
||||||
|
reviewableType: createReviewDto.reviewableType,
|
||||||
|
reviewableId: createReviewDto.reviewableId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingReview) {
|
||||||
|
throw new ConflictException('You have already reviewed this item');
|
||||||
|
}
|
||||||
|
|
||||||
|
const review = this.reviewRepository.create(createReviewDto);
|
||||||
|
return this.reviewRepository.save(review);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findReviewsForItem(
|
||||||
|
reviewableType: string,
|
||||||
|
reviewableId: string,
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 10,
|
||||||
|
): Promise<{
|
||||||
|
reviews: Review[];
|
||||||
|
total: number;
|
||||||
|
averageRating: number;
|
||||||
|
ratingDistribution: Record<number, number>;
|
||||||
|
}> {
|
||||||
|
const [reviews, total] = await this.reviewRepository.findAndCount({
|
||||||
|
where: { reviewableType, reviewableId },
|
||||||
|
relations: ['user'],
|
||||||
|
skip: (page - 1) * limit,
|
||||||
|
take: limit,
|
||||||
|
order: { createdAt: 'DESC' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate average rating
|
||||||
|
const averageResult = await this.reviewRepository
|
||||||
|
.createQueryBuilder('review')
|
||||||
|
.select('AVG(review.rating)', 'average')
|
||||||
|
.where('review.reviewableType = :type AND review.reviewableId = :id', {
|
||||||
|
type: reviewableType,
|
||||||
|
id: reviewableId,
|
||||||
|
})
|
||||||
|
.getRawOne();
|
||||||
|
|
||||||
|
const averageRating = parseFloat(averageResult.average) || 0;
|
||||||
|
|
||||||
|
// Calculate rating distribution
|
||||||
|
const distributionResult = await this.reviewRepository
|
||||||
|
.createQueryBuilder('review')
|
||||||
|
.select('review.rating', 'rating')
|
||||||
|
.addSelect('COUNT(*)', 'count')
|
||||||
|
.where('review.reviewableType = :type AND review.reviewableId = :id', {
|
||||||
|
type: reviewableType,
|
||||||
|
id: reviewableId,
|
||||||
|
})
|
||||||
|
.groupBy('review.rating')
|
||||||
|
.getRawMany();
|
||||||
|
|
||||||
|
const ratingDistribution: Record<number, number> = {};
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
ratingDistribution[i] = 0;
|
||||||
|
}
|
||||||
|
distributionResult.forEach(item => {
|
||||||
|
ratingDistribution[item.rating] = parseInt(item.count);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { reviews, total, averageRating, ratingDistribution };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAnalyticsOverview(): Promise<{
|
||||||
|
totalReviews: number;
|
||||||
|
averageRating: number;
|
||||||
|
reviewsByType: Array<{ type: string; count: number; avgRating: number }>;
|
||||||
|
recentReviews: Review[];
|
||||||
|
}> {
|
||||||
|
const totalReviews = await this.reviewRepository.count();
|
||||||
|
|
||||||
|
const averageResult = await this.reviewRepository
|
||||||
|
.createQueryBuilder('review')
|
||||||
|
.select('AVG(review.rating)', 'average')
|
||||||
|
.getRawOne();
|
||||||
|
|
||||||
|
const averageRating = parseFloat(averageResult.average) || 0;
|
||||||
|
|
||||||
|
const reviewsByType = await this.reviewRepository
|
||||||
|
.createQueryBuilder('review')
|
||||||
|
.select('review.reviewableType', 'type')
|
||||||
|
.addSelect('COUNT(*)', 'count')
|
||||||
|
.addSelect('AVG(review.rating)', 'avgRating')
|
||||||
|
.groupBy('review.reviewableType')
|
||||||
|
.getRawMany();
|
||||||
|
|
||||||
|
const recentReviews = await this.reviewRepository.find({
|
||||||
|
relations: ['user'],
|
||||||
|
order: { createdAt: 'DESC' },
|
||||||
|
take: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalReviews,
|
||||||
|
averageRating,
|
||||||
|
reviewsByType: reviewsByType.map(item => ({
|
||||||
|
type: item.type,
|
||||||
|
count: parseInt(item.count),
|
||||||
|
avgRating: parseFloat(item.avgRating),
|
||||||
|
})),
|
||||||
|
recentReviews,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/modules/analytics/dto/create-review.dto.ts
Executable file
36
src/modules/analytics/dto/create-review.dto.ts
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
import { IsString, IsNumber, IsOptional, Min, Max } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class CreateReviewDto {
|
||||||
|
@ApiProperty({ description: 'User ID' })
|
||||||
|
@IsString()
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Reviewable type', example: 'establishment' })
|
||||||
|
@IsString()
|
||||||
|
reviewableType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Reviewable ID' })
|
||||||
|
@IsString()
|
||||||
|
reviewableId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Rating (1-5)', example: 5 })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Max(5)
|
||||||
|
rating: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Review title', example: 'Amazing experience!' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Review comment' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
comment?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Review images' })
|
||||||
|
@IsOptional()
|
||||||
|
images?: Record<string, any>;
|
||||||
|
}
|
||||||
110
src/modules/auth/auth.controller.ts
Executable file
110
src/modules/auth/auth.controller.ts
Executable file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Controller, Post, Body, HttpCode, HttpStatus, UseGuards, Get, Request } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiBody } from '@nestjs/swagger';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { RegisterDto } from './dto/register.dto';
|
||||||
|
import { LoginDto } from './dto/login.dto';
|
||||||
|
import { AuthResponseDto } from './dto/auth-response.dto';
|
||||||
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
|
import { User } from '../../entities/user.entity';
|
||||||
|
|
||||||
|
@ApiTags('Authentication')
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@Post('register')
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Register a new user',
|
||||||
|
description: 'Creates a new user account with tourist role by default'
|
||||||
|
})
|
||||||
|
@ApiBody({ type: RegisterDto })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'User successfully registered',
|
||||||
|
type: AuthResponseDto
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 409,
|
||||||
|
description: 'User with this email already exists'
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: 'Invalid input data'
|
||||||
|
})
|
||||||
|
async register(@Body() registerDto: RegisterDto): Promise<AuthResponseDto> {
|
||||||
|
return this.authService.register(registerDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('login')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'User login',
|
||||||
|
description: 'Authenticates user and returns JWT tokens'
|
||||||
|
})
|
||||||
|
@ApiBody({ type: LoginDto })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'User successfully authenticated',
|
||||||
|
type: AuthResponseDto
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 401,
|
||||||
|
description: 'Invalid credentials or account locked'
|
||||||
|
})
|
||||||
|
async login(@Body() loginDto: LoginDto): Promise<AuthResponseDto> {
|
||||||
|
return this.authService.login(loginDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('refresh')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Refresh access token',
|
||||||
|
description: 'Refresh JWT access token using refresh token'
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
refreshToken: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Valid refresh token',
|
||||||
|
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['refreshToken']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Tokens refreshed successfully',
|
||||||
|
type: AuthResponseDto
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 401,
|
||||||
|
description: 'Invalid or expired refresh token'
|
||||||
|
})
|
||||||
|
async refresh(@Body() body: { refreshToken: string }): Promise<AuthResponseDto> {
|
||||||
|
return this.authService.refresh(body.refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('profile')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth('JWT-auth')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Get current user profile',
|
||||||
|
description: 'Returns the profile of the authenticated user'
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'User profile retrieved successfully',
|
||||||
|
type: User
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 401,
|
||||||
|
description: 'Unauthorized - Invalid or missing token'
|
||||||
|
})
|
||||||
|
async getProfile(@Request() req): Promise<User> {
|
||||||
|
return req.user;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/modules/auth/auth.module.ts
Executable file
30
src/modules/auth/auth.module.ts
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||||
|
import { User } from '../../entities/user.entity';
|
||||||
|
import { Role } from '../../entities/role.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([User, Role]),
|
||||||
|
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
useFactory: (configService: ConfigService) => ({
|
||||||
|
secret: configService.get<string>('JWT_SECRET'),
|
||||||
|
signOptions: {
|
||||||
|
expiresIn: configService.get<string>('JWT_EXPIRES_IN') || '24h',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [AuthController],
|
||||||
|
providers: [AuthService, JwtStrategy],
|
||||||
|
exports: [AuthService, JwtStrategy, PassportModule],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
173
src/modules/auth/auth.service.ts
Executable file
173
src/modules/auth/auth.service.ts
Executable file
@@ -0,0 +1,173 @@
|
|||||||
|
import { Injectable, UnauthorizedException, ConflictException, InternalServerErrorException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import { User } from '../../entities/user.entity';
|
||||||
|
import { Role } from '../../entities/role.entity';
|
||||||
|
import { RegisterDto } from './dto/register.dto';
|
||||||
|
import { LoginDto } from './dto/login.dto';
|
||||||
|
import { AuthResponseDto } from './dto/auth-response.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(User)
|
||||||
|
private readonly userRepository: Repository<User>,
|
||||||
|
@InjectRepository(Role)
|
||||||
|
private readonly roleRepository: Repository<Role>,
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async register(registerDto: RegisterDto): Promise<AuthResponseDto> {
|
||||||
|
const { email, password, ...userData } = registerDto;
|
||||||
|
|
||||||
|
// Check if user already exists
|
||||||
|
const existingUser = await this.userRepository.findOne({ where: { email } });
|
||||||
|
if (existingUser) {
|
||||||
|
throw new ConflictException('User with this email already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
const saltRounds = 12;
|
||||||
|
const passwordHash = await bcrypt.hash(password, saltRounds);
|
||||||
|
|
||||||
|
// Get default tourist role
|
||||||
|
const defaultRole = await this.roleRepository.findOne({ where: { name: 'tourist' } });
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
const user = this.userRepository.create({
|
||||||
|
email,
|
||||||
|
passwordHash,
|
||||||
|
roleId: defaultRole?.id || 2, // Default to tourist role
|
||||||
|
...userData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const savedUser = await this.userRepository.save(user);
|
||||||
|
|
||||||
|
// Generate tokens
|
||||||
|
const { accessToken, refreshToken } = await this.generateTokens(savedUser);
|
||||||
|
|
||||||
|
// Load user with relations for response
|
||||||
|
const userWithRelations = await this.userRepository.findOne({
|
||||||
|
where: { id: savedUser.id },
|
||||||
|
relations: ['country', 'role', 'preferredLanguageEntity'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userWithRelations) {
|
||||||
|
throw new InternalServerErrorException('Failed to retrieve user after registration');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
user: userWithRelations,
|
||||||
|
expiresIn: '24h',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(loginDto: LoginDto): Promise<AuthResponseDto> {
|
||||||
|
const { email, password } = loginDto;
|
||||||
|
|
||||||
|
// Find user with relations
|
||||||
|
const user = await this.userRepository.findOne({
|
||||||
|
where: { email },
|
||||||
|
relations: ['country', 'role', 'preferredLanguageEntity'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException('Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if account is locked
|
||||||
|
if (user.lockedUntil && new Date() < user.lockedUntil) {
|
||||||
|
throw new UnauthorizedException('Account is temporarily locked');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify password
|
||||||
|
const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
// Increment failed login attempts
|
||||||
|
await this.handleFailedLogin(user);
|
||||||
|
throw new UnauthorizedException('Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset failed login attempts on successful login
|
||||||
|
await this.userRepository.update(user.id, {
|
||||||
|
failedLoginAttempts: 0,
|
||||||
|
lockedUntil: undefined,
|
||||||
|
lastLogin: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate tokens
|
||||||
|
const { accessToken, refreshToken } = await this.generateTokens(user);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
user,
|
||||||
|
expiresIn: '24h',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateUser(userId: string): Promise<User | null> {
|
||||||
|
return this.userRepository.findOne({
|
||||||
|
where: { id: userId, isActive: true },
|
||||||
|
relations: ['country', 'role', 'preferredLanguageEntity'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh(refreshToken: string): Promise<AuthResponseDto> {
|
||||||
|
try {
|
||||||
|
// Verificar el refresh token
|
||||||
|
const payload = this.jwtService.verify(refreshToken);
|
||||||
|
|
||||||
|
// Buscar el usuario
|
||||||
|
const user = await this.userRepository.findOne({
|
||||||
|
where: { id: payload.sub, isActive: true },
|
||||||
|
relations: ['country', 'role', 'preferredLanguageEntity'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException('User not found or inactive');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generar nuevos tokens
|
||||||
|
const { accessToken, refreshToken: newRefreshToken } = await this.generateTokens(user);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken: newRefreshToken,
|
||||||
|
user,
|
||||||
|
expiresIn: '24h',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new UnauthorizedException('Invalid or expired refresh token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateTokens(user: User): Promise<{ accessToken: string; refreshToken: string }> {
|
||||||
|
const payload = {
|
||||||
|
sub: user.id,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role?.name || 'tourist',
|
||||||
|
};
|
||||||
|
|
||||||
|
const accessToken = this.jwtService.sign(payload);
|
||||||
|
const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });
|
||||||
|
|
||||||
|
return { accessToken, refreshToken };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleFailedLogin(user: User): Promise<void> {
|
||||||
|
const failedAttempts = user.failedLoginAttempts + 1;
|
||||||
|
const updateData: any = { failedLoginAttempts: failedAttempts };
|
||||||
|
|
||||||
|
// Lock account after 5 failed attempts for 15 minutes
|
||||||
|
if (failedAttempts >= 5) {
|
||||||
|
updateData.lockedUntil = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userRepository.update(user.id, updateData);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/modules/auth/dto/auth-response.dto.ts
Executable file
16
src/modules/auth/dto/auth-response.dto.ts
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { User } from '../../../entities/user.entity';
|
||||||
|
|
||||||
|
export class AuthResponseDto {
|
||||||
|
@ApiProperty({ description: 'JWT access token' })
|
||||||
|
accessToken: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'JWT refresh token' })
|
||||||
|
refreshToken: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'User information', type: () => User })
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Token expiration time' })
|
||||||
|
expiresIn: string;
|
||||||
|
}
|
||||||
12
src/modules/auth/dto/login.dto.ts
Executable file
12
src/modules/auth/dto/login.dto.ts
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
import { IsEmail, IsString } from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
@ApiProperty({ description: 'Email address', example: 'john.doe@example.com' })
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Password', example: 'SecurePass123!' })
|
||||||
|
@IsString()
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
41
src/modules/auth/dto/register.dto.ts
Executable file
41
src/modules/auth/dto/register.dto.ts
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
import { IsEmail, IsString, MinLength, IsOptional, IsNumber } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class RegisterDto {
|
||||||
|
@ApiProperty({ description: 'Email address', example: 'john.doe@example.com' })
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Password (minimum 8 characters)', example: 'SecurePass123!' })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8)
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'First name', example: 'John' })
|
||||||
|
@IsString()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Last name', example: 'Doe' })
|
||||||
|
@IsString()
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Phone number', example: '+1234567890' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
phone?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Country ID', example: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
countryId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Preferred language', example: 'en' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
preferredLanguage?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Preferred currency', example: 'USD' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
preferredCurrency?: string;
|
||||||
|
}
|
||||||
35
src/modules/auth/strategies/jwt.strategy.ts
Executable file
35
src/modules/auth/strategies/jwt.strategy.ts
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AuthService } from '../auth.service';
|
||||||
|
|
||||||
|
export interface JwtPayload {
|
||||||
|
sub: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
iat?: number;
|
||||||
|
exp?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: configService.get<string>('JWT_SECRET') || 'karibeo_jwt_secret_key_2025_very_secure',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: JwtPayload) {
|
||||||
|
const user = await this.authService.validateUser(payload.sub);
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException('User not found or inactive');
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
Controller, Get, Post, Body, Query, UseGuards, Param
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { AvailabilityManagementService } from './availability-management.service';
|
||||||
|
import { GetAvailabilityDto } from './dto/get-availability.dto';
|
||||||
|
import { UpdateAvailabilityDto } from './dto/update-availability.dto';
|
||||||
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
|
import { RolesGuard } from '../../common/guards/roles.guard';
|
||||||
|
import { Roles } from '../../common/decorators/roles.decorator';
|
||||||
|
import { Availability } from '../../entities/availability.entity';
|
||||||
|
@ApiTags('Availability Management')
|
||||||
|
@ApiBearerAuth('JWT-auth')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Controller('api/v1/availability')
|
||||||
|
export class AvailabilityManagementController {
|
||||||
|
constructor(private readonly availabilityManagementService: AvailabilityManagementService) {}
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'Get availability for a specific resource (hotel, restaurant, vehicle, room, table) in a date range' })
|
||||||
|
@ApiQuery({ name: 'resourceId', type: String, example: 'uuid-hotel-123', description: 'ID of the resource' })
|
||||||
|
@ApiQuery({ name: 'resourceType', type: String, example: 'hotel', description: 'Type of the resource (hotel, restaurant, vehicle, room, table)' })
|
||||||
|
@ApiQuery({ name: 'startDate', type: String, format: 'date', example: '2025-10-01' })
|
||||||
|
@ApiQuery({ name: 'endDate', type: String, format: 'date', example: '2025-10-05' })
|
||||||
|
@ApiQuery({ name: 'minQuantity', type: Number, required: false, example: 1, description: 'Minimum quantity desired' })
|
||||||
|
@ApiResponse({ status: 200, type: [Availability] })
|
||||||
|
getAvailability(@Query() queryDto: GetAvailabilityDto) {
|
||||||
|
return this.availabilityManagementService.getAvailability(queryDto);
|
||||||
|
}
|
||||||
|
@Post()
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('admin', 'establishment', 'owner') // Admins, establishment managers, or owners can update availability
|
||||||
|
@ApiOperation({ summary: 'Update or create availability for a specific resource on a given date' })
|
||||||
|
@ApiResponse({ status: 201, description: 'Availability updated/created successfully', type: Availability })
|
||||||
|
updateAvailability(@Body() updateDto: UpdateAvailabilityDto) {
|
||||||
|
return this.availabilityManagementService.updateAvailability(updateDto);
|
||||||
|
}
|
||||||
|
// Specific endpoints for convenience (delegating to generic availability service)
|
||||||
|
@Get('hotel/:id')
|
||||||
|
@ApiOperation({ summary: 'Get hotel room availability for a specific hotel in a date range' })
|
||||||
|
@ApiParam({ name: 'id', type: String, description: 'Hotel ID (Establishment ID)' })
|
||||||
|
@ApiQuery({ name: 'startDate', type: String, format: 'date', example: '2025-10-01' })
|
||||||
|
@ApiQuery({ name: 'endDate', type: String, format: 'date', example: '2025-10-05' })
|
||||||
|
@ApiQuery({ name: 'minQuantity', type: Number, required: false, example: 1, description: 'Minimum number of rooms desired' })
|
||||||
|
@ApiResponse({ status: 200, type: [Availability] })
|
||||||
|
getHotelAvailability(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Query('startDate') startDate: string,
|
||||||
|
@Query('endDate') endDate: string,
|
||||||
|
@Query('minQuantity') minQuantity?: number,
|
||||||
|
) {
|
||||||
|
return this.availabilityManagementService.getAvailability({
|
||||||
|
resourceId: id,
|
||||||
|
resourceType: 'hotel', // Or 'hotel-room' if granular
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
minQuantity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@Get('restaurant/:id')
|
||||||
|
@ApiOperation({ summary: 'Get restaurant table availability for a specific restaurant on a date' })
|
||||||
|
@ApiParam({ name: 'id', type: String, description: 'Restaurant ID (Establishment ID)' })
|
||||||
|
@ApiQuery({ name: 'date', type: String, format: 'date', example: '2025-10-01' })
|
||||||
|
@ApiQuery({ name: 'minQuantity', type: Number, required: false, example: 1, description: 'Minimum number of tables/seats desired' })
|
||||||
|
@ApiResponse({ status: 200, type: [Availability] })
|
||||||
|
getRestaurantAvailability(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Query('date') date: string,
|
||||||
|
@Query('minQuantity') minQuantity?: number,
|
||||||
|
) {
|
||||||
|
// For restaurants, availability might be per day/slot, so startDate and endDate are the same
|
||||||
|
return this.availabilityManagementService.getAvailability({
|
||||||
|
resourceId: id,
|
||||||
|
resourceType: 'restaurant', // Or 'restaurant-table' if granular
|
||||||
|
startDate: date,
|
||||||
|
endDate: date,
|
||||||
|
minQuantity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AvailabilityManagementService } from './availability-management.service';
|
||||||
|
import { AvailabilityManagementController } from './availability-management.controller';
|
||||||
|
import { Availability } from '../../entities/availability.entity';
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
Availability,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
controllers: [AvailabilityManagementController],
|
||||||
|
providers: [AvailabilityManagementService],
|
||||||
|
exports: [AvailabilityManagementService], // Export for other modules to use (e.g., booking)
|
||||||
|
})
|
||||||
|
export class AvailabilityManagementModule {}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { Injectable, NotFoundException, BadRequestException, Logger } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Availability } from '../../entities/availability.entity';
|
||||||
|
import { GetAvailabilityDto } from './dto/get-availability.dto';
|
||||||
|
import { UpdateAvailabilityDto } from './dto/update-availability.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AvailabilityManagementService {
|
||||||
|
private readonly logger = new Logger(AvailabilityManagementService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Availability)
|
||||||
|
private readonly availabilityRepository: Repository<Availability>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves availability for a given resource within a date range.
|
||||||
|
*/
|
||||||
|
async getAvailability(queryDto: GetAvailabilityDto): Promise<Availability[]> {
|
||||||
|
const { resourceId, resourceType, startDate, endDate, minQuantity } = queryDto;
|
||||||
|
|
||||||
|
const start = new Date(startDate);
|
||||||
|
const end = new Date(endDate);
|
||||||
|
|
||||||
|
if (start > end) {
|
||||||
|
throw new BadRequestException('Start date cannot be after end date.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = this.availabilityRepository.createQueryBuilder('availability')
|
||||||
|
.where('availability.resourceId = :resourceId', { resourceId })
|
||||||
|
.andWhere('availability.resourceType = :resourceType', { resourceType })
|
||||||
|
.andWhere('availability.date >= :startDate', { startDate: queryDto.startDate })
|
||||||
|
.andWhere('availability.date <= :endDate', { endDate: queryDto.endDate })
|
||||||
|
.andWhere('availability.isAvailable = :isAvailable', { isAvailable: true });
|
||||||
|
|
||||||
|
if (minQuantity !== undefined && minQuantity > 0) {
|
||||||
|
query.andWhere('availability.availableQuantity >= :minQuantity', { minQuantity });
|
||||||
|
} else {
|
||||||
|
// Ensure at least 1 unit is available if no minQuantity is specified
|
||||||
|
query.andWhere('availability.availableQuantity >= :defaultMinQuantity', { defaultMinQuantity: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.orderBy('availability.date', 'ASC').getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates or creates availability records for a specific date.
|
||||||
|
* This would typically be used by a channel manager or an establishment owner.
|
||||||
|
*/
|
||||||
|
async updateAvailability(updateDto: UpdateAvailabilityDto): Promise<Availability> {
|
||||||
|
const { resourceId, resourceType, date, availableQuantity, totalQuantity, basePrice, priceModifiers, minStay, restrictions, isAvailable, status } = updateDto;
|
||||||
|
|
||||||
|
let availability = await this.availabilityRepository.findOne({
|
||||||
|
where: {
|
||||||
|
resourceId,
|
||||||
|
resourceType,
|
||||||
|
date: new Date(date),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (availability) {
|
||||||
|
// Update existing record
|
||||||
|
availability.availableQuantity = availableQuantity;
|
||||||
|
availability.totalQuantity = totalQuantity;
|
||||||
|
availability.basePrice = basePrice ?? availability.basePrice;
|
||||||
|
availability.finalPrice = this.calculateFinalPrice(basePrice ?? availability.basePrice, priceModifiers ?? {});
|
||||||
|
availability.priceModifiers = priceModifiers ?? availability.priceModifiers;
|
||||||
|
availability.minStay = minStay ?? availability.minStay;
|
||||||
|
availability.restrictions = restrictions ?? availability.restrictions;
|
||||||
|
availability.isAvailable = isAvailable ?? (availableQuantity > 0);
|
||||||
|
availability.status = status ?? (availability.isAvailable ? 'open' : 'sold-out');
|
||||||
|
availability.lastSynced = new Date();
|
||||||
|
} else {
|
||||||
|
// Create new record
|
||||||
|
availability = this.availabilityRepository.create({
|
||||||
|
resourceId,
|
||||||
|
resourceType,
|
||||||
|
date: new Date(date),
|
||||||
|
availableQuantity,
|
||||||
|
totalQuantity,
|
||||||
|
bookedQuantity: 0,
|
||||||
|
blockedQuantity: 0,
|
||||||
|
basePrice: basePrice ?? 0,
|
||||||
|
priceModifiers: priceModifiers ?? {},
|
||||||
|
finalPrice: this.calculateFinalPrice(basePrice ?? 0, priceModifiers ?? {}),
|
||||||
|
minStay: minStay ?? 1,
|
||||||
|
restrictions: restrictions,
|
||||||
|
isAvailable: isAvailable ?? (availableQuantity > 0),
|
||||||
|
status: status ?? (availableQuantity > 0 ? 'open' : 'sold-out'),
|
||||||
|
lastSynced: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.availabilityRepository.save(availability);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the final price based on base price and modifiers.
|
||||||
|
*/
|
||||||
|
private calculateFinalPrice(basePrice: number, modifiers: Record<string, any>): number {
|
||||||
|
let finalPrice = basePrice;
|
||||||
|
if (modifiers) {
|
||||||
|
for (const key in modifiers) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(modifiers, key)) {
|
||||||
|
// Example: Apply percentage modifiers
|
||||||
|
if (typeof modifiers[key] === 'number' && modifiers[key] > 0) {
|
||||||
|
finalPrice *= modifiers[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parseFloat(finalPrice.toFixed(2)); // Round to 2 decimal places
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility to check and adjust availability when a booking occurs
|
||||||
|
async decrementAvailability(resourceId: string, resourceType: string, date: Date, quantity: number): Promise<void> {
|
||||||
|
const availability = await this.availabilityRepository.findOne({
|
||||||
|
where: { resourceId, resourceType, date },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!availability) {
|
||||||
|
throw new NotFoundException(`Availability for ${resourceType} ${resourceId} on ${date.toDateString()} not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availability.availableQuantity < quantity) {
|
||||||
|
throw new BadRequestException(`Not enough availability for ${resourceType} ${resourceId} on ${date.toDateString()}. Only ${availability.availableQuantity} left.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
availability.availableQuantity -= quantity;
|
||||||
|
availability.bookedQuantity += quantity;
|
||||||
|
if (availability.availableQuantity === 0) {
|
||||||
|
availability.isAvailable = false;
|
||||||
|
availability.status = 'sold-out';
|
||||||
|
}
|
||||||
|
await this.availabilityRepository.save(availability);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility to check and adjust availability when a booking is cancelled
|
||||||
|
async incrementAvailability(resourceId: string, resourceType: string, date: Date, quantity: number): Promise<void> {
|
||||||
|
const availability = await this.availabilityRepository.findOne({
|
||||||
|
where: { resourceId, resourceType, date },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!availability) {
|
||||||
|
this.logger.warn(`Availability for ${resourceType} ${resourceId} on ${date.toDateString()} not found during increment. Skipping.`);
|
||||||
|
return; // Or create a new record if this scenario is expected for initial unavailability
|
||||||
|
}
|
||||||
|
|
||||||
|
availability.availableQuantity += quantity;
|
||||||
|
availability.bookedQuantity -= quantity;
|
||||||
|
if (availability.availableQuantity > 0) {
|
||||||
|
availability.isAvailable = true;
|
||||||
|
availability.status = 'open';
|
||||||
|
}
|
||||||
|
await this.availabilityRepository.save(availability);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { IsString, IsNotEmpty, IsDateString, IsOptional, IsNumber, Min } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
export class GetAvailabilityDto {
|
||||||
|
@ApiProperty({ description: 'Resource ID (e.g., Hotel ID, Restaurant ID, Vehicle ID, Room ID, Table ID)', example: 'uuid-hotel-123' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
resourceId: string;
|
||||||
|
@ApiProperty({ description: 'Resource type (hotel, restaurant, vehicle, room, table)', example: 'hotel' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
resourceType: string;
|
||||||
|
@ApiProperty({ description: 'Start date for availability query (YYYY-MM-DD)', example: '2025-10-01' })
|
||||||
|
@IsDateString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
startDate: string;
|
||||||
|
@ApiProperty({ description: 'End date for availability query (YYYY-MM-DD)', example: '2025-10-05' })
|
||||||
|
@IsDateString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
endDate: string;
|
||||||
|
@ApiPropertyOptional({ description: 'Minimum quantity required', example: 1 })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
@IsOptional()
|
||||||
|
minQuantity?: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { IsString, IsNotEmpty, IsDateString, IsNumber, Min, IsOptional, IsObject, IsBoolean } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
export class UpdateAvailabilityDto {
|
||||||
|
@ApiProperty({ description: 'Resource ID (e.g., Hotel ID, Restaurant ID, Vehicle ID, Room ID, Table ID)', example: 'uuid-hotel-room-456' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
resourceId: string;
|
||||||
|
@ApiProperty({ description: 'Resource type (hotel, restaurant, vehicle, room, table)', example: 'room' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
resourceType: string;
|
||||||
|
@ApiProperty({ description: 'Date for availability update (YYYY-MM-DD)', example: '2025-10-02' })
|
||||||
|
@IsDateString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
date: string;
|
||||||
|
@ApiProperty({ description: 'Available quantity for the specified date', example: 5 })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
availableQuantity: number;
|
||||||
|
@ApiProperty({ description: 'Total quantity of the resource (e.g., total rooms of this type)', example: 10 })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
totalQuantity: number;
|
||||||
|
@ApiPropertyOptional({ description: 'Base price for this date', example: 150.00 })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
@IsOptional()
|
||||||
|
basePrice?: number;
|
||||||
|
@ApiPropertyOptional({ description: 'Dynamic price adjustments (e.g., { demand: 1.2 })' })
|
||||||
|
@IsObject()
|
||||||
|
@IsOptional()
|
||||||
|
priceModifiers?: Record<string, any>;
|
||||||
|
@ApiPropertyOptional({ description: 'Minimum stay requirement for this date', example: 2 })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@IsOptional()
|
||||||
|
minStay?: number;
|
||||||
|
@ApiPropertyOptional({ description: 'Special restrictions or notes for this date' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
restrictions?: string;
|
||||||
|
@ApiPropertyOptional({ description: 'Is available for booking', example: true })
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
isAvailable?: boolean;
|
||||||
|
@ApiPropertyOptional({ description: 'Availability status (open, closed, limited, sold-out)', example: 'open' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import {
|
||||||
|
Controller, Get, Post, Body, Patch, Param, Delete, UseGuards
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiParam
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { ChannelManagementService } from './channel-management.service';
|
||||||
|
import { CreateChannelDto } from './dto/create-channel.dto'; // Mantenemos solo CreateChannelDto aquí
|
||||||
|
import { ConnectChannelDto } from './dto/connect-channel.dto'; // CORREGIDO: Importar ConnectChannelDto de su propio archivo
|
||||||
|
import { UpdateChannelDto } from './dto/update-channel.dto';
|
||||||
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
|
import { RolesGuard } from '../../common/guards/roles.guard';
|
||||||
|
import { Roles } from '../../common/decorators/roles.decorator';
|
||||||
|
import { Channel } from '../../entities/channel.entity';
|
||||||
|
|
||||||
|
@ApiTags('Channel Management')
|
||||||
|
@ApiBearerAuth('JWT-auth')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Controller('api/v1/channels')
|
||||||
|
export class ChannelManagementController {
|
||||||
|
constructor(private readonly channelManagementService: ChannelManagementService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('admin', 'establishment')
|
||||||
|
@ApiOperation({ summary: 'Create a new channel' })
|
||||||
|
@ApiResponse({ status: 201, description: 'Channel created successfully', type: Channel })
|
||||||
|
create(@Body() createChannelDto: CreateChannelDto) {
|
||||||
|
return this.channelManagementService.createChannel(createChannelDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('admin', 'establishment')
|
||||||
|
@ApiOperation({ summary: 'Get a list of all connected distribution channels' })
|
||||||
|
@ApiResponse({ status: 200, type: [Channel] })
|
||||||
|
findAll() {
|
||||||
|
return this.channelManagementService.findAllChannels();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('admin', 'establishment')
|
||||||
|
@ApiOperation({ summary: 'Get a specific channel by ID' })
|
||||||
|
@ApiParam({ name: 'id', type: 'string', description: 'Channel ID' })
|
||||||
|
@ApiResponse({ status: 200, type: Channel })
|
||||||
|
findOne(@Param('id') id: string) {
|
||||||
|
return this.channelManagementService.findChannelById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('admin', 'establishment')
|
||||||
|
@ApiOperation({ summary: 'Update an existing channel by ID' })
|
||||||
|
@ApiParam({ name: 'id', type: 'string', description: 'Channel ID' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Channel updated successfully', type: Channel })
|
||||||
|
update(@Param('id') id: string, @Body() updateChannelDto: UpdateChannelDto) {
|
||||||
|
return this.channelManagementService.updateChannel(id, updateChannelDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('admin', 'establishment')
|
||||||
|
@ApiOperation({ summary: 'Delete a channel by ID' })
|
||||||
|
@ApiParam({ name: 'id', type: 'string', description: 'Channel ID' })
|
||||||
|
@ApiResponse({ status: 204, description: 'Channel deleted successfully' })
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.channelManagementService.deleteChannel(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('connect')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('admin', 'establishment')
|
||||||
|
@ApiOperation({ summary: 'Connect a new distribution channel' })
|
||||||
|
@ApiResponse({ status: 201, description: 'Channel connected and initiated sync', type: Channel })
|
||||||
|
connectChannel(@Body() connectChannelDto: ConnectChannelDto) {
|
||||||
|
return this.channelManagementService.connectChannel(connectChannelDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id/disconnect')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('admin', 'establishment')
|
||||||
|
@ApiOperation({ summary: 'Disconnect a specific distribution channel by its ID' })
|
||||||
|
@ApiParam({ name: 'id', type: 'string', description: 'Channel ID' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Channel disconnected successfully', type: Channel })
|
||||||
|
disconnectChannel(@Param('id') id: string) {
|
||||||
|
return this.channelManagementService.disconnectChannel(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/sync')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('admin', 'establishment')
|
||||||
|
@ApiOperation({ summary: 'Initiate a manual synchronization for a specific channel' })
|
||||||
|
@ApiParam({ name: 'id', type: 'string', description: 'Channel ID' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Channel synchronization initiated', type: Channel })
|
||||||
|
syncChannel(@Param('id') id: string) {
|
||||||
|
return this.channelManagementService.syncChannel(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/modules/channel-management/channel-management.module.ts
Normal file
19
src/modules/channel-management/channel-management.module.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ChannelManagementService } from './channel-management.service';
|
||||||
|
import { ChannelManagementController } from './channel-management.controller';
|
||||||
|
import { Channel } from '../../entities/channel.entity';
|
||||||
|
import { NotificationsModule } from '../notifications/notifications.module';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Channel]),
|
||||||
|
NotificationsModule,
|
||||||
|
ScheduleModule,
|
||||||
|
],
|
||||||
|
controllers: [ChannelManagementController],
|
||||||
|
providers: [ChannelManagementService],
|
||||||
|
exports: [ChannelManagementService],
|
||||||
|
})
|
||||||
|
export class ChannelManagementModule {}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user