Initial commit

This commit is contained in:
2025-10-10 21:47:56 -04:00
parent 6e445eb057
commit 6e0ad420ab
227 changed files with 34899 additions and 0 deletions

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

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

15124
package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

104
package.json Executable file
View File

@@ -0,0 +1,104 @@
{
"name": "karibeo-api",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.835.0",
"@aws-sdk/s3-request-presigner": "^3.835.0",
"@nestjs/common": "^11.1.3",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.3",
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/schedule": "^6.0.1",
"@nestjs/swagger": "^11.2.0",
"@nestjs/throttler": "^6.4.0",
"@nestjs/typeorm": "^11.0.0",
"@sendgrid/mail": "^8.1.5",
"axios": "^1.10.0",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"joi": "^17.13.3",
"multer": "^2.0.1",
"multer-s3": "^3.0.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pg": "^8.16.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"sharp": "^0.34.2",
"stripe": "^18.2.1",
"swagger-ui-express": "^5.0.1",
"typeorm": "^0.3.25",
"uuid": "^11.1.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/bcrypt": "^5.0.2",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/multer": "^1.4.13",
"@types/node": "^22.15.33",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/supertest": "^6.0.2",
"@types/uuid": "^10.0.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

22
src/app.controller.spec.ts Executable file
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 Executable 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();
}
}

232
src/app.module.ts Executable file
View File

@@ -0,0 +1,232 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ThrottlerModule } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';
import { ThrottlerGuard } from '@nestjs/throttler';
import { ScheduleModule } from '@nestjs/schedule'; // Importado para tareas programadas (ej. sincronización de canales)
// Config imports
import databaseConfig from './config/database.config';
import jwtConfig from './config/jwt.config';
import appConfig from './config/app.config';
import stripeConfig from './config/integrations/stripe.config';
import awsConfig from './config/integrations/aws.config';
import communicationConfig from './config/integrations/communication.config';
// Entity imports
import { User } from './entities/user.entity';
import { Country } from './entities/country.entity';
import { Language } from './entities/language.entity';
import { Role } from './entities/role.entity';
import { UserPreferences } from './entities/user-preferences.entity';
import { Destination } from './entities/destination.entity';
import { PlaceOfInterest } from './entities/place-of-interest.entity';
import { Establishment } from './entities/establishment.entity';
import { TourGuide } from './entities/tour-guide.entity';
import { TaxiDriver } from './entities/taxi-driver.entity';
import { SecurityOfficer } from './entities/security-officer.entity';
import { HotelRoom } from './entities/hotel-room.entity';
import { Product } from './entities/product.entity';
import { Reservation } from './entities/reservation.entity';
import { Transaction } from './entities/transaction.entity';
import { Notification } from './entities/notification.entity';
import { Incident } from './entities/incident.entity';
import { EmergencyAlert } from './entities/emergency-alert.entity';
import { Itinerary } from './entities/itinerary.entity';
// Restaurant entities
import { MenuItem } from './entities/menu-item.entity';
import { Table } from './entities/table.entity';
import { Order } from './entities/order.entity';
import { OrderItem } from './entities/order-item.entity';
// Hotel entities
import { HotelCheckin } from './entities/hotel-checkin.entity';
import { HotelService } from './entities/hotel-service.entity';
// AI/AR entities
import { AIGuideInteraction } from './entities/ai-guide-interaction.entity';
import { ARContent } from './entities/ar-content.entity';
// Geolocation entities
import { Geofence } from './entities/geofence.entity';
import { LocationTracking } from './entities/location-tracking.entity';
// Advanced Reviews entities
import { AdvancedReview } from './entities/advanced-review.entity';
import { ReviewHelpfulness } from './entities/review-helpfulness.entity';
// AI Generator entities
import { AIGeneratedContent } from './entities/ai-generated-content.entity';
// Personalization entities
import { UserPersonalization } from './entities/user-personalization.entity';
// Sustainability entities
import { SustainabilityTracking } from './entities/sustainability-tracking.entity';
import { EcoEstablishment } from './entities/eco-establishment.entity';
// Social Commerce entities
import { InfluencerProfile } from './entities/influencer-profile.entity';
import { CreatorCampaign } from './entities/creator-campaign.entity';
import { UGCContent } from './entities/ugc-content.entity';
// IoT Tourism entities
import { IoTDevice } from './entities/iot-device.entity';
import { SmartTourismData } from './entities/smart-tourism-data.entity';
import { WearableDevice } from './entities/wearable-device.entity';
// Finance entities
import { CommissionRate } from './entities/commission-rate.entity';
import { AdminTransaction } from './entities/admin-transaction.entity';
import { Settlement } from './entities/settlement.entity';
// NUEVAS Entidades para Channel Management, Listings, Vehicle, Flight, Availability
import { Channel } from './entities/channel.entity';
import { Listing } from './entities/listing.entity';
import { Vehicle } from './entities/vehicle.entity';
import { Flight } from './entities/flight.entity';
import { Availability } from './entities/availability.entity';
// Module imports
import { AuthModule } from './modules/auth/auth.module';
import { UsersModule } from './modules/users/users.module';
import { TourismModule } from './modules/tourism/tourism.module';
import { CommerceModule } from './modules/commerce/commerce.module';
import { SecurityModule } from './modules/security/security.module';
import { AnalyticsModule } from './modules/analytics/analytics.module';
import { NotificationsModule } from './modules/notifications/notifications.module';
import { PaymentsModule } from './modules/payments/payments.module';
import { UploadModule } from './modules/upload/upload.module';
import { CommunicationModule } from './modules/communication/communication.module';
import { RestaurantModule } from './modules/restaurant/restaurant.module';
import { HotelModule } from './modules/hotel/hotel.module';
import { AIGuideModule } from './modules/ai-guide/ai-guide.module';
import { GeolocationModule } from './modules/geolocation/geolocation.module';
import { ReviewsModule } from './modules/reviews/reviews.module';
import { AIGeneratorModule } from './modules/ai-generator/ai-generator.module';
import { PersonalizationModule } from './modules/personalization/personalization.module';
import { SustainabilityModule } from './modules/sustainability/sustainability.module';
import { SocialCommerceModule } from './modules/social-commerce/social-commerce.module';
import { IoTTourismModule } from './modules/iot-tourism/iot-tourism.module';
import { FinanceModule } from './modules/finance/finance.module';
// NUEVOS Módulos
import { ChannelManagementModule } from './modules/channel-management/channel-management.module';
import { ListingsModule } from './modules/listings/listings.module';
import { VehicleManagementModule } from './modules/vehicle-management/vehicle-management.module';
import { FlightManagementModule } from './modules/flight-management/flight-management.module';
import { AvailabilityManagementModule } from './modules/availability-management/availability-management.module';
@Module({
imports: [
// Configuration
ConfigModule.forRoot({
isGlobal: true,
load: [
databaseConfig,
jwtConfig,
appConfig,
stripeConfig,
awsConfig,
communicationConfig,
],
envFilePath: '.env',
}),
// Database
TypeOrmModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
...configService.get('database'),
entities: [
User, Country, Language, Role, UserPreferences,
Destination, PlaceOfInterest, Establishment,
TourGuide, TaxiDriver, SecurityOfficer,
HotelRoom, Product, Reservation, Transaction,
Notification, Incident, EmergencyAlert, Itinerary,
// Restaurant entities
MenuItem, Table, Order, OrderItem,
// Hotel entities
HotelCheckin, HotelService,
// AI/AR entities
AIGuideInteraction, ARContent,
// Geolocation entities
Geofence, LocationTracking,
// Advanced Reviews entities
AdvancedReview, ReviewHelpfulness,
// AI Generator entities
AIGeneratedContent,
// Personalization entities
UserPersonalization,
// Sustainability entities
SustainabilityTracking, EcoEstablishment,
// Social Commerce entities
InfluencerProfile, CreatorCampaign, UGCContent,
// IoT Tourism entities
IoTDevice, SmartTourismData, WearableDevice,
// Finance entities
CommissionRate, AdminTransaction, Settlement,
// NUEVAS Entidades
Channel, Listing, Vehicle, Flight, Availability,
],
}),
inject: [ConfigService],
}),
// Rate Limiting
ThrottlerModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
throttlers: [
{
name: 'default',
ttl: configService.get<number>('app.throttle.ttl') || 60,
limit: configService.get<number>('app.throttle.limit') || 100,
},
],
}),
inject: [ConfigService],
}),
// Schedule Module para tareas cron (ej. sincronización de canales)
ScheduleModule.forRoot(),
// ========================================
// TODOS LOS MÓDULOS - AHORA 26 MÓDULOS TOTALES
// ========================================
// Core modules (4)
AuthModule,
UsersModule,
AnalyticsModule,
NotificationsModule,
// Business & Operations modules (9) - Actualizado con nuevos módulos
TourismModule,
CommerceModule,
SecurityModule,
FinanceModule,
RestaurantModule,
HotelModule,
ChannelManagementModule,
ListingsModule,
AvailabilityManagementModule,
// Integration modules (3)
PaymentsModule,
UploadModule,
CommunicationModule,
// Advanced features modules (3)
AIGuideModule,
GeolocationModule,
ReviewsModule,
// Logistics & Booking Modules (2) - Nueva categoría
VehicleManagementModule,
FlightManagementModule,
// Innovation 2025 modules (5)
AIGeneratorModule,
PersonalizationModule,
SustainabilityModule,
SocialCommerceModule,
IoTTourismModule,
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}

8
src/app.service.ts Executable file
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,32 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
if (!user || !user.role) {
return false;
}
// Super admin tiene acceso a todo automáticamente
if (user.role.name === 'super_admin') {
return true;
}
return requiredRoles.some((role) => user.role?.name === role);
}
}

16
src/config/app.config.ts Executable file
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),
},
}));

20
src/config/database.config.ts Executable file
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 Executable 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,52 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { User } from './user.entity';
@Entity({ schema: 'finance', name: 'admin_transactions' })
export class AdminTransaction {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'original_transaction_id', type: 'uuid', nullable: true })
originalTransactionId: string;
@Column({ name: 'merchant_id', type: 'uuid' })
merchantId: string;
@Column({ name: 'service_type', length: 50 })
serviceType: string;
@Column({ name: 'gross_amount', type: 'decimal', precision: 10, scale: 2 })
grossAmount: number;
@Column({ name: 'commission_rate', type: 'decimal', precision: 5, scale: 2 })
commissionRate: number;
@Column({ name: 'commission_amount', type: 'decimal', precision: 10, scale: 2 })
commissionAmount: number;
@Column({ name: 'net_amount', type: 'decimal', precision: 10, scale: 2 })
netAmount: number;
@Column({ length: 3, default: 'USD' })
currency: string;
@Column({ length: 20, default: 'pending' })
status: string; // pending, settled, refunded
@Column({ name: 'payment_intent_id', length: 255, nullable: true })
paymentIntentId: string;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@ManyToOne(() => User)
@JoinColumn({ name: 'merchant_id' })
merchant: User;
}

View File

@@ -0,0 +1,112 @@
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from './base.entity';
import { User } from './user.entity';
@Entity({ name: 'advanced_reviews', schema: 'analytics' })
export class AdvancedReview extends BaseEntity {
@ApiProperty({ description: 'User ID' })
@Column({ name: 'user_id' })
userId: string;
@ApiProperty({ description: 'Reviewable type', example: 'establishment' })
@Column({ name: 'reviewable_type', length: 30 })
reviewableType: string;
@ApiProperty({ description: 'Reviewable ID' })
@Column({ name: 'reviewable_id' })
reviewableId: string;
@ApiProperty({ description: 'Overall rating (1-5)', example: 5 })
@Column({ type: 'integer' })
overallRating: number;
@ApiProperty({ description: 'Detailed ratings by category' })
@Column({ name: 'detailed_ratings', type: 'jsonb', nullable: true })
detailedRatings: Record<string, number> | null;
@ApiProperty({ description: 'Review title', example: 'Amazing experience!' })
@Column({ type: 'varchar', length: 255, nullable: true })
title: string | null;
@ApiProperty({ description: 'Review comment' })
@Column({ type: 'text', nullable: true })
comment: string | null;
@ApiProperty({ description: 'Review pros' })
@Column({ type: 'text', array: true, nullable: true })
pros: string[] | null;
@ApiProperty({ description: 'Review cons' })
@Column({ type: 'text', array: true, nullable: true })
cons: string[] | null;
@ApiProperty({ description: 'Review images and videos' })
@Column({ type: 'jsonb', nullable: true })
media: Record<string, any>[] | null;
@ApiProperty({ description: 'Visit date' })
@Column({ name: 'visit_date', type: 'date', nullable: true })
visitDate: Date | null;
@ApiProperty({ description: 'Travel type', example: 'solo' })
@Column({ name: 'travel_type', type: 'varchar', length: 30, nullable: true })
travelType: string | null;
@ApiProperty({ description: 'Visit purpose', example: 'leisure' })
@Column({ name: 'visit_purpose', type: 'varchar', length: 30, nullable: true })
visitPurpose: string | null;
@ApiProperty({ description: 'Recommended for', example: 'couples' })
@Column({ name: 'recommended_for', type: 'text', array: true, nullable: true })
recommendedFor: string[] | null;
@ApiProperty({ description: 'Language of review', example: 'en' })
@Column({ length: 5, default: 'en' })
language: string;
@ApiProperty({ description: 'Is verified review', example: false })
@Column({ name: 'is_verified', default: false })
isVerified: boolean;
@ApiProperty({ description: 'Verification method' })
@Column({ name: 'verification_method', type: 'varchar', length: 50, nullable: true })
verificationMethod: string | null;
@ApiProperty({ description: 'Helpful count', example: 15 })
@Column({ name: 'helpful_count', default: 0 })
helpfulCount: number;
@ApiProperty({ description: 'Unhelpful count', example: 2 })
@Column({ name: 'unhelpful_count', default: 0 })
unhelpfulCount: number;
@ApiProperty({ description: 'Sentiment analysis score (-1 to 1)' })
@Column({ name: 'sentiment_score', type: 'decimal', precision: 3, scale: 2, nullable: true })
sentimentScore: number | null;
@ApiProperty({ description: 'AI-generated tags' })
@Column({ name: 'ai_tags', type: 'text', array: true, nullable: true })
aiTags: string[] | null;
@ApiProperty({ description: 'Response from establishment' })
@Column({ name: 'establishment_response', type: 'text', nullable: true })
establishmentResponse: string | null;
@ApiProperty({ description: 'Response date' })
@Column({ name: 'response_date', type: 'timestamp', nullable: true })
responseDate: Date | null;
@ApiProperty({ description: 'Review source', example: 'app' })
@Column({ length: 20, default: 'app' })
source: string;
@ApiProperty({ description: 'Is featured review', example: false })
@Column({ name: 'is_featured', default: false })
isFeatured: boolean;
// Relations
@ManyToOne(() => User)
@JoinColumn({ name: 'user_id' })
user: User;
}

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', type: 'uuid', nullable: true })
placeId: string | null;
@ApiProperty({ description: 'User query/question' })
@Column({ name: 'user_query', type: 'text' })
userQuery: string;
@ApiProperty({ description: 'AI response' })
@Column({ name: 'ai_response', type: 'text' })
aiResponse: string;
@ApiProperty({ description: 'User location at time of interaction' })
@Column({ name: 'user_location', type: 'point', nullable: true })
userLocation: string | null;
@ApiProperty({ description: 'Interaction type', example: 'monument-recognition' })
@Column({ name: 'interaction_type', length: 50 })
interactionType: string;
@ApiProperty({ description: 'Language used', example: 'en' })
@Column({ length: 5, default: 'en' })
language: string;
@ApiProperty({ description: 'Session ID for conversation context' })
@Column({ name: 'session_id', length: 100 })
sessionId: string;
@ApiProperty({ description: 'User satisfaction rating' })
@Column({ type: 'integer', nullable: true })
rating: number | null;
@ApiProperty({ description: 'Additional metadata' })
@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any> | null;
// Relations
@ManyToOne(() => User)
@JoinColumn({ name: 'user_id' })
user: User;
@ManyToOne(() => PlaceOfInterest)
@JoinColumn({ name: 'place_id' })
place: PlaceOfInterest;
}

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;
@ApiProperty({ description: 'Content file URL' })
@Column({ name: 'content_url', type: 'text' })
contentUrl: string;
@ApiProperty({ description: 'Thumbnail image URL' })
@Column({ name: 'thumbnail_url', type: 'text', nullable: true })
thumbnailUrl: string | null;
@ApiProperty({ description: 'Trigger coordinates for AR activation' })
@Column({ name: 'trigger_coordinates', type: 'point' })
triggerCoordinates: string;
@ApiProperty({ description: 'Trigger radius in meters', example: 50 })
@Column({ name: 'trigger_radius', type: 'decimal', precision: 8, scale: 2 })
triggerRadius: number;
@ApiProperty({ description: 'Languages available' })
@Column({ type: 'text', array: true })
languages: string[];
@ApiProperty({ description: 'Historical period', example: '1492-1520' })
@Column({ name: 'historical_period', type: 'varchar', length: 100, nullable: true })
historicalPeriod: string | null;
@ApiProperty({ description: 'AR tracking markers' })
@Column({ name: 'tracking_data', type: 'jsonb', nullable: true })
trackingData: Record<string, any> | null;
@ApiProperty({ description: 'Content metadata' })
@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any> | null;
@ApiProperty({ description: 'Is active', example: true })
@Column({ name: 'is_active', default: true })
isActive: boolean;
@ApiProperty({ description: 'Views count' })
@Column({ name: 'views_count', default: 0 })
viewsCount: number;
// Relations
@ManyToOne(() => PlaceOfInterest)
@JoinColumn({ name: 'place_id' })
place: PlaceOfInterest;
}

View File

@@ -0,0 +1,68 @@
import { Entity, Column, Index, Unique } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from './base.entity';
@Entity({ name: 'availability', schema: 'commerce' })
@Unique(['resourceType', 'resourceId', 'date'])
@Index(['resourceType', 'resourceId', 'date'])
export class Availability extends BaseEntity {
@ApiProperty({ description: 'Resource type', example: 'hotel' })
@Column({ name: 'resource_type', length: 50 })
resourceType: string; // hotel, restaurant, vehicle, room, table
@ApiProperty({ description: 'Resource ID (hotel, restaurant, vehicle, etc.)' })
@Column({ name: 'resource_id' })
resourceId: string;
@ApiProperty({ description: 'Available date' })
@Column({ type: 'date' })
date: Date;
@ApiProperty({ description: 'Available quantity', example: 5 })
@Column({ name: 'available_quantity' })
availableQuantity: number;
@ApiProperty({ description: 'Total quantity', example: 10 })
@Column({ name: 'total_quantity' })
totalQuantity: number;
@ApiProperty({ description: 'Booked quantity', example: 3 })
@Column({ name: 'booked_quantity', default: 0 })
bookedQuantity: number;
@ApiProperty({ description: 'Blocked quantity', example: 1 })
@Column({ name: 'blocked_quantity', default: 0 })
blockedQuantity: number;
@ApiProperty({ description: 'Base price for this date' })
@Column({ name: 'base_price', type: 'decimal', precision: 10, scale: 2 })
basePrice: number;
@ApiProperty({ description: 'Dynamic price adjustments' })
@Column({ name: 'price_modifiers', type: 'jsonb', nullable: true })
priceModifiers: Record<string, any> | null;
@ApiProperty({ description: 'Final calculated price' })
@Column({ name: 'final_price', type: 'decimal', precision: 10, scale: 2 })
finalPrice: number;
@ApiProperty({ description: 'Minimum stay requirement', example: 1 })
@Column({ name: 'min_stay', default: 1 })
minStay: number;
@ApiProperty({ description: 'Special restrictions or notes' })
@Column({ type: 'text', nullable: true })
restrictions: string | null;
@ApiProperty({ description: 'Is available for booking', example: true })
@Column({ name: 'is_available', default: true })
isAvailable: boolean;
@ApiProperty({ description: 'Availability status', example: 'open' })
@Column({ length: 20, default: 'open' })
status: string; // open, closed, limited, sold-out
@ApiProperty({ description: 'Last updated by channel sync' })
@Column({ name: 'last_synced', type: 'timestamp', nullable: true })
lastSynced: Date | null;
}

29
src/entities/base.entity.ts Executable file
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,54 @@
import { Entity, Column } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from './base.entity';
@Entity({ name: 'channels', schema: 'commerce' })
export class Channel extends BaseEntity {
@ApiProperty({ description: 'Channel name', example: 'Booking.com' })
@Column({ length: 100 })
name: string;
@ApiProperty({ description: 'Channel type', example: 'hotel' })
@Column({ length: 50 })
type: string; // hotel, restaurant, vehicle, flight
@ApiProperty({ description: 'Channel provider', example: 'booking' })
@Column({ length: 50 })
provider: string; // booking, expedia, airbnb, opentable, etc.
@ApiProperty({ description: 'Connection status', example: 'connected' })
@Column({ length: 20, default: 'disconnected' })
status: string; // connected, disconnected, error, syncing
@ApiProperty({ description: 'API credentials (encrypted)' })
@Column({ type: 'jsonb', nullable: true })
credentials: Record<string, any> | null;
@ApiProperty({ description: 'Channel configuration' })
@Column({ type: 'jsonb', nullable: true })
config: Record<string, any> | null;
@ApiProperty({ description: 'Last sync timestamp' })
@Column({ name: 'last_sync', type: 'timestamp', nullable: true })
lastSync: Date | null;
@ApiProperty({ description: 'Sync frequency in hours', example: 24 })
@Column({ name: 'sync_frequency', default: 24 })
syncFrequency: number;
@ApiProperty({ description: 'Auto sync enabled', example: true })
@Column({ name: 'auto_sync', default: true })
autoSync: boolean;
@ApiProperty({ description: 'Properties synchronized' })
@Column({ name: 'properties_synced', default: 0 })
propertiesSynced: number;
@ApiProperty({ description: 'Last error message' })
@Column({ name: 'last_error', type: 'text', nullable: true })
lastError: string | null;
@ApiProperty({ description: 'Is active', example: true })
@Column({ name: 'is_active', default: true })
isActive: boolean;
}

View File

@@ -0,0 +1,38 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { User } from './user.entity';
@Entity({ schema: 'finance', name: 'commission_rates' })
export class CommissionRate {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'service_type', length: 50 })
serviceType: string;
@Column({ name: 'commission_percentage', type: 'decimal', precision: 5, scale: 2 })
commissionPercentage: number;
@Column({ default: true })
active: boolean;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedBy: string;
@ManyToOne(() => User, { nullable: true })
@JoinColumn({ name: 'updated_by' })
updatedByUser: User;
}

37
src/entities/country.entity.ts Executable file
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,98 @@
import { Entity, Column } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from './base.entity';
@Entity({ name: 'flights', schema: 'tourism' })
export class Flight extends BaseEntity {
@ApiProperty({ description: 'Airline code', example: 'AA' })
@Column({ name: 'airline_code', length: 3 })
airlineCode: string;
@ApiProperty({ description: 'Airline name', example: 'American Airlines' })
@Column({ name: 'airline_name', length: 100 })
airlineName: string;
@ApiProperty({ description: 'Flight number', example: 'AA1234' })
@Column({ name: 'flight_number', length: 10 })
flightNumber: string;
@ApiProperty({ description: 'Origin airport code', example: 'JFK' })
@Column({ name: 'origin_code', length: 3 })
originCode: string;
@ApiProperty({ description: 'Origin airport name', example: 'John F. Kennedy International Airport' })
@Column({ name: 'origin_name', length: 255 })
originName: string;
@ApiProperty({ description: 'Origin city', example: 'New York' })
@Column({ name: 'origin_city', length: 100 })
originCity: string;
@ApiProperty({ description: 'Destination airport code', example: 'SDQ' })
@Column({ name: 'destination_code', length: 3 })
destinationCode: string;
@ApiProperty({ description: 'Destination airport name', example: 'Las Américas International Airport' })
@Column({ name: 'destination_name', length: 255 })
destinationName: string;
@ApiProperty({ description: 'Destination city', example: 'Santo Domingo' })
@Column({ name: 'destination_city', length: 100 })
destinationCity: string;
@ApiProperty({ description: 'Departure date and time' })
@Column({ name: 'departure_time', type: 'timestamp' })
departureTime: Date;
@ApiProperty({ description: 'Arrival date and time' })
@Column({ name: 'arrival_time', type: 'timestamp' })
arrivalTime: Date;
@ApiProperty({ description: 'Flight duration in minutes', example: 240 })
@Column({ name: 'duration_minutes' })
durationMinutes: number;
@ApiProperty({ description: 'Aircraft type', example: 'Boeing 737' })
@Column({ name: 'aircraft_type', length: 50 })
aircraftType: string;
@ApiProperty({ description: 'Available seat classes' })
@Column({ name: 'seat_classes', type: 'jsonb' })
seatClasses: Record<string, any>; // { economy: { available: 150, price: 450 }, business: { available: 20, price: 1200 } }
@ApiProperty({ description: 'Flight status', example: 'scheduled' })
@Column({ length: 20, default: 'scheduled' })
status: string; // scheduled, delayed, cancelled, boarding, departed, arrived
@ApiProperty({ description: 'Stops/layovers' })
@Column({ type: 'jsonb', nullable: true })
stops: Record<string, any>[] | null;
@ApiProperty({ description: 'Baggage policy' })
@Column({ name: 'baggage_policy', type: 'jsonb', nullable: true })
baggagePolicy: Record<string, any> | null;
@ApiProperty({ description: 'Amenities offered' })
@Column({ type: 'text', array: true, nullable: true })
amenities: string[] | null;
@ApiProperty({ description: 'Base price in USD' })
@Column({ name: 'base_price', type: 'decimal', precision: 10, scale: 2 })
basePrice: number;
@ApiProperty({ description: 'Currency', example: 'USD' })
@Column({ length: 3, default: 'USD' })
currency: string;
@ApiProperty({ description: 'Total seats available' })
@Column({ name: 'total_seats' })
totalSeats: number;
@ApiProperty({ description: 'Seats booked' })
@Column({ name: 'seats_booked', default: 0 })
seatsBooked: number;
@ApiProperty({ description: 'Is direct flight', example: true })
@Column({ name: 'is_direct', default: true })
isDirect: boolean;
}

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 | null;
@ApiProperty({ description: 'Entry alert message' })
@Column({ name: 'entry_message', type: 'text', nullable: true })
entryMessage: string | null;
@ApiProperty({ description: 'Exit alert message' })
@Column({ name: 'exit_message', type: 'text', nullable: true })
exitMessage: string | null;
@ApiProperty({ description: 'Additional metadata' })
@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any> | null;
@ApiProperty({ description: 'Is active', example: true })
@Column({ name: 'is_active', default: true })
isActive: boolean;
@ApiProperty({ description: 'Entry count' })
@Column({ name: 'entry_count', default: 0 })
entryCount: number;
}

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

65
src/entities/incident.entity.ts Executable file
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;
}

37
src/entities/language.entity.ts Executable file
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,109 @@
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from './base.entity';
import { Establishment } from './establishment.entity';
import { User } from './user.entity';
@Entity({ name: 'listings', schema: 'commerce' })
export class Listing extends BaseEntity {
@ApiProperty({ description: 'Property owner user ID' })
@Column({ name: 'owner_id' })
ownerId: string;
@ApiProperty({ description: 'Associated establishment ID' })
@Column({ name: 'establishment_id', type: 'uuid', nullable: true })
establishmentId: string | null;
@ApiProperty({ description: 'Listing type', example: 'hotel' })
@Column({ name: 'listing_type', length: 50 })
listingType: string;
@ApiProperty({ description: 'Listing title', example: 'Luxury Beach Resort in Punta Cana' })
@Column({ length: 255 })
title: string;
@ApiProperty({ description: 'Detailed description' })
@Column({ type: 'text' })
description: string;
@ApiProperty({ description: 'Property location' })
@Column({ type: 'point', nullable: true })
coordinates: string | null;
@ApiProperty({ description: 'Address' })
@Column({ type: 'text', nullable: true })
address: string | null;
@ApiProperty({ description: 'Base price per night/hour/day' })
@Column({ name: 'base_price', type: 'decimal', precision: 10, scale: 2 })
basePrice: number;
@ApiProperty({ description: 'Currency', example: 'USD' })
@Column({ length: 3, default: 'USD' })
currency: string;
@ApiProperty({ description: 'Maximum capacity', example: 4 })
@Column({ type: 'integer', nullable: true })
capacity: number | null;
@ApiProperty({ description: 'Amenities list' })
@Column({ type: 'text', array: true, nullable: true })
amenities: string[] | null;
@ApiProperty({ description: 'Property images' })
@Column({ type: 'jsonb', nullable: true })
images: Record<string, any>[] | null;
@ApiProperty({ description: 'Property rules and policies' })
@Column({ type: 'jsonb', nullable: true })
policies: Record<string, any> | null;
@ApiProperty({ description: 'Check-in time', example: '15:00' })
@Column({ name: 'checkin_time', type: 'varchar', nullable: true })
checkinTime: string | null;
@ApiProperty({ description: 'Check-out time', example: '11:00' })
@Column({ name: 'checkout_time', type: 'varchar', nullable: true })
checkoutTime: string | null;
@ApiProperty({ description: 'Minimum stay nights', example: 2 })
@Column({ name: 'min_stay', type: 'integer', nullable: true })
minStay: number | null;
@ApiProperty({ description: 'Maximum stay nights', example: 30 })
@Column({ name: 'max_stay', type: 'integer', nullable: true })
maxStay: number | null;
@ApiProperty({ description: 'Channel distribution settings' })
@Column({ name: 'channel_settings', type: 'jsonb', nullable: true })
channelSettings: Record<string, any> | null;
@ApiProperty({ description: 'Listing status', example: 'published' })
@Column({ length: 20, default: 'draft' })
status: string;
@ApiProperty({ description: 'Average rating', example: 4.5 })
@Column({ type: 'decimal', precision: 3, scale: 2, nullable: true })
rating: number | null;
@ApiProperty({ description: 'Total reviews count' })
@Column({ name: 'reviews_count', default: 0 })
reviewsCount: number;
@ApiProperty({ description: 'Booking count' })
@Column({ name: 'bookings_count', default: 0 })
bookingsCount: number;
@ApiProperty({ description: 'Last updated' })
@Column({ name: 'last_updated', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
lastUpdated: Date;
// Relations
@ManyToOne(() => User)
@JoinColumn({ name: 'owner_id' })
owner: User;
@ManyToOne(() => Establishment)
@JoinColumn({ name: 'establishment_id' })
establishment: Establishment;
}

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 | null;
@ApiProperty({ description: 'Speed in km/h' })
@Column({ type: 'decimal', precision: 8, scale: 2, nullable: true })
speed: number | null;
@ApiProperty({ description: 'Heading in degrees' })
@Column({ type: 'decimal', precision: 8, scale: 2, nullable: true })
heading: number | null;
@ApiProperty({ description: 'Activity type', example: 'walking' })
@Column({ type: 'varchar', length: 50, nullable: true })
activity: string | null;
@ApiProperty({ description: 'Device info' })
@Column({ name: 'device_info', type: 'jsonb', nullable: true })
deviceInfo: Record<string, any> | null;
@ApiProperty({ description: 'Geofences triggered' })
@Column({ name: 'geofences_triggered', type: 'text', array: true, nullable: true })
geofencesTriggered: string[] | null;
// Relations
@ManyToOne(() => User)
@JoinColumn({ name: 'user_id' })
user: User;
}

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

94
src/entities/order.entity.ts Executable file
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;
}

56
src/entities/product.entity.ts Executable file
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;
}

49
src/entities/review.entity.ts Executable file
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;
}

33
src/entities/role.entity.ts Executable file
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,55 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { User } from './user.entity';
@Entity({ schema: 'finance', name: 'settlements' })
export class Settlement {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'merchant_id', type: 'uuid' })
merchantId: string;
@Column({ name: 'settlement_period', length: 20 })
settlementPeriod: string; // weekly, biweekly, monthly
@Column({ name: 'period_start', type: 'date' })
periodStart: Date;
@Column({ name: 'period_end', type: 'date' })
periodEnd: Date;
@Column({ name: 'total_gross', type: 'decimal', precision: 10, scale: 2 })
totalGross: number;
@Column({ name: 'total_commission', type: 'decimal', precision: 10, scale: 2 })
totalCommission: number;
@Column({ name: 'total_net', type: 'decimal', precision: 10, scale: 2 })
totalNet: number;
@Column({ name: 'transaction_count', type: 'integer' })
transactionCount: number;
@Column({ length: 20, default: 'pending' })
status: string; // pending, processing, completed, failed
@Column({ name: 'stripe_transfer_id', length: 255, nullable: true })
stripeTransferId: string;
@Column({ name: 'processed_at', type: 'timestamp', nullable: true })
processedAt: Date;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@ManyToOne(() => User)
@JoinColumn({ name: 'merchant_id' })
merchant: User;
}

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

40
src/entities/table.entity.ts Executable file
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;
}

94
src/entities/user.entity.ts Executable file
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,96 @@
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from './base.entity';
import { User } from './user.entity';
@Entity({ name: 'vehicles', schema: 'tourism' })
export class Vehicle extends BaseEntity {
@ApiProperty({ description: 'Vehicle owner user ID' })
@Column({ name: 'owner_id' })
ownerId: string;
@ApiProperty({ description: 'Vehicle type', example: 'car' })
@Column({ name: 'vehicle_type', length: 50 })
vehicleType: string;
@ApiProperty({ description: 'Vehicle brand', example: 'Toyota' })
@Column({ length: 50 })
brand: string;
@ApiProperty({ description: 'Vehicle model', example: 'Corolla' })
@Column({ length: 50 })
model: string;
@ApiProperty({ description: 'Manufacturing year', example: 2020 })
@Column()
year: number;
@ApiProperty({ description: 'License plate', example: 'A123456' })
@Column({ name: 'license_plate', length: 20, unique: true })
licensePlate: string;
@ApiProperty({ description: 'Vehicle color', example: 'White' })
@Column({ length: 30 })
color: string;
@ApiProperty({ description: 'Seating capacity', example: 5 })
@Column()
capacity: number;
@ApiProperty({ description: 'Transmission type', example: 'automatic' })
@Column({ name: 'transmission_type', length: 20 })
transmissionType: string;
@ApiProperty({ description: 'Fuel type', example: 'gasoline' })
@Column({ name: 'fuel_type', length: 20 })
fuelType: string;
@ApiProperty({ description: 'Daily rental rate' })
@Column({ name: 'daily_rate', type: 'decimal', precision: 8, scale: 2 })
dailyRate: number;
@ApiProperty({ description: 'Currency', example: 'USD' })
@Column({ length: 3, default: 'USD' })
currency: string;
@ApiProperty({ description: 'Vehicle features' })
@Column({ type: 'text', array: true, nullable: true })
features: string[] | null;
@ApiProperty({ description: 'Vehicle images' })
@Column({ type: 'jsonb', nullable: true })
images: Record<string, any>[] | null;
@ApiProperty({ description: 'Current location' })
@Column({ name: 'current_location', type: 'point', nullable: true })
currentLocation: string | null;
@ApiProperty({ description: 'Insurance info' })
@Column({ name: 'insurance_info', type: 'jsonb', nullable: true })
insuranceInfo: Record<string, any> | null;
@ApiProperty({ description: 'Maintenance records' })
@Column({ name: 'maintenance_records', type: 'jsonb', nullable: true })
maintenanceRecords: Record<string, any>[] | null;
@ApiProperty({ description: 'Is available for rental', example: true })
@Column({ name: 'is_available', default: true })
isAvailable: boolean;
@ApiProperty({ description: 'Is verified', example: false })
@Column({ name: 'is_verified', default: false })
isVerified: boolean;
@ApiProperty({ description: 'Average rating', example: 4.2 })
@Column({ type: 'decimal', precision: 3, scale: 2, nullable: true })
rating: number | null;
@ApiProperty({ description: 'Total rentals' })
@Column({ name: 'total_rentals', default: 0 })
totalRentals: number;
// Relations
@ManyToOne(() => User)
@JoinColumn({ name: 'owner_id' })
owner: User;
}

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

112
src/main.ts Executable file
View File

@@ -0,0 +1,112 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe, VersioningType } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
// No SSL - nginx lo maneja
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
// Enable CORS
app.enableCors({
origin: (origin, callback) => {
callback(null, true);
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Requested-With', 'Origin'],
credentials: true,
preflightContinue: false,
optionsSuccessStatus: 204,
exposedHeaders: ['Set-Cookie']
});
// Global prefix
app.setGlobalPrefix('api');
// API Versioning
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
// Global validation pipe
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
// Swagger Documentation
const config = new DocumentBuilder()
.setTitle(configService.get<string>('app.name') || 'Karibeo API')
.setDescription(configService.get<string>('app.description') || 'Tourism API')
.setVersion(configService.get<string>('app.version') || '1.0.0')
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
name: 'JWT',
description: 'Enter JWT token',
in: 'header',
},
'JWT-auth',
)
.addTag('Authentication', 'User authentication and authorization')
.addTag('Users', 'User management operations')
.addTag('Tourism', 'Tourism-related operations')
.addTag('Commerce', 'Commerce and booking operations')
.addTag('Security', 'Security and emergency operations')
.addTag('Analytics', 'Analytics and metrics')
.addTag('Notifications', 'Push, Email, and WhatsApp notifications')
.addTag('Payments', 'Payment processing and transactions (Stripe)')
.addTag('Upload', 'File upload to AWS S3')
.addTag('Communication', 'Email and WhatsApp messaging')
.addTag('Restaurant', 'Restaurant Point of Sale (POS) system')
.addTag('Hotel', 'Hotel management (Rooms, Check-ins, Room Service)')
.addTag('AI Guide', 'AI-powered virtual tour guide and AR content')
.addTag('Geolocation', 'Location tracking, geofencing, smart navigation')
.addTag('Channel Management', 'Management of external distribution channels (OTAs)')
.addTag('Listings Management', 'Management of properties and tourism resources (hotels, vehicles, etc.)')
.addTag('Vehicle Management', 'Management and availability of rental vehicles')
.addTag('Flight Management', 'Flight search and booking operations')
.addTag('Availability Management', 'Generic availability management for all resources')
.addTag('Reviews', 'Advanced user reviews with multimedia and sentiment analysis')
.addTag('AI Generator', 'Generative AI content creation')
.addTag('Personalization', 'User experience personalization')
.addTag('Sustainability', 'Sustainable tourism tracking and eco-certifications')
.addTag('Social Commerce', 'Influencer marketing and UGC management')
.addTag('IoT Tourism', 'IoT device integration and smart tourism data')
.addTag('Finance', 'Commission rates, admin transactions, and settlements')
.addServer('https://karibeo.lesoluciones.net:8443', 'Production HTTPS')
.addServer('http://localhost:3000', 'Local development')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document, {
customSiteTitle: 'Karibeo API Documentation',
customfavIcon: '/favicon.ico',
customCssUrl: '/swagger-ui.css',
swaggerOptions: {
persistAuthorization: true,
displayRequestDuration: true,
},
});
// Siempre puerto 3000 HTTP - nginx maneja SSL
const port = process.env.PORT || 3000;
await app.listen(port);
console.log(`Karibeo API is running on: http://localhost:${port}`);
console.log(`API Documentation: http://localhost:${port}/api/docs`);
console.log(`External access: https://karibeo.lesoluciones.net:8443`);
}
bootstrap();

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,110 @@
import { Controller, Post, Body, HttpCode, HttpStatus, UseGuards, Get, Request } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiBody } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
import { AuthResponseDto } from './dto/auth-response.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { User } from '../../entities/user.entity';
@ApiTags('Authentication')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('register')
@HttpCode(HttpStatus.CREATED)
@ApiOperation({
summary: 'Register a new user',
description: 'Creates a new user account with tourist role by default'
})
@ApiBody({ type: RegisterDto })
@ApiResponse({
status: 201,
description: 'User successfully registered',
type: AuthResponseDto
})
@ApiResponse({
status: 409,
description: 'User with this email already exists'
})
@ApiResponse({
status: 400,
description: 'Invalid input data'
})
async register(@Body() registerDto: RegisterDto): Promise<AuthResponseDto> {
return this.authService.register(registerDto);
}
@Post('login')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'User login',
description: 'Authenticates user and returns JWT tokens'
})
@ApiBody({ type: LoginDto })
@ApiResponse({
status: 200,
description: 'User successfully authenticated',
type: AuthResponseDto
})
@ApiResponse({
status: 401,
description: 'Invalid credentials or account locked'
})
async login(@Body() loginDto: LoginDto): Promise<AuthResponseDto> {
return this.authService.login(loginDto);
}
@Post('refresh')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Refresh access token',
description: 'Refresh JWT access token using refresh token'
})
@ApiBody({
schema: {
type: 'object',
properties: {
refreshToken: {
type: 'string',
description: 'Valid refresh token',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
}
},
required: ['refreshToken']
}
})
@ApiResponse({
status: 200,
description: 'Tokens refreshed successfully',
type: AuthResponseDto
})
@ApiResponse({
status: 401,
description: 'Invalid or expired refresh token'
})
async refresh(@Body() body: { refreshToken: string }): Promise<AuthResponseDto> {
return this.authService.refresh(body.refreshToken);
}
@Get('profile')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT-auth')
@ApiOperation({
summary: 'Get current user profile',
description: 'Returns the profile of the authenticated user'
})
@ApiResponse({
status: 200,
description: 'User profile retrieved successfully',
type: User
})
@ApiResponse({
status: 401,
description: 'Unauthorized - Invalid or missing token'
})
async getProfile(@Request() req): Promise<User> {
return req.user;
}
}

30
src/modules/auth/auth.module.ts Executable file
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 {}

173
src/modules/auth/auth.service.ts Executable file
View File

@@ -0,0 +1,173 @@
import { Injectable, UnauthorizedException, ConflictException, InternalServerErrorException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { User } from '../../entities/user.entity';
import { Role } from '../../entities/role.entity';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
import { AuthResponseDto } from './dto/auth-response.dto';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@InjectRepository(Role)
private readonly roleRepository: Repository<Role>,
private readonly jwtService: JwtService,
) {}
async register(registerDto: RegisterDto): Promise<AuthResponseDto> {
const { email, password, ...userData } = registerDto;
// Check if user already exists
const existingUser = await this.userRepository.findOne({ where: { email } });
if (existingUser) {
throw new ConflictException('User with this email already exists');
}
// Hash password
const saltRounds = 12;
const passwordHash = await bcrypt.hash(password, saltRounds);
// Get default tourist role
const defaultRole = await this.roleRepository.findOne({ where: { name: 'tourist' } });
// Create user
const user = this.userRepository.create({
email,
passwordHash,
roleId: defaultRole?.id || 2, // Default to tourist role
...userData,
});
const savedUser = await this.userRepository.save(user);
// Generate tokens
const { accessToken, refreshToken } = await this.generateTokens(savedUser);
// Load user with relations for response
const userWithRelations = await this.userRepository.findOne({
where: { id: savedUser.id },
relations: ['country', 'role', 'preferredLanguageEntity'],
});
if (!userWithRelations) {
throw new InternalServerErrorException('Failed to retrieve user after registration');
}
return {
accessToken,
refreshToken,
user: userWithRelations,
expiresIn: '24h',
};
}
async login(loginDto: LoginDto): Promise<AuthResponseDto> {
const { email, password } = loginDto;
// Find user with relations
const user = await this.userRepository.findOne({
where: { email },
relations: ['country', 'role', 'preferredLanguageEntity'],
});
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
// Check if account is locked
if (user.lockedUntil && new Date() < user.lockedUntil) {
throw new UnauthorizedException('Account is temporarily locked');
}
// Verify password
const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
if (!isPasswordValid) {
// Increment failed login attempts
await this.handleFailedLogin(user);
throw new UnauthorizedException('Invalid credentials');
}
// Reset failed login attempts on successful login
await this.userRepository.update(user.id, {
failedLoginAttempts: 0,
lockedUntil: undefined,
lastLogin: new Date(),
});
// Generate tokens
const { accessToken, refreshToken } = await this.generateTokens(user);
return {
accessToken,
refreshToken,
user,
expiresIn: '24h',
};
}
async validateUser(userId: string): Promise<User | null> {
return this.userRepository.findOne({
where: { id: userId, isActive: true },
relations: ['country', 'role', 'preferredLanguageEntity'],
});
}
async refresh(refreshToken: string): Promise<AuthResponseDto> {
try {
// Verificar el refresh token
const payload = this.jwtService.verify(refreshToken);
// Buscar el usuario
const user = await this.userRepository.findOne({
where: { id: payload.sub, isActive: true },
relations: ['country', 'role', 'preferredLanguageEntity'],
});
if (!user) {
throw new UnauthorizedException('User not found or inactive');
}
// Generar nuevos tokens
const { accessToken, refreshToken: newRefreshToken } = await this.generateTokens(user);
return {
accessToken,
refreshToken: newRefreshToken,
user,
expiresIn: '24h',
};
} catch (error) {
throw new UnauthorizedException('Invalid or expired refresh token');
}
}
private async generateTokens(user: User): Promise<{ accessToken: string; refreshToken: string }> {
const payload = {
sub: user.id,
email: user.email,
role: user.role?.name || 'tourist',
};
const accessToken = this.jwtService.sign(payload);
const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });
return { accessToken, refreshToken };
}
private async handleFailedLogin(user: User): Promise<void> {
const failedAttempts = user.failedLoginAttempts + 1;
const updateData: any = { failedLoginAttempts: failedAttempts };
// Lock account after 5 failed attempts for 15 minutes
if (failedAttempts >= 5) {
updateData.lockedUntil = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes
}
await this.userRepository.update(user.id, updateData);
}
}

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,81 @@
import {
Controller, Get, Post, Body, Query, UseGuards, Param
} from '@nestjs/common';
import {
ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam
} from '@nestjs/swagger';
import { AvailabilityManagementService } from './availability-management.service';
import { GetAvailabilityDto } from './dto/get-availability.dto';
import { UpdateAvailabilityDto } from './dto/update-availability.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RolesGuard } from '../../common/guards/roles.guard';
import { Roles } from '../../common/decorators/roles.decorator';
import { Availability } from '../../entities/availability.entity';
@ApiTags('Availability Management')
@ApiBearerAuth('JWT-auth')
@UseGuards(JwtAuthGuard)
@Controller('api/v1/availability')
export class AvailabilityManagementController {
constructor(private readonly availabilityManagementService: AvailabilityManagementService) {}
@Get()
@ApiOperation({ summary: 'Get availability for a specific resource (hotel, restaurant, vehicle, room, table) in a date range' })
@ApiQuery({ name: 'resourceId', type: String, example: 'uuid-hotel-123', description: 'ID of the resource' })
@ApiQuery({ name: 'resourceType', type: String, example: 'hotel', description: 'Type of the resource (hotel, restaurant, vehicle, room, table)' })
@ApiQuery({ name: 'startDate', type: String, format: 'date', example: '2025-10-01' })
@ApiQuery({ name: 'endDate', type: String, format: 'date', example: '2025-10-05' })
@ApiQuery({ name: 'minQuantity', type: Number, required: false, example: 1, description: 'Minimum quantity desired' })
@ApiResponse({ status: 200, type: [Availability] })
getAvailability(@Query() queryDto: GetAvailabilityDto) {
return this.availabilityManagementService.getAvailability(queryDto);
}
@Post()
@UseGuards(RolesGuard)
@Roles('admin', 'establishment', 'owner') // Admins, establishment managers, or owners can update availability
@ApiOperation({ summary: 'Update or create availability for a specific resource on a given date' })
@ApiResponse({ status: 201, description: 'Availability updated/created successfully', type: Availability })
updateAvailability(@Body() updateDto: UpdateAvailabilityDto) {
return this.availabilityManagementService.updateAvailability(updateDto);
}
// Specific endpoints for convenience (delegating to generic availability service)
@Get('hotel/:id')
@ApiOperation({ summary: 'Get hotel room availability for a specific hotel in a date range' })
@ApiParam({ name: 'id', type: String, description: 'Hotel ID (Establishment ID)' })
@ApiQuery({ name: 'startDate', type: String, format: 'date', example: '2025-10-01' })
@ApiQuery({ name: 'endDate', type: String, format: 'date', example: '2025-10-05' })
@ApiQuery({ name: 'minQuantity', type: Number, required: false, example: 1, description: 'Minimum number of rooms desired' })
@ApiResponse({ status: 200, type: [Availability] })
getHotelAvailability(
@Param('id') id: string,
@Query('startDate') startDate: string,
@Query('endDate') endDate: string,
@Query('minQuantity') minQuantity?: number,
) {
return this.availabilityManagementService.getAvailability({
resourceId: id,
resourceType: 'hotel', // Or 'hotel-room' if granular
startDate,
endDate,
minQuantity,
});
}
@Get('restaurant/:id')
@ApiOperation({ summary: 'Get restaurant table availability for a specific restaurant on a date' })
@ApiParam({ name: 'id', type: String, description: 'Restaurant ID (Establishment ID)' })
@ApiQuery({ name: 'date', type: String, format: 'date', example: '2025-10-01' })
@ApiQuery({ name: 'minQuantity', type: Number, required: false, example: 1, description: 'Minimum number of tables/seats desired' })
@ApiResponse({ status: 200, type: [Availability] })
getRestaurantAvailability(
@Param('id') id: string,
@Query('date') date: string,
@Query('minQuantity') minQuantity?: number,
) {
// For restaurants, availability might be per day/slot, so startDate and endDate are the same
return this.availabilityManagementService.getAvailability({
resourceId: id,
resourceType: 'restaurant', // Or 'restaurant-table' if granular
startDate: date,
endDate: date,
minQuantity,
});
}
}

View File

@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AvailabilityManagementService } from './availability-management.service';
import { AvailabilityManagementController } from './availability-management.controller';
import { Availability } from '../../entities/availability.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
Availability,
]),
],
controllers: [AvailabilityManagementController],
providers: [AvailabilityManagementService],
exports: [AvailabilityManagementService], // Export for other modules to use (e.g., booking)
})
export class AvailabilityManagementModule {}

View File

@@ -0,0 +1,158 @@
import { Injectable, NotFoundException, BadRequestException, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Availability } from '../../entities/availability.entity';
import { GetAvailabilityDto } from './dto/get-availability.dto';
import { UpdateAvailabilityDto } from './dto/update-availability.dto';
@Injectable()
export class AvailabilityManagementService {
private readonly logger = new Logger(AvailabilityManagementService.name);
constructor(
@InjectRepository(Availability)
private readonly availabilityRepository: Repository<Availability>,
) {}
/**
* Retrieves availability for a given resource within a date range.
*/
async getAvailability(queryDto: GetAvailabilityDto): Promise<Availability[]> {
const { resourceId, resourceType, startDate, endDate, minQuantity } = queryDto;
const start = new Date(startDate);
const end = new Date(endDate);
if (start > end) {
throw new BadRequestException('Start date cannot be after end date.');
}
const query = this.availabilityRepository.createQueryBuilder('availability')
.where('availability.resourceId = :resourceId', { resourceId })
.andWhere('availability.resourceType = :resourceType', { resourceType })
.andWhere('availability.date >= :startDate', { startDate: queryDto.startDate })
.andWhere('availability.date <= :endDate', { endDate: queryDto.endDate })
.andWhere('availability.isAvailable = :isAvailable', { isAvailable: true });
if (minQuantity !== undefined && minQuantity > 0) {
query.andWhere('availability.availableQuantity >= :minQuantity', { minQuantity });
} else {
// Ensure at least 1 unit is available if no minQuantity is specified
query.andWhere('availability.availableQuantity >= :defaultMinQuantity', { defaultMinQuantity: 1 });
}
return query.orderBy('availability.date', 'ASC').getMany();
}
/**
* Updates or creates availability records for a specific date.
* This would typically be used by a channel manager or an establishment owner.
*/
async updateAvailability(updateDto: UpdateAvailabilityDto): Promise<Availability> {
const { resourceId, resourceType, date, availableQuantity, totalQuantity, basePrice, priceModifiers, minStay, restrictions, isAvailable, status } = updateDto;
let availability = await this.availabilityRepository.findOne({
where: {
resourceId,
resourceType,
date: new Date(date),
},
});
if (availability) {
// Update existing record
availability.availableQuantity = availableQuantity;
availability.totalQuantity = totalQuantity;
availability.basePrice = basePrice ?? availability.basePrice;
availability.finalPrice = this.calculateFinalPrice(basePrice ?? availability.basePrice, priceModifiers ?? {});
availability.priceModifiers = priceModifiers ?? availability.priceModifiers;
availability.minStay = minStay ?? availability.minStay;
availability.restrictions = restrictions ?? availability.restrictions;
availability.isAvailable = isAvailable ?? (availableQuantity > 0);
availability.status = status ?? (availability.isAvailable ? 'open' : 'sold-out');
availability.lastSynced = new Date();
} else {
// Create new record
availability = this.availabilityRepository.create({
resourceId,
resourceType,
date: new Date(date),
availableQuantity,
totalQuantity,
bookedQuantity: 0,
blockedQuantity: 0,
basePrice: basePrice ?? 0,
priceModifiers: priceModifiers ?? {},
finalPrice: this.calculateFinalPrice(basePrice ?? 0, priceModifiers ?? {}),
minStay: minStay ?? 1,
restrictions: restrictions,
isAvailable: isAvailable ?? (availableQuantity > 0),
status: status ?? (availableQuantity > 0 ? 'open' : 'sold-out'),
lastSynced: new Date(),
});
}
return this.availabilityRepository.save(availability);
}
/**
* Calculates the final price based on base price and modifiers.
*/
private calculateFinalPrice(basePrice: number, modifiers: Record<string, any>): number {
let finalPrice = basePrice;
if (modifiers) {
for (const key in modifiers) {
if (Object.prototype.hasOwnProperty.call(modifiers, key)) {
// Example: Apply percentage modifiers
if (typeof modifiers[key] === 'number' && modifiers[key] > 0) {
finalPrice *= modifiers[key];
}
}
}
}
return parseFloat(finalPrice.toFixed(2)); // Round to 2 decimal places
}
// Utility to check and adjust availability when a booking occurs
async decrementAvailability(resourceId: string, resourceType: string, date: Date, quantity: number): Promise<void> {
const availability = await this.availabilityRepository.findOne({
where: { resourceId, resourceType, date },
});
if (!availability) {
throw new NotFoundException(`Availability for ${resourceType} ${resourceId} on ${date.toDateString()} not found.`);
}
if (availability.availableQuantity < quantity) {
throw new BadRequestException(`Not enough availability for ${resourceType} ${resourceId} on ${date.toDateString()}. Only ${availability.availableQuantity} left.`);
}
availability.availableQuantity -= quantity;
availability.bookedQuantity += quantity;
if (availability.availableQuantity === 0) {
availability.isAvailable = false;
availability.status = 'sold-out';
}
await this.availabilityRepository.save(availability);
}
// Utility to check and adjust availability when a booking is cancelled
async incrementAvailability(resourceId: string, resourceType: string, date: Date, quantity: number): Promise<void> {
const availability = await this.availabilityRepository.findOne({
where: { resourceId, resourceType, date },
});
if (!availability) {
this.logger.warn(`Availability for ${resourceType} ${resourceId} on ${date.toDateString()} not found during increment. Skipping.`);
return; // Or create a new record if this scenario is expected for initial unavailability
}
availability.availableQuantity += quantity;
availability.bookedQuantity -= quantity;
if (availability.availableQuantity > 0) {
availability.isAvailable = true;
availability.status = 'open';
}
await this.availabilityRepository.save(availability);
}
}

View File

@@ -0,0 +1,25 @@
import { IsString, IsNotEmpty, IsDateString, IsOptional, IsNumber, Min } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class GetAvailabilityDto {
@ApiProperty({ description: 'Resource ID (e.g., Hotel ID, Restaurant ID, Vehicle ID, Room ID, Table ID)', example: 'uuid-hotel-123' })
@IsString()
@IsNotEmpty()
resourceId: string;
@ApiProperty({ description: 'Resource type (hotel, restaurant, vehicle, room, table)', example: 'hotel' })
@IsString()
@IsNotEmpty()
resourceType: string;
@ApiProperty({ description: 'Start date for availability query (YYYY-MM-DD)', example: '2025-10-01' })
@IsDateString()
@IsNotEmpty()
startDate: string;
@ApiProperty({ description: 'End date for availability query (YYYY-MM-DD)', example: '2025-10-05' })
@IsDateString()
@IsNotEmpty()
endDate: string;
@ApiPropertyOptional({ description: 'Minimum quantity required', example: 1 })
@IsNumber()
@Min(0)
@IsOptional()
minQuantity?: number;
}

View File

@@ -0,0 +1,50 @@
import { IsString, IsNotEmpty, IsDateString, IsNumber, Min, IsOptional, IsObject, IsBoolean } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class UpdateAvailabilityDto {
@ApiProperty({ description: 'Resource ID (e.g., Hotel ID, Restaurant ID, Vehicle ID, Room ID, Table ID)', example: 'uuid-hotel-room-456' })
@IsString()
@IsNotEmpty()
resourceId: string;
@ApiProperty({ description: 'Resource type (hotel, restaurant, vehicle, room, table)', example: 'room' })
@IsString()
@IsNotEmpty()
resourceType: string;
@ApiProperty({ description: 'Date for availability update (YYYY-MM-DD)', example: '2025-10-02' })
@IsDateString()
@IsNotEmpty()
date: string;
@ApiProperty({ description: 'Available quantity for the specified date', example: 5 })
@IsNumber()
@Min(0)
availableQuantity: number;
@ApiProperty({ description: 'Total quantity of the resource (e.g., total rooms of this type)', example: 10 })
@IsNumber()
@Min(0)
totalQuantity: number;
@ApiPropertyOptional({ description: 'Base price for this date', example: 150.00 })
@IsNumber()
@Min(0)
@IsOptional()
basePrice?: number;
@ApiPropertyOptional({ description: 'Dynamic price adjustments (e.g., { demand: 1.2 })' })
@IsObject()
@IsOptional()
priceModifiers?: Record<string, any>;
@ApiPropertyOptional({ description: 'Minimum stay requirement for this date', example: 2 })
@IsNumber()
@Min(1)
@IsOptional()
minStay?: number;
@ApiPropertyOptional({ description: 'Special restrictions or notes for this date' })
@IsString()
@IsOptional()
restrictions?: string;
@ApiPropertyOptional({ description: 'Is available for booking', example: true })
@IsBoolean()
@IsOptional()
isAvailable?: boolean;
@ApiPropertyOptional({ description: 'Availability status (open, closed, limited, sold-out)', example: 'open' })
@IsString()
@IsOptional()
status?: string;
}

View File

@@ -0,0 +1,99 @@
import {
Controller, Get, Post, Body, Patch, Param, Delete, UseGuards
} from '@nestjs/common';
import {
ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiParam
} from '@nestjs/swagger';
import { ChannelManagementService } from './channel-management.service';
import { CreateChannelDto } from './dto/create-channel.dto'; // Mantenemos solo CreateChannelDto aquí
import { ConnectChannelDto } from './dto/connect-channel.dto'; // CORREGIDO: Importar ConnectChannelDto de su propio archivo
import { UpdateChannelDto } from './dto/update-channel.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RolesGuard } from '../../common/guards/roles.guard';
import { Roles } from '../../common/decorators/roles.decorator';
import { Channel } from '../../entities/channel.entity';
@ApiTags('Channel Management')
@ApiBearerAuth('JWT-auth')
@UseGuards(JwtAuthGuard)
@Controller('api/v1/channels')
export class ChannelManagementController {
constructor(private readonly channelManagementService: ChannelManagementService) {}
@Post()
@UseGuards(RolesGuard)
@Roles('admin', 'establishment')
@ApiOperation({ summary: 'Create a new channel' })
@ApiResponse({ status: 201, description: 'Channel created successfully', type: Channel })
create(@Body() createChannelDto: CreateChannelDto) {
return this.channelManagementService.createChannel(createChannelDto);
}
@Get()
@UseGuards(RolesGuard)
@Roles('admin', 'establishment')
@ApiOperation({ summary: 'Get a list of all connected distribution channels' })
@ApiResponse({ status: 200, type: [Channel] })
findAll() {
return this.channelManagementService.findAllChannels();
}
@Get(':id')
@UseGuards(RolesGuard)
@Roles('admin', 'establishment')
@ApiOperation({ summary: 'Get a specific channel by ID' })
@ApiParam({ name: 'id', type: 'string', description: 'Channel ID' })
@ApiResponse({ status: 200, type: Channel })
findOne(@Param('id') id: string) {
return this.channelManagementService.findChannelById(id);
}
@Patch(':id')
@UseGuards(RolesGuard)
@Roles('admin', 'establishment')
@ApiOperation({ summary: 'Update an existing channel by ID' })
@ApiParam({ name: 'id', type: 'string', description: 'Channel ID' })
@ApiResponse({ status: 200, description: 'Channel updated successfully', type: Channel })
update(@Param('id') id: string, @Body() updateChannelDto: UpdateChannelDto) {
return this.channelManagementService.updateChannel(id, updateChannelDto);
}
@Delete(':id')
@UseGuards(RolesGuard)
@Roles('admin', 'establishment')
@ApiOperation({ summary: 'Delete a channel by ID' })
@ApiParam({ name: 'id', type: 'string', description: 'Channel ID' })
@ApiResponse({ status: 204, description: 'Channel deleted successfully' })
remove(@Param('id') id: string) {
return this.channelManagementService.deleteChannel(id);
}
@Post('connect')
@UseGuards(RolesGuard)
@Roles('admin', 'establishment')
@ApiOperation({ summary: 'Connect a new distribution channel' })
@ApiResponse({ status: 201, description: 'Channel connected and initiated sync', type: Channel })
connectChannel(@Body() connectChannelDto: ConnectChannelDto) {
return this.channelManagementService.connectChannel(connectChannelDto);
}
@Delete(':id/disconnect')
@UseGuards(RolesGuard)
@Roles('admin', 'establishment')
@ApiOperation({ summary: 'Disconnect a specific distribution channel by its ID' })
@ApiParam({ name: 'id', type: 'string', description: 'Channel ID' })
@ApiResponse({ status: 200, description: 'Channel disconnected successfully', type: Channel })
disconnectChannel(@Param('id') id: string) {
return this.channelManagementService.disconnectChannel(id);
}
@Post(':id/sync')
@UseGuards(RolesGuard)
@Roles('admin', 'establishment')
@ApiOperation({ summary: 'Initiate a manual synchronization for a specific channel' })
@ApiParam({ name: 'id', type: 'string', description: 'Channel ID' })
@ApiResponse({ status: 200, description: 'Channel synchronization initiated', type: Channel })
syncChannel(@Param('id') id: string) {
return this.channelManagementService.syncChannel(id);
}
}

View File

@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ChannelManagementService } from './channel-management.service';
import { ChannelManagementController } from './channel-management.controller';
import { Channel } from '../../entities/channel.entity';
import { NotificationsModule } from '../notifications/notifications.module';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [
TypeOrmModule.forFeature([Channel]),
NotificationsModule,
ScheduleModule,
],
controllers: [ChannelManagementController],
providers: [ChannelManagementService],
exports: [ChannelManagementService],
})
export class ChannelManagementModule {}

Some files were not shown because too many files have changed in this diff Show More