karibeo-api inicial
This commit is contained in:
56
.gitignore
vendored
Normal file
56
.gitignore
vendored
Normal 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
Normal file
4
.prettierrc
Normal 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
Normal file
34
eslint.config.mjs
Normal 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
Normal file
8
nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
15082
package-lock.json
generated
Normal file
15082
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
103
package.json
Normal file
103
package.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"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/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
Normal file
22
src/app.controller.spec.ts
Normal 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
Normal file
12
src/app.controller.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
200
src/app.module.ts
Normal file
200
src/app.module.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
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';
|
||||
|
||||
// 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 { Review } from './entities/review.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';
|
||||
|
||||
// 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';
|
||||
|
||||
@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,
|
||||
Review, 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,
|
||||
],
|
||||
}),
|
||||
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],
|
||||
}),
|
||||
|
||||
// ========================================
|
||||
// TODOS LOS MÓDULOS - 20 MÓDULOS TOTALES
|
||||
// ========================================
|
||||
|
||||
// Core modules (4)
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
AnalyticsModule,
|
||||
NotificationsModule,
|
||||
|
||||
// Business modules (3)
|
||||
TourismModule,
|
||||
CommerceModule,
|
||||
SecurityModule,
|
||||
|
||||
// Integration modules (3)
|
||||
PaymentsModule,
|
||||
UploadModule,
|
||||
CommunicationModule,
|
||||
|
||||
// Industry-specific modules (2)
|
||||
RestaurantModule,
|
||||
HotelModule,
|
||||
|
||||
// Advanced features modules (3)
|
||||
AIGuideModule,
|
||||
GeolocationModule,
|
||||
ReviewsModule,
|
||||
|
||||
// Innovation 2025 modules (5)
|
||||
AIGeneratorModule, // IA Generativa
|
||||
PersonalizationModule, // Super-Personalización
|
||||
SustainabilityModule, // Turismo Sostenible
|
||||
SocialCommerceModule, // Social Commerce & Influencers
|
||||
IoTTourismModule, // IoT & 5G Tourism
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: ThrottlerGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
8
src/app.service.ts
Normal file
8
src/app.service.ts
Normal 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
Normal file
4
src/common/decorators/roles.decorator.ts
Normal 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
Normal file
5
src/common/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||
21
src/common/guards/roles.guard.ts
Normal file
21
src/common/guards/roles.guard.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (!requiredRoles) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
return requiredRoles.some((role) => user.role?.name === role);
|
||||
}
|
||||
}
|
||||
16
src/config/app.config.ts
Normal file
16
src/config/app.config.ts
Normal 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
Normal file
20
src/config/database.config.ts
Normal 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
Normal file
11
src/config/integrations/aws.config.ts
Normal 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
Normal file
21
src/config/integrations/communication.config.ts
Normal 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
Normal file
9
src/config/integrations/stripe.config.ts
Normal 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
Normal file
9
src/config/jwt.config.ts
Normal 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',
|
||||
},
|
||||
}));
|
||||
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, OneToMany } 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({ name: 'overall_rating' })
|
||||
overallRating: number;
|
||||
|
||||
@ApiProperty({ description: 'Detailed ratings by category' })
|
||||
@Column({ name: 'detailed_ratings', type: 'jsonb', nullable: true })
|
||||
detailedRatings: Record<string, number>; // { service: 5, food: 4, ambiance: 5, value: 4 }
|
||||
|
||||
@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 pros' })
|
||||
@Column({ type: 'text', array: true, nullable: true })
|
||||
pros: string[];
|
||||
|
||||
@ApiProperty({ description: 'Review cons' })
|
||||
@Column({ type: 'text', array: true, nullable: true })
|
||||
cons: string[];
|
||||
|
||||
@ApiProperty({ description: 'Review images and videos' })
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
media: Record<string, any>[];
|
||||
|
||||
@ApiProperty({ description: 'Visit date' })
|
||||
@Column({ name: 'visit_date', type: 'date', nullable: true })
|
||||
visitDate: Date;
|
||||
|
||||
@ApiProperty({ description: 'Travel type', example: 'solo' })
|
||||
@Column({ name: 'travel_type', length: 30, nullable: true })
|
||||
travelType: string; // solo, couple, family, business, friends
|
||||
|
||||
@ApiProperty({ description: 'Visit purpose', example: 'leisure' })
|
||||
@Column({ name: 'visit_purpose', length: 30, nullable: true })
|
||||
visitPurpose: string; // leisure, business, special-occasion
|
||||
|
||||
@ApiProperty({ description: 'Recommended for', example: 'couples' })
|
||||
@Column({ name: 'recommended_for', type: 'text', array: true, nullable: true })
|
||||
recommendedFor: string[];
|
||||
|
||||
@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', length: 50, nullable: true })
|
||||
verificationMethod: string; // booking-confirmed, location-verified, receipt-uploaded
|
||||
|
||||
@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;
|
||||
|
||||
@ApiProperty({ description: 'AI-generated tags' })
|
||||
@Column({ name: 'ai_tags', type: 'text', array: true, nullable: true })
|
||||
aiTags: string[];
|
||||
|
||||
@ApiProperty({ description: 'Response from establishment' })
|
||||
@Column({ name: 'establishment_response', type: 'text', nullable: true })
|
||||
establishmentResponse: string;
|
||||
|
||||
@ApiProperty({ description: 'Response date' })
|
||||
@Column({ name: 'response_date', type: 'timestamp', nullable: true })
|
||||
responseDate: Date;
|
||||
|
||||
@ApiProperty({ description: 'Review source', example: 'app' })
|
||||
@Column({ length: 20, default: 'app' })
|
||||
source: string; // app, google, tripadvisor, imported
|
||||
|
||||
@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', nullable: true })
|
||||
placeId: string;
|
||||
|
||||
@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;
|
||||
|
||||
@ApiProperty({ description: 'Interaction type', example: 'monument-recognition' })
|
||||
@Column({ name: 'interaction_type', length: 50 })
|
||||
interactionType: string; // monument-recognition, general-question, ar-content, audio-guide
|
||||
|
||||
@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({ nullable: true })
|
||||
rating: number;
|
||||
|
||||
@ApiProperty({ description: 'Additional metadata' })
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
metadata: Record<string, any>;
|
||||
|
||||
// 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; // 3d-model, overlay-info, historical-recreation, audio-guide
|
||||
|
||||
@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;
|
||||
|
||||
@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', length: 100, nullable: true })
|
||||
historicalPeriod: string;
|
||||
|
||||
@ApiProperty({ description: 'AR tracking markers' })
|
||||
@Column({ name: 'tracking_data', type: 'jsonb', nullable: true })
|
||||
trackingData: Record<string, any>;
|
||||
|
||||
@ApiProperty({ description: 'Content metadata' })
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
metadata: Record<string, any>;
|
||||
|
||||
@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;
|
||||
}
|
||||
29
src/entities/base.entity.ts
Normal file
29
src/entities/base.entity.ts
Normal 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;
|
||||
}
|
||||
37
src/entities/country.entity.ts
Normal file
37
src/entities/country.entity.ts
Normal 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
Normal file
47
src/entities/destination.entity.ts
Normal 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
Normal file
36
src/entities/emergency-alert.entity.ts
Normal 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
Normal file
80
src/entities/establishment.entity.ts
Normal 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;
|
||||
}
|
||||
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;
|
||||
|
||||
@ApiProperty({ description: 'Entry alert message' })
|
||||
@Column({ name: 'entry_message', type: 'text', nullable: true })
|
||||
entryMessage: string;
|
||||
|
||||
@ApiProperty({ description: 'Exit alert message' })
|
||||
@Column({ name: 'exit_message', type: 'text', nullable: true })
|
||||
exitMessage: string;
|
||||
|
||||
@ApiProperty({ description: 'Additional metadata' })
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
metadata: Record<string, any>;
|
||||
|
||||
@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
Normal file
69
src/entities/hotel-checkin.entity.ts
Normal 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
Normal file
44
src/entities/hotel-room.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 { 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
Normal file
69
src/entities/hotel-service.entity.ts
Normal 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
Normal file
65
src/entities/incident.entity.ts
Normal 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
Normal file
56
src/entities/itinerary.entity.ts
Normal 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
Normal file
37
src/entities/language.entity.ts
Normal 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[];
|
||||
}
|
||||
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;
|
||||
|
||||
@ApiProperty({ description: 'Speed in km/h' })
|
||||
@Column({ type: 'decimal', precision: 8, scale: 2, nullable: true })
|
||||
speed: number;
|
||||
|
||||
@ApiProperty({ description: 'Heading in degrees' })
|
||||
@Column({ type: 'decimal', precision: 8, scale: 2, nullable: true })
|
||||
heading: number;
|
||||
|
||||
@ApiProperty({ description: 'Activity type', example: 'walking' })
|
||||
@Column({ length: 50, nullable: true })
|
||||
activity: string;
|
||||
|
||||
@ApiProperty({ description: 'Device info' })
|
||||
@Column({ name: 'device_info', type: 'jsonb', nullable: true })
|
||||
deviceInfo: Record<string, any>;
|
||||
|
||||
@ApiProperty({ description: 'Geofences triggered' })
|
||||
@Column({ name: 'geofences_triggered', type: 'text', array: true, nullable: true })
|
||||
geofencesTriggered: string[];
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User;
|
||||
}
|
||||
72
src/entities/menu-item.entity.ts
Normal file
72
src/entities/menu-item.entity.ts
Normal 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
Normal file
48
src/entities/notification.entity.ts
Normal 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
Normal file
45
src/entities/order-item.entity.ts
Normal 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
Normal file
94
src/entities/order.entity.ts
Normal 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
Normal file
80
src/entities/place-of-interest.entity.ts
Normal 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
Normal file
56
src/entities/product.entity.ts
Normal 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
Normal file
61
src/entities/reservation.entity.ts
Normal 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
Normal file
49
src/entities/review.entity.ts
Normal 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
Normal file
33
src/entities/role.entity.ts
Normal 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
Normal file
40
src/entities/security-officer.entity.ts
Normal 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;
|
||||
}
|
||||
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
Normal file
40
src/entities/table.entity.ts
Normal 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
Normal file
64
src/entities/taxi-driver.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 { 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
Normal file
64
src/entities/tour-guide.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 { 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
Normal file
73
src/entities/transaction.entity.ts
Normal 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
Normal file
53
src/entities/user-preferences.entity.ts
Normal 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
Normal file
94
src/entities/user.entity.ts
Normal 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}`;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
92
src/main.ts
Normal file
92
src/main.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
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')
|
||||
.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 = 3000;
|
||||
await app.listen(port, '0.0.0.0');
|
||||
|
||||
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
Normal file
46
src/modules/analytics/analytics.controller.ts
Normal 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
Normal file
13
src/modules/analytics/analytics.module.ts
Normal 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
Normal file
126
src/modules/analytics/analytics.service.ts
Normal 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
Normal file
36
src/modules/analytics/dto/create-review.dto.ts
Normal 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>;
|
||||
}
|
||||
78
src/modules/auth/auth.controller.ts
Normal file
78
src/modules/auth/auth.controller.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
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);
|
||||
}
|
||||
|
||||
@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
Normal file
30
src/modules/auth/auth.module.ts
Normal 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 {}
|
||||
144
src/modules/auth/auth.service.ts
Normal file
144
src/modules/auth/auth.service.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
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'],
|
||||
});
|
||||
}
|
||||
|
||||
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
Normal file
16
src/modules/auth/dto/auth-response.dto.ts
Normal 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
Normal file
12
src/modules/auth/dto/login.dto.ts
Normal 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
Normal file
41
src/modules/auth/dto/register.dto.ts
Normal 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
Normal file
35
src/modules/auth/strategies/jwt.strategy.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
166
src/modules/commerce/commerce.controller.ts
Normal file
166
src/modules/commerce/commerce.controller.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, Request, ParseBoolPipe
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam
|
||||
} from '@nestjs/swagger';
|
||||
import { CommerceService } from './commerce.service';
|
||||
import { CreateEstablishmentDto } from './dto/create-establishment.dto';
|
||||
import { UpdateEstablishmentDto } from './dto/update-establishment.dto';
|
||||
import { CreateReservationDto } from './dto/create-reservation.dto';
|
||||
import { UpdateReservationDto } from './dto/update-reservation.dto';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../../common/guards/roles.guard';
|
||||
import { Roles } from '../../common/decorators/roles.decorator';
|
||||
import { Establishment } from '../../entities/establishment.entity';
|
||||
import { Reservation } from '../../entities/reservation.entity';
|
||||
|
||||
@ApiTags('Commerce')
|
||||
@Controller('commerce')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
export class CommerceController {
|
||||
constructor(private readonly commerceService: CommerceService) {}
|
||||
|
||||
// ESTABLISHMENTS ENDPOINTS
|
||||
@Post('establishments')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles('admin', 'establishment')
|
||||
@ApiOperation({ summary: 'Register a new establishment' })
|
||||
@ApiResponse({ status: 201, description: 'Establishment created successfully', type: Establishment })
|
||||
createEstablishment(@Body() createEstablishmentDto: CreateEstablishmentDto) {
|
||||
return this.commerceService.createEstablishment(createEstablishmentDto);
|
||||
}
|
||||
|
||||
@Get('establishments')
|
||||
@ApiOperation({ summary: 'Get all establishments with filters' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@ApiQuery({ name: 'type', required: false, type: String, description: 'Filter by type (restaurant, hotel, store)' })
|
||||
@ApiQuery({ name: 'category', required: false, type: String, description: 'Filter by category' })
|
||||
@ApiQuery({ name: 'isVerified', required: false, type: Boolean, description: 'Filter by verification status' })
|
||||
findAllEstablishments(
|
||||
@Query('page') page?: number,
|
||||
@Query('limit') limit?: number,
|
||||
@Query('type') type?: string,
|
||||
@Query('category') category?: string,
|
||||
@Query('isVerified', ParseBoolPipe) isVerified?: boolean,
|
||||
) {
|
||||
return this.commerceService.findAllEstablishments(page, limit, type, category, isVerified);
|
||||
}
|
||||
|
||||
@Get('establishments/search')
|
||||
@ApiOperation({ summary: 'Search establishments by name or description' })
|
||||
@ApiQuery({ name: 'q', type: String, description: 'Search query' })
|
||||
@ApiQuery({ name: 'type', required: false, type: String, description: 'Filter by type' })
|
||||
searchEstablishments(
|
||||
@Query('q') query: string,
|
||||
@Query('type') type?: string,
|
||||
) {
|
||||
return this.commerceService.searchEstablishments(query, type);
|
||||
}
|
||||
|
||||
@Get('establishments/:id')
|
||||
@ApiOperation({ summary: 'Get establishment by ID' })
|
||||
@ApiParam({ name: 'id', type: 'string' })
|
||||
@ApiResponse({ status: 200, type: Establishment })
|
||||
findOneEstablishment(@Param('id') id: string) {
|
||||
return this.commerceService.findOneEstablishment(id);
|
||||
}
|
||||
|
||||
@Patch('establishments/:id')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles('admin', 'establishment')
|
||||
@ApiOperation({ summary: 'Update establishment' })
|
||||
@ApiParam({ name: 'id', type: 'string' })
|
||||
updateEstablishment(
|
||||
@Param('id') id: string,
|
||||
@Body() updateEstablishmentDto: UpdateEstablishmentDto,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.commerceService.updateEstablishment(id, updateEstablishmentDto, req.user.id);
|
||||
}
|
||||
|
||||
@Delete('establishments/:id')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles('admin', 'establishment')
|
||||
@ApiOperation({ summary: 'Deactivate establishment' })
|
||||
@ApiParam({ name: 'id', type: 'string' })
|
||||
removeEstablishment(@Param('id') id: string, @Request() req) {
|
||||
return this.commerceService.removeEstablishment(id, req.user.id);
|
||||
}
|
||||
|
||||
// RESERVATIONS ENDPOINTS
|
||||
@Post('reservations')
|
||||
@ApiOperation({ summary: 'Create a new reservation' })
|
||||
@ApiResponse({ status: 201, description: 'Reservation created successfully', type: Reservation })
|
||||
createReservation(@Body() createReservationDto: CreateReservationDto) {
|
||||
return this.commerceService.createReservation(createReservationDto);
|
||||
}
|
||||
|
||||
@Get('reservations')
|
||||
@ApiOperation({ summary: 'Get reservations with filters' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@ApiQuery({ name: 'userId', required: false, type: String, description: 'Filter by user ID' })
|
||||
@ApiQuery({ name: 'establishmentId', required: false, type: String, description: 'Filter by establishment ID' })
|
||||
@ApiQuery({ name: 'status', required: false, type: String, description: 'Filter by status' })
|
||||
findAllReservations(
|
||||
@Query('page') page?: number,
|
||||
@Query('limit') limit?: number,
|
||||
@Query('userId') userId?: string,
|
||||
@Query('establishmentId') establishmentId?: string,
|
||||
@Query('status') status?: string,
|
||||
) {
|
||||
return this.commerceService.findAllReservations(page, limit, userId, establishmentId, status);
|
||||
}
|
||||
|
||||
@Get('reservations/my')
|
||||
@ApiOperation({ summary: 'Get current user reservations' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@ApiQuery({ name: 'status', required: false, type: String })
|
||||
getMyReservations(
|
||||
@Request() req,
|
||||
@Query('page') page?: number,
|
||||
@Query('limit') limit?: number,
|
||||
@Query('status') status?: string,
|
||||
) {
|
||||
return this.commerceService.findAllReservations(page, limit, req.user.id, undefined, status);
|
||||
}
|
||||
|
||||
@Get('reservations/:id')
|
||||
@ApiOperation({ summary: 'Get reservation by ID' })
|
||||
@ApiParam({ name: 'id', type: 'string' })
|
||||
@ApiResponse({ status: 200, type: Reservation })
|
||||
findOneReservation(@Param('id') id: string) {
|
||||
return this.commerceService.findOneReservation(id);
|
||||
}
|
||||
|
||||
@Patch('reservations/:id')
|
||||
@ApiOperation({ summary: 'Update reservation' })
|
||||
@ApiParam({ name: 'id', type: 'string' })
|
||||
updateReservation(
|
||||
@Param('id') id: string,
|
||||
@Body() updateReservationDto: UpdateReservationDto,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.commerceService.updateReservation(id, updateReservationDto, req.user.id);
|
||||
}
|
||||
|
||||
@Patch('reservations/:id/cancel')
|
||||
@ApiOperation({ summary: 'Cancel reservation' })
|
||||
@ApiParam({ name: 'id', type: 'string' })
|
||||
cancelReservation(@Param('id') id: string, @Request() req) {
|
||||
return this.commerceService.cancelReservation(id, req.user.id);
|
||||
}
|
||||
|
||||
// STATISTICS
|
||||
@Get('stats')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: 'Get commerce statistics (Admin only)' })
|
||||
getCommerceStats() {
|
||||
return this.commerceService.getCommerceStats();
|
||||
}
|
||||
}
|
||||
25
src/modules/commerce/commerce.module.ts
Normal file
25
src/modules/commerce/commerce.module.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CommerceService } from './commerce.service';
|
||||
import { CommerceController } from './commerce.controller';
|
||||
import { Establishment } from '../../entities/establishment.entity';
|
||||
import { Reservation } from '../../entities/reservation.entity';
|
||||
import { Product } from '../../entities/product.entity';
|
||||
import { HotelRoom } from '../../entities/hotel-room.entity';
|
||||
import { Transaction } from '../../entities/transaction.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
Establishment,
|
||||
Reservation,
|
||||
Product,
|
||||
HotelRoom,
|
||||
Transaction,
|
||||
]),
|
||||
],
|
||||
controllers: [CommerceController],
|
||||
providers: [CommerceService],
|
||||
exports: [CommerceService],
|
||||
})
|
||||
export class CommerceModule {}
|
||||
227
src/modules/commerce/commerce.service.ts
Normal file
227
src/modules/commerce/commerce.service.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Establishment } from '../../entities/establishment.entity';
|
||||
import { Reservation } from '../../entities/reservation.entity';
|
||||
import { Product } from '../../entities/product.entity';
|
||||
import { HotelRoom } from '../../entities/hotel-room.entity';
|
||||
import { Transaction } from '../../entities/transaction.entity';
|
||||
import { CreateEstablishmentDto } from './dto/create-establishment.dto';
|
||||
import { UpdateEstablishmentDto } from './dto/update-establishment.dto';
|
||||
import { CreateReservationDto } from './dto/create-reservation.dto';
|
||||
import { UpdateReservationDto } from './dto/update-reservation.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CommerceService {
|
||||
constructor(
|
||||
@InjectRepository(Establishment)
|
||||
private readonly establishmentRepository: Repository<Establishment>,
|
||||
@InjectRepository(Reservation)
|
||||
private readonly reservationRepository: Repository<Reservation>,
|
||||
@InjectRepository(Product)
|
||||
private readonly productRepository: Repository<Product>,
|
||||
@InjectRepository(HotelRoom)
|
||||
private readonly hotelRoomRepository: Repository<HotelRoom>,
|
||||
@InjectRepository(Transaction)
|
||||
private readonly transactionRepository: Repository<Transaction>,
|
||||
) {}
|
||||
|
||||
// Establishments CRUD
|
||||
async createEstablishment(createEstablishmentDto: CreateEstablishmentDto): Promise<Establishment> {
|
||||
const establishment = this.establishmentRepository.create(createEstablishmentDto);
|
||||
return this.establishmentRepository.save(establishment);
|
||||
}
|
||||
|
||||
async findAllEstablishments(
|
||||
page: number = 1,
|
||||
limit: number = 10,
|
||||
type?: string,
|
||||
category?: string,
|
||||
isVerified?: boolean
|
||||
): Promise<{
|
||||
establishments: Establishment[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}> {
|
||||
const query = this.establishmentRepository.createQueryBuilder('establishment')
|
||||
.leftJoinAndSelect('establishment.owner', 'owner')
|
||||
.where('establishment.isActive = :active', { active: true });
|
||||
|
||||
if (type) {
|
||||
query.andWhere('establishment.type = :type', { type });
|
||||
}
|
||||
|
||||
if (category) {
|
||||
query.andWhere('establishment.category = :category', { category });
|
||||
}
|
||||
|
||||
if (isVerified !== undefined) {
|
||||
query.andWhere('establishment.isVerified = :isVerified', { isVerified });
|
||||
}
|
||||
|
||||
const [establishments, total] = await query
|
||||
.skip((page - 1) * limit)
|
||||
.take(limit)
|
||||
.orderBy('establishment.rating', 'DESC')
|
||||
.getManyAndCount();
|
||||
|
||||
return { establishments, total, page, limit };
|
||||
}
|
||||
|
||||
async findOneEstablishment(id: string): Promise<Establishment> {
|
||||
const establishment = await this.establishmentRepository.findOne({
|
||||
where: { id, isActive: true },
|
||||
relations: ['owner'],
|
||||
});
|
||||
|
||||
if (!establishment) {
|
||||
throw new NotFoundException(`Establishment with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return establishment;
|
||||
}
|
||||
|
||||
async updateEstablishment(id: string, updateEstablishmentDto: UpdateEstablishmentDto, userId: string): Promise<Establishment> {
|
||||
const establishment = await this.findOneEstablishment(id);
|
||||
|
||||
// Check if user owns the establishment or is admin
|
||||
if (establishment.userId !== userId) {
|
||||
throw new ForbiddenException('You can only update your own establishment');
|
||||
}
|
||||
|
||||
await this.establishmentRepository.update(id, updateEstablishmentDto);
|
||||
return this.findOneEstablishment(id);
|
||||
}
|
||||
|
||||
async removeEstablishment(id: string, userId: string): Promise<void> {
|
||||
const establishment = await this.findOneEstablishment(id);
|
||||
|
||||
if (establishment.userId !== userId) {
|
||||
throw new ForbiddenException('You can only delete your own establishment');
|
||||
}
|
||||
|
||||
await this.establishmentRepository.update(id, { isActive: false });
|
||||
}
|
||||
|
||||
async searchEstablishments(query: string, type?: string): Promise<Establishment[]> {
|
||||
const searchQuery = this.establishmentRepository.createQueryBuilder('establishment')
|
||||
.leftJoinAndSelect('establishment.owner', 'owner')
|
||||
.where('establishment.isActive = :active', { active: true })
|
||||
.andWhere('(establishment.name ILIKE :query OR establishment.description ILIKE :query)',
|
||||
{ query: `%${query}%` });
|
||||
|
||||
if (type) {
|
||||
searchQuery.andWhere('establishment.type = :type', { type });
|
||||
}
|
||||
|
||||
return searchQuery
|
||||
.orderBy('establishment.rating', 'DESC')
|
||||
.limit(20)
|
||||
.getMany();
|
||||
}
|
||||
|
||||
// Reservations CRUD
|
||||
async createReservation(createReservationDto: CreateReservationDto): Promise<Reservation> {
|
||||
const reservation = this.reservationRepository.create(createReservationDto);
|
||||
return this.reservationRepository.save(reservation);
|
||||
}
|
||||
|
||||
async findAllReservations(
|
||||
page: number = 1,
|
||||
limit: number = 10,
|
||||
userId?: string,
|
||||
establishmentId?: string,
|
||||
status?: string
|
||||
): Promise<{
|
||||
reservations: Reservation[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}> {
|
||||
const query = this.reservationRepository.createQueryBuilder('reservation')
|
||||
.leftJoinAndSelect('reservation.establishment', 'establishment')
|
||||
.leftJoinAndSelect('reservation.user', 'user');
|
||||
|
||||
if (userId) {
|
||||
query.andWhere('reservation.userId = :userId', { userId });
|
||||
}
|
||||
|
||||
if (establishmentId) {
|
||||
query.andWhere('reservation.establishmentId = :establishmentId', { establishmentId });
|
||||
}
|
||||
|
||||
if (status) {
|
||||
query.andWhere('reservation.status = :status', { status });
|
||||
}
|
||||
|
||||
const [reservations, total] = await query
|
||||
.skip((page - 1) * limit)
|
||||
.take(limit)
|
||||
.orderBy('reservation.createdAt', 'DESC')
|
||||
.getManyAndCount();
|
||||
|
||||
return { reservations, total, page, limit };
|
||||
}
|
||||
|
||||
async findOneReservation(id: string): Promise<Reservation> {
|
||||
const reservation = await this.reservationRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['establishment', 'user'],
|
||||
});
|
||||
|
||||
if (!reservation) {
|
||||
throw new NotFoundException(`Reservation with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return reservation;
|
||||
}
|
||||
|
||||
async updateReservation(id: string, updateReservationDto: UpdateReservationDto, userId: string): Promise<Reservation> {
|
||||
const reservation = await this.findOneReservation(id);
|
||||
|
||||
// Check if user owns the reservation or the establishment
|
||||
if (reservation.userId !== userId && reservation.establishment.userId !== userId) {
|
||||
throw new ForbiddenException('You can only update your own reservations');
|
||||
}
|
||||
|
||||
await this.reservationRepository.update(id, updateReservationDto);
|
||||
return this.findOneReservation(id);
|
||||
}
|
||||
|
||||
async cancelReservation(id: string, userId: string): Promise<Reservation> {
|
||||
const reservation = await this.findOneReservation(id);
|
||||
|
||||
if (reservation.userId !== userId && reservation.establishment.userId !== userId) {
|
||||
throw new ForbiddenException('You can only cancel your own reservations');
|
||||
}
|
||||
|
||||
await this.reservationRepository.update(id, { status: 'cancelled' });
|
||||
return this.findOneReservation(id);
|
||||
}
|
||||
|
||||
// Statistics
|
||||
async getCommerceStats(): Promise<{
|
||||
establishments: number;
|
||||
reservations: number;
|
||||
products: number;
|
||||
revenue: number;
|
||||
}> {
|
||||
const [establishments, reservations, products] = await Promise.all([
|
||||
this.establishmentRepository.count({ where: { isActive: true } }),
|
||||
this.reservationRepository.count(),
|
||||
this.productRepository.count({ where: { isActive: true } }),
|
||||
]);
|
||||
|
||||
// Calculate total revenue from completed transactions
|
||||
const revenueResult = await this.transactionRepository
|
||||
.createQueryBuilder('transaction')
|
||||
.select('SUM(transaction.amount)', 'total')
|
||||
.where('transaction.status = :status', { status: 'completed' })
|
||||
.getRawOne();
|
||||
|
||||
const revenue = parseFloat(revenueResult.total) || 0;
|
||||
|
||||
return { establishments, reservations, products, revenue };
|
||||
}
|
||||
}
|
||||
74
src/modules/commerce/dto/create-establishment.dto.ts
Normal file
74
src/modules/commerce/dto/create-establishment.dto.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { IsString, IsOptional, IsBoolean, IsArray, IsEmail } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreateEstablishmentDto {
|
||||
@ApiProperty({ description: 'Owner user ID' })
|
||||
@IsString()
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ description: 'Establishment type', example: 'restaurant' })
|
||||
@IsString()
|
||||
type: string;
|
||||
|
||||
@ApiProperty({ description: 'Establishment name', example: 'La Casita Restaurant' })
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Description' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Category', example: 'caribbean-cuisine' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
category?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Address' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
address?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Coordinates (lat,lng)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
coordinates?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Phone number' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Email' })
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Website' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
website?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Business hours' })
|
||||
@IsOptional()
|
||||
businessHours?: Record<string, any>;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Images' })
|
||||
@IsOptional()
|
||||
images?: Record<string, any>;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Amenities' })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
amenities?: string[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Is verified', example: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isVerified?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Is active', example: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
51
src/modules/commerce/dto/create-reservation.dto.ts
Normal file
51
src/modules/commerce/dto/create-reservation.dto.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { IsString, IsOptional, IsNumber, IsDateString } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreateReservationDto {
|
||||
@ApiProperty({ description: 'Establishment ID' })
|
||||
@IsString()
|
||||
establishmentId: string;
|
||||
|
||||
@ApiProperty({ description: 'User ID' })
|
||||
@IsString()
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ description: 'Reservation type', example: 'room' })
|
||||
@IsString()
|
||||
type: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Reference ID (room, table, etc.)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
referenceId?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Check-in date', example: '2025-07-01' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
checkInDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Check-out date', example: '2025-07-03' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
checkOutDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Check-in time', example: '15:00' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
checkInTime?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Number of guests', example: 2 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
guestsCount?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Special requests' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
specialRequests?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Total amount', example: 240.00 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
totalAmount?: number;
|
||||
}
|
||||
4
src/modules/commerce/dto/update-establishment.dto.ts
Normal file
4
src/modules/commerce/dto/update-establishment.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateEstablishmentDto } from './create-establishment.dto';
|
||||
|
||||
export class UpdateEstablishmentDto extends PartialType(CreateEstablishmentDto) {}
|
||||
4
src/modules/commerce/dto/update-reservation.dto.ts
Normal file
4
src/modules/commerce/dto/update-reservation.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateReservationDto } from './create-reservation.dto';
|
||||
|
||||
export class UpdateReservationDto extends PartialType(CreateReservationDto) {}
|
||||
11
src/modules/communication/communication.module.ts
Normal file
11
src/modules/communication/communication.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { EmailService } from './email.service';
|
||||
import { WhatsAppService } from './whatsapp.service';
|
||||
import { WhatsAppController } from './whatsapp.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [WhatsAppController],
|
||||
providers: [EmailService, WhatsAppService],
|
||||
exports: [EmailService, WhatsAppService],
|
||||
})
|
||||
export class CommunicationModule {}
|
||||
225
src/modules/communication/email.service.ts
Normal file
225
src/modules/communication/email.service.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as sgMail from '@sendgrid/mail';
|
||||
|
||||
interface EmailTemplate {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
text?: string;
|
||||
templateData?: Record<string, any>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
constructor(private configService: ConfigService) {
|
||||
const apiKey = this.configService.get<string>('communication.sendgrid.apiKey');
|
||||
if (!apiKey) {
|
||||
throw new Error('SendGrid API key is not configured');
|
||||
}
|
||||
sgMail.setApiKey(apiKey);
|
||||
}
|
||||
|
||||
async sendEmail(emailData: EmailTemplate): Promise<void> {
|
||||
try {
|
||||
const fromEmail = this.configService.get<string>('communication.sendgrid.fromEmail') || 'noreply@karibeo.com';
|
||||
const fromName = this.configService.get<string>('communication.sendgrid.fromName') || 'Karibeo';
|
||||
|
||||
const msg = {
|
||||
to: emailData.to,
|
||||
from: {
|
||||
email: fromEmail,
|
||||
name: fromName,
|
||||
},
|
||||
subject: emailData.subject,
|
||||
html: emailData.html,
|
||||
text: emailData.text || this.stripHtml(emailData.html),
|
||||
};
|
||||
|
||||
await sgMail.send(msg);
|
||||
} catch (error) {
|
||||
throw new BadRequestException(`Email sending failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async sendBookingConfirmation(
|
||||
email: string,
|
||||
userName: string,
|
||||
bookingDetails: {
|
||||
establishmentName: string;
|
||||
checkIn: string;
|
||||
checkOut: string;
|
||||
totalAmount: number;
|
||||
confirmationNumber: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: #007bff; color: white; padding: 20px; text-align: center; }
|
||||
.content { padding: 20px; background: #f9f9f9; }
|
||||
.booking-details { background: white; padding: 15px; margin: 20px 0; border-radius: 5px; }
|
||||
.footer { text-align: center; padding: 20px; color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🏝️ Karibeo - Booking Confirmed</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>Hello ${userName}!</h2>
|
||||
<p>Your booking has been confirmed! Get ready for an amazing experience in the Caribbean.</p>
|
||||
|
||||
<div class="booking-details">
|
||||
<h3>Booking Details</h3>
|
||||
<p><strong>Establishment:</strong> ${bookingDetails.establishmentName}</p>
|
||||
<p><strong>Check-in:</strong> ${bookingDetails.checkIn}</p>
|
||||
<p><strong>Check-out:</strong> ${bookingDetails.checkOut}</p>
|
||||
<p><strong>Total Amount:</strong> $${bookingDetails.totalAmount}</p>
|
||||
<p><strong>Confirmation Number:</strong> ${bookingDetails.confirmationNumber}</p>
|
||||
</div>
|
||||
|
||||
<p>We're excited to welcome you to the Dominican Republic/Puerto Rico!</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>© 2025 Karibeo - Your Caribbean Adventure Starts Here</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
await this.sendEmail({
|
||||
to: email,
|
||||
subject: `🏝️ Booking Confirmed - ${bookingDetails.establishmentName}`,
|
||||
html,
|
||||
});
|
||||
}
|
||||
|
||||
async sendSecurityAlert(
|
||||
email: string,
|
||||
userName: string,
|
||||
alertDetails: {
|
||||
type: string;
|
||||
location: string;
|
||||
timestamp: string;
|
||||
message: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: #dc3545; color: white; padding: 20px; text-align: center; }
|
||||
.content { padding: 20px; background: #f9f9f9; }
|
||||
.alert-details { background: white; padding: 15px; margin: 20px 0; border-radius: 5px; border-left: 4px solid #dc3545; }
|
||||
.footer { text-align: center; padding: 20px; color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚨 Karibeo Security Alert</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>Hello ${userName},</h2>
|
||||
<p>This is an important security notification regarding your current location.</p>
|
||||
|
||||
<div class="alert-details">
|
||||
<h3>Alert Details</h3>
|
||||
<p><strong>Type:</strong> ${alertDetails.type}</p>
|
||||
<p><strong>Location:</strong> ${alertDetails.location}</p>
|
||||
<p><strong>Time:</strong> ${alertDetails.timestamp}</p>
|
||||
<p><strong>Message:</strong> ${alertDetails.message}</p>
|
||||
</div>
|
||||
|
||||
<p>For immediate assistance, contact POLITUR or use the emergency button in your Karibeo app.</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>© 2025 Karibeo - Your Safety is Our Priority</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
await this.sendEmail({
|
||||
to: email,
|
||||
subject: `🚨 Security Alert - ${alertDetails.type}`,
|
||||
html,
|
||||
});
|
||||
}
|
||||
|
||||
async sendWelcomeEmail(email: string, userName: string): Promise<void> {
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: linear-gradient(135deg, #007bff, #28a745); color: white; padding: 30px; text-align: center; }
|
||||
.content { padding: 30px; background: #f9f9f9; }
|
||||
.features { display: flex; flex-wrap: wrap; gap: 20px; margin: 20px 0; }
|
||||
.feature { background: white; padding: 20px; border-radius: 10px; flex: 1; min-width: 200px; }
|
||||
.footer { text-align: center; padding: 20px; color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🏝️ Welcome to Karibeo!</h1>
|
||||
<p>Your Caribbean Adventure Starts Here</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>Hello ${userName}!</h2>
|
||||
<p>Welcome to Karibeo, your ultimate companion for exploring the Dominican Republic and Puerto Rico!</p>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature">
|
||||
<h3>🏨 Book Accommodations</h3>
|
||||
<p>Find and book the perfect place to stay</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>🗺️ Discover Places</h3>
|
||||
<p>Explore hidden gems and popular attractions</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>👨🏫 Tour Guides</h3>
|
||||
<p>Connect with local expert guides</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>🚗 Transportation</h3>
|
||||
<p>Safe and reliable taxi services</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Start exploring now and make unforgettable memories in the Caribbean!</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>© 2025 Karibeo - Discover the Caribbean Like Never Before</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
await this.sendEmail({
|
||||
to: email,
|
||||
subject: '🏝️ Welcome to Karibeo - Your Caribbean Adventure Awaits!',
|
||||
html,
|
||||
});
|
||||
}
|
||||
|
||||
private stripHtml(html: string): string {
|
||||
return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
}
|
||||
44
src/modules/communication/whatsapp.controller.ts
Normal file
44
src/modules/communication/whatsapp.controller.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Controller, Post, Get, Body, Query, Res, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
import { WhatsAppService } from './whatsapp.service';
|
||||
|
||||
@ApiTags('WhatsApp')
|
||||
@Controller('whatsapp')
|
||||
export class WhatsAppController {
|
||||
constructor(private readonly whatsAppService: WhatsAppService) {}
|
||||
|
||||
@Get('webhook')
|
||||
@ApiOperation({ summary: 'WhatsApp webhook verification' })
|
||||
@ApiQuery({ name: 'hub.mode', description: 'Webhook mode' })
|
||||
@ApiQuery({ name: 'hub.verify_token', description: 'Verification token' })
|
||||
@ApiQuery({ name: 'hub.challenge', description: 'Challenge string' })
|
||||
async verifyWebhook(
|
||||
@Query('hub.mode') mode: string,
|
||||
@Query('hub.verify_token') token: string,
|
||||
@Query('hub.challenge') challenge: string,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const verification = await this.whatsAppService.verifyWebhook(mode, token, challenge);
|
||||
|
||||
if (verification) {
|
||||
res.status(HttpStatus.OK).send(verification);
|
||||
} else {
|
||||
res.status(HttpStatus.FORBIDDEN).send('Forbidden');
|
||||
}
|
||||
}
|
||||
|
||||
@Post('webhook')
|
||||
@ApiOperation({ summary: 'WhatsApp webhook endpoint' })
|
||||
@ApiResponse({ status: 200, description: 'Webhook processed successfully' })
|
||||
async handleWebhook(@Body() body: any, @Res() res: Response) {
|
||||
await this.whatsAppService.handleIncomingMessage(body);
|
||||
res.status(HttpStatus.OK).send('OK');
|
||||
}
|
||||
|
||||
@Post('send-test')
|
||||
@ApiOperation({ summary: 'Send test WhatsApp message' })
|
||||
async sendTestMessage(@Body() body: { to: string; message: string }) {
|
||||
return this.whatsAppService.sendTextMessage(body.to, body.message);
|
||||
}
|
||||
}
|
||||
232
src/modules/communication/whatsapp.service.ts
Normal file
232
src/modules/communication/whatsapp.service.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import axios from 'axios';
|
||||
|
||||
interface WhatsAppMessage {
|
||||
to: string;
|
||||
type: 'text' | 'template' | 'location' | 'image';
|
||||
content: any;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WhatsAppService {
|
||||
private apiUrl: string;
|
||||
private accessToken: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.apiUrl = this.configService.get<string>('communication.whatsapp.apiUrl') || '';
|
||||
this.accessToken = this.configService.get<string>('communication.whatsapp.accessToken') || '';
|
||||
|
||||
if (!this.apiUrl || !this.accessToken) {
|
||||
console.warn('WhatsApp API credentials not configured');
|
||||
}
|
||||
}
|
||||
|
||||
async sendTextMessage(to: string, message: string): Promise<void> {
|
||||
try {
|
||||
const payload = {
|
||||
messaging_product: 'whatsapp',
|
||||
to: this.formatPhoneNumber(to),
|
||||
type: 'text',
|
||||
text: {
|
||||
body: message,
|
||||
},
|
||||
};
|
||||
|
||||
await this.makeRequest(payload);
|
||||
} catch (error) {
|
||||
throw new BadRequestException(`WhatsApp message failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async sendBookingConfirmation(
|
||||
to: string,
|
||||
bookingDetails: {
|
||||
userName: string;
|
||||
establishmentName: string;
|
||||
checkIn: string;
|
||||
checkOut: string;
|
||||
confirmationNumber: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
const message = `🏝️ *Karibeo - Booking Confirmed!*
|
||||
|
||||
Hello ${bookingDetails.userName}!
|
||||
|
||||
Your Caribbean adventure is confirmed! ✅
|
||||
|
||||
📍 *${bookingDetails.establishmentName}*
|
||||
📅 Check-in: ${bookingDetails.checkIn}
|
||||
📅 Check-out: ${bookingDetails.checkOut}
|
||||
🎫 Confirmation: ${bookingDetails.confirmationNumber}
|
||||
|
||||
We can't wait to welcome you to paradise! 🌴
|
||||
|
||||
Need help? Just reply to this message.`;
|
||||
|
||||
await this.sendTextMessage(to, message);
|
||||
}
|
||||
|
||||
async sendSecurityAlert(
|
||||
to: string,
|
||||
alertDetails: {
|
||||
userName: string;
|
||||
type: string;
|
||||
location: string;
|
||||
timestamp: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
const message = `🚨 *KARIBEO SECURITY ALERT*
|
||||
|
||||
${alertDetails.userName}, this is an important safety notification.
|
||||
|
||||
⚠️ Alert Type: ${alertDetails.type}
|
||||
📍 Location: ${alertDetails.location}
|
||||
🕐 Time: ${alertDetails.timestamp}
|
||||
|
||||
For immediate assistance:
|
||||
- Contact POLITUR
|
||||
- Use emergency button in Karibeo app
|
||||
- Reply to this message
|
||||
|
||||
Your safety is our priority! 🛡️`;
|
||||
|
||||
await this.sendTextMessage(to, message);
|
||||
}
|
||||
|
||||
async sendTaxiUpdate(
|
||||
to: string,
|
||||
taxiDetails: {
|
||||
driverName: string;
|
||||
vehicleInfo: string;
|
||||
estimatedArrival: string;
|
||||
trackingLink?: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
const message = `🚗 *Your Karibeo Taxi is on the way!*
|
||||
|
||||
Driver: ${taxiDetails.driverName}
|
||||
Vehicle: ${taxiDetails.vehicleInfo}
|
||||
ETA: ${taxiDetails.estimatedArrival}
|
||||
|
||||
${taxiDetails.trackingLink ? `Track your ride: ${taxiDetails.trackingLink}` : ''}
|
||||
|
||||
Safe travels! 🛣️`;
|
||||
|
||||
await this.sendTextMessage(to, message);
|
||||
}
|
||||
|
||||
async sendLocationMessage(to: string, latitude: number, longitude: number, name?: string): Promise<void> {
|
||||
try {
|
||||
const payload = {
|
||||
messaging_product: 'whatsapp',
|
||||
to: this.formatPhoneNumber(to),
|
||||
type: 'location',
|
||||
location: {
|
||||
latitude,
|
||||
longitude,
|
||||
name: name || 'Shared Location',
|
||||
},
|
||||
};
|
||||
|
||||
await this.makeRequest(payload);
|
||||
} catch (error) {
|
||||
throw new BadRequestException(`WhatsApp location message failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async sendImageMessage(to: string, imageUrl: string, caption?: string): Promise<void> {
|
||||
try {
|
||||
const payload = {
|
||||
messaging_product: 'whatsapp',
|
||||
to: this.formatPhoneNumber(to),
|
||||
type: 'image',
|
||||
image: {
|
||||
link: imageUrl,
|
||||
caption: caption || '',
|
||||
},
|
||||
};
|
||||
|
||||
await this.makeRequest(payload);
|
||||
} catch (error) {
|
||||
throw new BadRequestException(`WhatsApp image message failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyWebhook(mode: string, token: string, challenge: string): Promise<string | null> {
|
||||
const verifyToken = this.configService.get<string>('communication.whatsapp.verifyToken');
|
||||
|
||||
if (mode === 'subscribe' && token === verifyToken) {
|
||||
return challenge;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async handleIncomingMessage(body: any): Promise<void> {
|
||||
try {
|
||||
if (body.entry && body.entry[0] && body.entry[0].changes) {
|
||||
const changes = body.entry[0].changes[0];
|
||||
|
||||
if (changes.value && changes.value.messages) {
|
||||
const message = changes.value.messages[0];
|
||||
const from = message.from;
|
||||
const messageBody = message.text?.body || '';
|
||||
|
||||
// Handle incoming message logic here
|
||||
console.log(`Received WhatsApp message from ${from}: ${messageBody}`);
|
||||
|
||||
// Auto-reply with help information
|
||||
await this.sendHelpMessage(from);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing WhatsApp webhook:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendHelpMessage(to: string): Promise<void> {
|
||||
const helpMessage = `👋 *Hello from Karibeo!*
|
||||
|
||||
Thank you for contacting us! Here's how we can help:
|
||||
|
||||
🏨 *Bookings*: Reply "booking" for reservation help
|
||||
🚨 *Emergency*: Reply "emergency" for immediate assistance
|
||||
🗺️ *Places*: Reply "places" for tourist attractions
|
||||
🚗 *Transport*: Reply "taxi" for transportation help
|
||||
|
||||
Or visit our app for instant assistance! 📱
|
||||
|
||||
How can we make your Caribbean experience amazing today? 🌴`;
|
||||
|
||||
await this.sendTextMessage(to, helpMessage);
|
||||
}
|
||||
|
||||
private formatPhoneNumber(phone: string): string {
|
||||
// Remove any non-digit characters and ensure it starts with country code
|
||||
const cleaned = phone.replace(/\D/g, '');
|
||||
|
||||
// If it doesn't start with a country code, assume it's US/DR/PR (+1)
|
||||
if (!cleaned.startsWith('1') && cleaned.length === 10) {
|
||||
return `1${cleaned}`;
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private async makeRequest(payload: any): Promise<any> {
|
||||
try {
|
||||
const response = await axios.post(`${this.apiUrl}/messages`, payload, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('WhatsApp API Error:', error.response?.data || error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/modules/geolocation/dto/geofence.dto.ts
Normal file
52
src/modules/geolocation/dto/geofence.dto.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { IsNumber, IsString, IsOptional, IsEnum, Min } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export enum GeofenceType {
|
||||
TOURIST_ZONE = 'tourist-zone',
|
||||
SAFETY_ALERT = 'safety-alert',
|
||||
ATTRACTION = 'attraction',
|
||||
RESTRICTED_AREA = 'restricted-area',
|
||||
PICKUP_ZONE = 'pickup-zone'
|
||||
}
|
||||
|
||||
export class CreateGeofenceDto {
|
||||
@ApiProperty({ description: 'Geofence name', example: 'Zona Colonial Safety Zone' })
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: 'Center latitude' })
|
||||
@IsNumber()
|
||||
latitude: number;
|
||||
|
||||
@ApiProperty({ description: 'Center longitude' })
|
||||
@IsNumber()
|
||||
longitude: number;
|
||||
|
||||
@ApiProperty({ description: 'Radius in meters', example: 500 })
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
radius: number;
|
||||
|
||||
@ApiProperty({ description: 'Geofence type', enum: GeofenceType })
|
||||
@IsEnum(GeofenceType)
|
||||
type: GeofenceType;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Description' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Alert message for entry' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
entryMessage?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Alert message for exit' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
exitMessage?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Additional metadata' })
|
||||
@IsOptional()
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
32
src/modules/geolocation/dto/location-update.dto.ts
Normal file
32
src/modules/geolocation/dto/location-update.dto.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { IsNumber, IsOptional, IsString } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class LocationUpdateDto {
|
||||
@ApiProperty({ description: 'User latitude' })
|
||||
@IsNumber()
|
||||
latitude: number;
|
||||
|
||||
@ApiProperty({ description: 'User longitude' })
|
||||
@IsNumber()
|
||||
longitude: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Accuracy in meters' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
accuracy?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Speed in km/h' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
speed?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Heading in degrees' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
heading?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Activity type', example: 'walking' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
activity?: string; // walking, driving, stationary, etc.
|
||||
}
|
||||
223
src/modules/geolocation/geolocation.controller.ts
Normal file
223
src/modules/geolocation/geolocation.controller.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import {
|
||||
Controller, Get, Post, Body, Patch, Param, Query, UseGuards, Request
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam
|
||||
} from '@nestjs/swagger';
|
||||
import { GeolocationService } from './geolocation.service';
|
||||
import { CreateGeofenceDto } from './dto/geofence.dto';
|
||||
import { LocationUpdateDto } from './dto/location-update.dto';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../../common/guards/roles.guard';
|
||||
import { Roles } from '../../common/decorators/roles.decorator';
|
||||
import { Geofence } from '../../entities/geofence.entity';
|
||||
|
||||
@ApiTags('Geolocation')
|
||||
@Controller('geolocation')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
export class GeolocationController {
|
||||
constructor(private readonly geolocationService: GeolocationService) {}
|
||||
|
||||
// GEOFENCE MANAGEMENT
|
||||
@Post('geofences')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: 'Create geofence (Admin only)' })
|
||||
@ApiResponse({ status: 201, description: 'Geofence created successfully', type: Geofence })
|
||||
createGeofence(@Body() createGeofenceDto: CreateGeofenceDto) {
|
||||
return this.geolocationService.createGeofence(createGeofenceDto);
|
||||
}
|
||||
|
||||
@Get('geofences')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: 'Get all active geofences (Admin only)' })
|
||||
@ApiResponse({ status: 200, type: [Geofence] })
|
||||
getActiveGeofences() {
|
||||
return this.geolocationService.getActiveGeofences();
|
||||
}
|
||||
|
||||
// LOCATION TRACKING
|
||||
@Post('location/update')
|
||||
@ApiOperation({ summary: 'Update user location' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Location updated successfully',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
geofenceAlerts: { type: 'array' },
|
||||
nearbyAttractions: { type: 'array' },
|
||||
smartSuggestions: { type: 'array', items: { type: 'string' } }
|
||||
}
|
||||
}
|
||||
})
|
||||
updateLocation(@Body() locationDto: LocationUpdateDto, @Request() req) {
|
||||
return this.geolocationService.updateUserLocation(req.user.id, locationDto);
|
||||
}
|
||||
|
||||
@Post('geofences/check')
|
||||
@ApiOperation({ summary: 'Check geofence triggers for location' })
|
||||
checkGeofences(@Body() body: { latitude: number; longitude: number }, @Request() req) {
|
||||
return this.geolocationService.checkGeofenceEntry(
|
||||
req.user.id,
|
||||
body.latitude,
|
||||
body.longitude,
|
||||
);
|
||||
}
|
||||
|
||||
// SMART NAVIGATION
|
||||
@Post('navigation/route')
|
||||
@ApiOperation({ summary: 'Get optimized route with attractions' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Optimized route generated',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
route: { type: 'object' },
|
||||
duration: { type: 'number' },
|
||||
distance: { type: 'number' },
|
||||
waypoints: { type: 'array' },
|
||||
weatherInfo: { type: 'object' },
|
||||
safetyTips: { type: 'array', items: { type: 'string' } }
|
||||
}
|
||||
}
|
||||
})
|
||||
getOptimizedRoute(@Body() body: {
|
||||
startLat: number;
|
||||
startLng: number;
|
||||
endLat: number;
|
||||
endLng: number;
|
||||
travelMode?: string;
|
||||
includeAttractions?: boolean;
|
||||
}) {
|
||||
return this.geolocationService.getOptimizedRoute(
|
||||
body.startLat,
|
||||
body.startLng,
|
||||
body.endLat,
|
||||
body.endLng,
|
||||
body.travelMode,
|
||||
body.includeAttractions,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('nearby/attractions')
|
||||
@ApiOperation({ summary: 'Get nearby attractions' })
|
||||
@ApiQuery({ name: 'latitude', type: Number })
|
||||
@ApiQuery({ name: 'longitude', type: Number })
|
||||
@ApiQuery({ name: 'radius', required: false, type: Number, description: 'Radius in meters' })
|
||||
async getNearbyAttractions(
|
||||
@Query('latitude') latitude: number,
|
||||
@Query('longitude') longitude: number,
|
||||
@Query('radius') radius?: number,
|
||||
) {
|
||||
return this.geolocationService['getNearbyAttractions'](latitude, longitude, radius);
|
||||
}
|
||||
|
||||
// SMART SUGGESTIONS
|
||||
@Post('suggestions/smart')
|
||||
@ApiOperation({ summary: 'Get location-based smart suggestions' })
|
||||
async getSmartSuggestions(@Body() locationDto: LocationUpdateDto, @Request() req) {
|
||||
const nearbyAttractions = await this.geolocationService['getNearbyAttractions'](
|
||||
locationDto.latitude,
|
||||
locationDto.longitude,
|
||||
);
|
||||
|
||||
const suggestions = await this.geolocationService['generateSmartSuggestions'](
|
||||
req.user.id,
|
||||
locationDto,
|
||||
nearbyAttractions,
|
||||
);
|
||||
|
||||
return {
|
||||
suggestions,
|
||||
nearbyAttractions,
|
||||
contextInfo: {
|
||||
currentTime: new Date(),
|
||||
activity: locationDto.activity,
|
||||
speed: locationDto.speed,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// EMERGENCY FEATURES
|
||||
@Post('emergency/panic-button')
|
||||
@ApiOperation({ summary: 'Trigger emergency panic button' })
|
||||
async triggerPanicButton(@Body() body: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
message?: string;
|
||||
}, @Request() req) {
|
||||
// Create emergency alert
|
||||
const alertData = {
|
||||
userId: req.user.id,
|
||||
location: `POINT(${body.longitude} ${body.latitude})`,
|
||||
type: 'emergency',
|
||||
message: body.message || 'Emergency assistance needed',
|
||||
};
|
||||
|
||||
// This would integrate with the SecurityService
|
||||
// const emergencyAlert = await this.securityService.createEmergencyAlert(alertData);
|
||||
|
||||
// Send immediate notifications to nearby POLITUR officers
|
||||
// await this.notificationsService.notifyNearbyOfficers(body.latitude, body.longitude);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Emergency alert sent successfully',
|
||||
estimatedResponseTime: '5-10 minutes',
|
||||
emergencyContact: '+1-809-200-7000', // POLITUR emergency number
|
||||
};
|
||||
}
|
||||
|
||||
// ANALYTICS
|
||||
@Get('analytics')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: 'Get location analytics (Admin only)' })
|
||||
@ApiQuery({ name: 'timeframe', required: false, type: String, description: 'Time period (7d, 30d, 90d)' })
|
||||
getLocationAnalytics(@Query('timeframe') timeframe?: string) {
|
||||
return this.geolocationService.getLocationAnalytics(timeframe);
|
||||
}
|
||||
|
||||
// SAFETY FEATURES
|
||||
@Get('safety/zones')
|
||||
@ApiOperation({ summary: 'Get safety information for area' })
|
||||
@ApiQuery({ name: 'latitude', type: Number })
|
||||
@ApiQuery({ name: 'longitude', type: Number })
|
||||
async getSafetyInfo(
|
||||
@Query('latitude') latitude: number,
|
||||
@Query('longitude') longitude: number,
|
||||
) {
|
||||
// Check safety geofences
|
||||
const geofenceCheck = await this.geolocationService.checkGeofenceEntry(
|
||||
'anonymous', // For public safety info
|
||||
latitude,
|
||||
longitude,
|
||||
);
|
||||
|
||||
const safetyTips = [
|
||||
'Stay in well-lit, populated areas',
|
||||
'Keep your belongings secure',
|
||||
'Use official transportation services',
|
||||
'Have emergency contacts readily available',
|
||||
];
|
||||
|
||||
const emergencyContacts = [
|
||||
{ name: 'POLITUR (Tourist Police)', number: '+1-809-200-7000' },
|
||||
{ name: 'General Emergency', number: '911' },
|
||||
{ name: 'Medical Emergency', number: '+1-809-688-4411' },
|
||||
];
|
||||
|
||||
return {
|
||||
safetyLevel: 'moderate', // This would be calculated based on various factors
|
||||
geofenceAlerts: geofenceCheck.alerts,
|
||||
safetyTips,
|
||||
emergencyContacts,
|
||||
nearbyPoliturStations: [], // TODO: Implement POLITUR station lookup
|
||||
};
|
||||
}
|
||||
}
|
||||
25
src/modules/geolocation/geolocation.module.ts
Normal file
25
src/modules/geolocation/geolocation.module.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { GeolocationService } from './geolocation.service';
|
||||
import { GeolocationController } from './geolocation.controller';
|
||||
import { Geofence } from '../../entities/geofence.entity';
|
||||
import { LocationTracking } from '../../entities/location-tracking.entity';
|
||||
import { PlaceOfInterest } from '../../entities/place-of-interest.entity';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { SecurityModule } from '../security/security.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
Geofence,
|
||||
LocationTracking,
|
||||
PlaceOfInterest,
|
||||
]),
|
||||
NotificationsModule,
|
||||
SecurityModule,
|
||||
],
|
||||
controllers: [GeolocationController],
|
||||
providers: [GeolocationService],
|
||||
exports: [GeolocationService],
|
||||
})
|
||||
export class GeolocationModule {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user