karibeo-api inicial

This commit is contained in:
2026-01-21 12:48:44 -04:00
parent 69f11e1601
commit a5c18f8bf3
174 changed files with 31899 additions and 0 deletions

56
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

99
ecosystem.config.js Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

103
package.json Normal file
View 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"
}
}

View 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
View 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
View 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
View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

View File

@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

View 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
View 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),
},
}));

View 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,
},
}));

View 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,
},
}));

View 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,
},
}));

View 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
View 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',
},
}));

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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[];
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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[];
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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[];
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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}`;
}
}

View 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
View 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();

View 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'],
},
],
};
}
}

View 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 {}

View 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'];
}
}

View 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>;
}

View 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;
}

View 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);
}
}

View 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 {}

View 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) })),
};
}
}

View 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>;
}

View 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;
}

View 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();
}
}

View 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 {}

View 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,
};
}
}

View 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>;
}

View 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;
}
}

View 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 {}

View 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);
}
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}
}

View 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();
}
}

View 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 {}

View 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 };
}
}

View 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;
}

View 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;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateEstablishmentDto } from './create-establishment.dto';
export class UpdateEstablishmentDto extends PartialType(CreateEstablishmentDto) {}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateReservationDto } from './create-reservation.dto';
export class UpdateReservationDto extends PartialType(CreateReservationDto) {}

View 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 {}

View 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();
}
}

View 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);
}
}

View 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;
}
}
}

View 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>;
}

View 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.
}

View 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
};
}
}

View 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