From 6e0ad420abac6c15d0995e54c92e1583dcf46244 Mon Sep 17 00:00:00 2001 From: ellecio2 Date: Fri, 10 Oct 2025 21:47:56 -0400 Subject: [PATCH] Initial commit --- .gitignore | 56 + .prettierrc | 4 + ecosystem.config.js | 99 + eslint.config.mjs | 34 + nest-cli.json | 8 + package-lock.json | 15124 ++++++++++++++++ package.json | 104 + src/app.controller.spec.ts | 22 + src/app.controller.ts | 12 + src/app.module.ts | 232 + src/app.service.ts | 8 + src/common/decorators/roles.decorator.ts | 4 + src/common/guards/jwt-auth.guard.ts | 5 + src/common/guards/roles.guard.ts | 32 + src/config/app.config.ts | 16 + src/config/database.config.ts | 20 + src/config/integrations/aws.config.ts | 11 + .../integrations/communication.config.ts | 21 + src/config/integrations/stripe.config.ts | 9 + src/config/jwt.config.ts | 9 + src/entities/admin-transaction.entity.ts | 52 + src/entities/advanced-review.entity.ts | 112 + src/entities/ai-generated-content.entity.ts | 52 + src/entities/ai-guide-interaction.entity.ts | 57 + src/entities/ar-content.entity.ts | 68 + src/entities/availability.entity.ts | 68 + src/entities/base.entity.ts | 29 + src/entities/channel.entity.ts | 54 + src/entities/commission-rate.entity.ts | 38 + src/entities/country.entity.ts | 37 + src/entities/creator-campaign.entity.ts | 117 + src/entities/destination.entity.ts | 47 + src/entities/eco-establishment.entity.ts | 110 + src/entities/emergency-alert.entity.ts | 36 + src/entities/establishment.entity.ts | 80 + src/entities/flight.entity.ts | 98 + src/entities/geofence.entity.ts | 46 + src/entities/hotel-checkin.entity.ts | 69 + src/entities/hotel-room.entity.ts | 44 + src/entities/hotel-service.entity.ts | 69 + src/entities/incident.entity.ts | 65 + src/entities/influencer-profile.entity.ts | 121 + src/entities/iot-device.entity.ts | 92 + src/entities/itinerary.entity.ts | 56 + src/entities/language.entity.ts | 37 + src/entities/listing.entity.ts | 109 + src/entities/location-tracking.entity.ts | 44 + src/entities/menu-item.entity.ts | 72 + src/entities/notification.entity.ts | 48 + src/entities/order-item.entity.ts | 45 + src/entities/order.entity.ts | 94 + src/entities/place-of-interest.entity.ts | 80 + src/entities/product.entity.ts | 56 + src/entities/reservation.entity.ts | 61 + src/entities/review-helpfulness.entity.ts | 30 + src/entities/review.entity.ts | 49 + src/entities/role.entity.ts | 33 + src/entities/security-officer.entity.ts | 40 + src/entities/settlement.entity.ts | 55 + src/entities/smart-tourism-data.entity.ts | 64 + .../sustainability-tracking.entity.ts | 70 + src/entities/table.entity.ts | 40 + src/entities/taxi-driver.entity.ts | 64 + src/entities/tour-guide.entity.ts | 64 + src/entities/transaction.entity.ts | 73 + src/entities/ugc-content.entity.ts | 117 + src/entities/user-personalization.entity.ts | 82 + src/entities/user-preferences.entity.ts | 53 + src/entities/user.entity.ts | 94 + src/entities/vehicle.entity.ts | 96 + src/entities/wearable-device.entity.ts | 99 + src/main.ts | 112 + .../ai-generator/ai-generator.controller.ts | 93 + .../ai-generator/ai-generator.module.ts | 21 + .../ai-generator/ai-generator.service.ts | 596 + .../ai-generator/dto/generate-content.dto.ts | 69 + .../ai-generator/dto/improve-content.dto.ts | 32 + src/modules/ai-guide/ai-guide.controller.ts | 162 + src/modules/ai-guide/ai-guide.module.ts | 21 + src/modules/ai-guide/ai-guide.service.ts | 372 + src/modules/ai-guide/dto/ai-query.dto.ts | 55 + .../ai-guide/dto/ar-content-query.dto.ts | 27 + src/modules/analytics/analytics.controller.ts | 46 + src/modules/analytics/analytics.module.ts | 13 + src/modules/analytics/analytics.service.ts | 126 + .../analytics/dto/create-review.dto.ts | 36 + src/modules/auth/auth.controller.ts | 110 + src/modules/auth/auth.module.ts | 30 + src/modules/auth/auth.service.ts | 173 + src/modules/auth/dto/auth-response.dto.ts | 16 + src/modules/auth/dto/login.dto.ts | 12 + src/modules/auth/dto/register.dto.ts | 41 + src/modules/auth/strategies/jwt.strategy.ts | 35 + .../availability-management.controller.ts | 81 + .../availability-management.module.ts | 16 + .../availability-management.service.ts | 158 + .../dto/get-availability.dto.ts | 25 + .../dto/update-availability.dto.ts | 50 + .../channel-management.controller.ts | 99 + .../channel-management.module.ts | 19 + .../channel-management.service.ts | 171 + .../dto/connect-channel.dto.ts | 29 + .../dto/create-channel.dto.ts | 40 + .../dto/update-channel.dto.ts | 3 + src/modules/commerce/commerce.controller.ts | 166 + src/modules/commerce/commerce.module.ts | 25 + src/modules/commerce/commerce.service.ts | 227 + .../commerce/dto/create-establishment.dto.ts | 74 + .../commerce/dto/create-reservation.dto.ts | 51 + .../commerce/dto/update-establishment.dto.ts | 4 + .../commerce/dto/update-reservation.dto.ts | 4 + .../communication/communication.module.ts | 11 + src/modules/communication/email.service.ts | 225 + .../communication/whatsapp.controller.ts | 44 + src/modules/communication/whatsapp.service.ts | 232 + .../commissions.controller.spec.ts | 18 + .../commissions/commissions.controller.ts | 4 + .../dashboard/dashboard.controller.spec.ts | 18 + .../finance/dashboard/dashboard.controller.ts | 4 + .../finance/finance.controller.spec.ts | 18 + src/modules/finance/finance.controller.ts | 234 + src/modules/finance/finance.module.ts | 23 + src/modules/finance/finance.service.spec.ts | 18 + src/modules/finance/finance.service.ts | 486 + .../reports/reports.controller.spec.ts | 18 + .../finance/reports/reports.controller.ts | 4 + .../flight-management/dto/book-flight.dto.ts | 64 + .../dto/search-flight.dto.ts | 47 + .../flight-management.controller.ts | 44 + .../flight-management.module.ts | 25 + .../flight-management.service.ts | 141 + src/modules/geolocation/dto/geofence.dto.ts | 52 + .../geolocation/dto/location-update.dto.ts | 32 + .../geolocation/geolocation.controller.ts | 211 + src/modules/geolocation/geolocation.module.ts | 25 + .../geolocation/geolocation.service.ts | 400 + .../hotel/dto/create-hotel-checkin.dto.ts | 38 + .../hotel/dto/room-service-request.dto.ts | 61 + src/modules/hotel/hotel.controller.ts | 220 + src/modules/hotel/hotel.module.ts | 25 + src/modules/hotel/hotel.service.ts | 389 + .../iot-tourism/dto/device-reading.dto.ts | 52 + .../iot-tourism/dto/wearable-sync.dto.ts | 48 + .../iot-tourism/iot-tourism.controller.ts | 111 + src/modules/iot-tourism/iot-tourism.module.ts | 23 + .../iot-tourism/iot-tourism.service.ts | 340 + .../listings/dto/create-listing.dto.ts | 98 + .../listings/dto/get-listings-filter.dto.ts | 36 + .../listings/dto/update-listing.dto.ts | 3 + src/modules/listings/listings.controller.ts | 67 + src/modules/listings/listings.module.ts | 16 + src/modules/listings/listings.service.ts | 140 + .../dto/create-notification.dto.ts | 58 + .../notifications/notifications.controller.ts | 79 + .../notifications/notifications.module.ts | 13 + .../notifications/notifications.service.ts | 198 + .../payments/dto/create-payment.dto.ts | 29 + .../payments/dto/process-payment.dto.ts | 33 + src/modules/payments/payments.controller.ts | 70 + src/modules/payments/payments.module.ts | 19 + src/modules/payments/payments.service.ts | 69 + .../dto/recommendation-request.dto.ts | 57 + .../dto/update-preferences.dto.ts | 75 + .../personalization.controller.ts | 301 + .../personalization/personalization.module.ts | 31 + .../personalization.service.ts | 754 + .../restaurant/dto/create-menu-item.dto.ts | 69 + .../restaurant/dto/create-order.dto.ts | 66 + .../restaurant/dto/create-table.dto.ts | 22 + .../restaurant/dto/update-order-status.dto.ts | 23 + .../restaurant/restaurant.controller.ts | 175 + src/modules/restaurant/restaurant.module.ts | 23 + src/modules/restaurant/restaurant.service.ts | 289 + .../reviews/dto/create-advanced-review.dto.ts | 47 + src/modules/reviews/dto/create-review.dto.ts | 30 + .../reviews/dto/establishment-response.dto.ts | 9 + .../reviews/dto/mark-helpfulness.dto.ts | 9 + .../reviews/dto/review-helpfulness.dto.ts | 9 + .../reviews/dto/review-response.dto.ts | 9 + src/modules/reviews/dto/update-review.dto.ts | 4 + src/modules/reviews/reviews.controller.ts | 293 + src/modules/reviews/reviews.module.ts | 21 + src/modules/reviews/reviews.service.ts | 127 + .../dto/create-emergency-alert.dto.ts | 35 + .../security/dto/create-incident.dto.ts | 47 + .../security/dto/update-incident.dto.ts | 29 + src/modules/security/security.controller.ts | 165 + src/modules/security/security.module.ts | 21 + src/modules/security/security.service.ts | 251 + .../dto/create-campaign.dto.ts | 86 + .../dto/create-influencer-profile.dto.ts | 103 + .../social-commerce.controller.ts | 518 + .../social-commerce/social-commerce.module.ts | 23 + .../social-commerce.service.ts | 871 + .../sustainability/dto/carbon-offset.dto.ts | 36 + .../sustainability/dto/track-activity.dto.ts | 71 + .../sustainability.controller.ts | 395 + .../sustainability/sustainability.module.ts | 23 + .../sustainability/sustainability.service.ts | 732 + .../tourism/dto/create-destination.dto.ts | 36 + src/modules/tourism/dto/create-place.dto.ts | 74 + .../tourism/dto/create-tour-guide.dto.ts | 52 + .../tourism/dto/update-destination.dto.ts | 4 + src/modules/tourism/dto/update-place.dto.ts | 4 + .../tourism/dto/update-tour-guide.dto.ts | 4 + src/modules/tourism/tourism.controller.ts | 221 + src/modules/tourism/tourism.module.ts | 25 + src/modules/tourism/tourism.service.ts | 246 + src/modules/upload/upload.controller.ts | 180 + src/modules/upload/upload.module.ts | 10 + src/modules/upload/upload.service.ts | 155 + src/modules/users/dto/create-user.dto.ts | 56 + src/modules/users/dto/update-user.dto.ts | 6 + src/modules/users/users.controller.ts | 68 + src/modules/users/users.module.ts | 13 + src/modules/users/users.service.ts | 153 + .../dto/create-vehicle.dto.ts | 88 + .../dto/update-vehicle.dto.ts | 3 + .../dto/vehicle-availability-query.dto.ts | 29 + .../vehicle-management.controller.ts | 89 + .../vehicle-management.module.ts | 22 + .../vehicle-management.service.ts | 164 + test.txt | 0 test/app.e2e-spec.ts | 25 + test/jest-e2e.json | 9 + tsconfig.build.json | 4 + tsconfig.json | 21 + 227 files changed, 34899 insertions(+) create mode 100755 .gitignore create mode 100755 .prettierrc create mode 100644 ecosystem.config.js create mode 100755 eslint.config.mjs create mode 100755 nest-cli.json create mode 100755 package-lock.json create mode 100755 package.json create mode 100755 src/app.controller.spec.ts create mode 100755 src/app.controller.ts create mode 100755 src/app.module.ts create mode 100755 src/app.service.ts create mode 100755 src/common/decorators/roles.decorator.ts create mode 100755 src/common/guards/jwt-auth.guard.ts create mode 100755 src/common/guards/roles.guard.ts create mode 100755 src/config/app.config.ts create mode 100755 src/config/database.config.ts create mode 100755 src/config/integrations/aws.config.ts create mode 100755 src/config/integrations/communication.config.ts create mode 100755 src/config/integrations/stripe.config.ts create mode 100755 src/config/jwt.config.ts create mode 100644 src/entities/admin-transaction.entity.ts create mode 100644 src/entities/advanced-review.entity.ts create mode 100644 src/entities/ai-generated-content.entity.ts create mode 100644 src/entities/ai-guide-interaction.entity.ts create mode 100644 src/entities/ar-content.entity.ts create mode 100644 src/entities/availability.entity.ts create mode 100755 src/entities/base.entity.ts create mode 100644 src/entities/channel.entity.ts create mode 100644 src/entities/commission-rate.entity.ts create mode 100755 src/entities/country.entity.ts create mode 100644 src/entities/creator-campaign.entity.ts create mode 100755 src/entities/destination.entity.ts create mode 100644 src/entities/eco-establishment.entity.ts create mode 100755 src/entities/emergency-alert.entity.ts create mode 100755 src/entities/establishment.entity.ts create mode 100644 src/entities/flight.entity.ts create mode 100644 src/entities/geofence.entity.ts create mode 100755 src/entities/hotel-checkin.entity.ts create mode 100755 src/entities/hotel-room.entity.ts create mode 100755 src/entities/hotel-service.entity.ts create mode 100755 src/entities/incident.entity.ts create mode 100644 src/entities/influencer-profile.entity.ts create mode 100644 src/entities/iot-device.entity.ts create mode 100755 src/entities/itinerary.entity.ts create mode 100755 src/entities/language.entity.ts create mode 100644 src/entities/listing.entity.ts create mode 100644 src/entities/location-tracking.entity.ts create mode 100755 src/entities/menu-item.entity.ts create mode 100755 src/entities/notification.entity.ts create mode 100755 src/entities/order-item.entity.ts create mode 100755 src/entities/order.entity.ts create mode 100755 src/entities/place-of-interest.entity.ts create mode 100755 src/entities/product.entity.ts create mode 100755 src/entities/reservation.entity.ts create mode 100644 src/entities/review-helpfulness.entity.ts create mode 100755 src/entities/review.entity.ts create mode 100755 src/entities/role.entity.ts create mode 100755 src/entities/security-officer.entity.ts create mode 100644 src/entities/settlement.entity.ts create mode 100644 src/entities/smart-tourism-data.entity.ts create mode 100644 src/entities/sustainability-tracking.entity.ts create mode 100755 src/entities/table.entity.ts create mode 100755 src/entities/taxi-driver.entity.ts create mode 100755 src/entities/tour-guide.entity.ts create mode 100755 src/entities/transaction.entity.ts create mode 100644 src/entities/ugc-content.entity.ts create mode 100644 src/entities/user-personalization.entity.ts create mode 100755 src/entities/user-preferences.entity.ts create mode 100755 src/entities/user.entity.ts create mode 100644 src/entities/vehicle.entity.ts create mode 100644 src/entities/wearable-device.entity.ts create mode 100755 src/main.ts create mode 100644 src/modules/ai-generator/ai-generator.controller.ts create mode 100644 src/modules/ai-generator/ai-generator.module.ts create mode 100644 src/modules/ai-generator/ai-generator.service.ts create mode 100644 src/modules/ai-generator/dto/generate-content.dto.ts create mode 100644 src/modules/ai-generator/dto/improve-content.dto.ts create mode 100644 src/modules/ai-guide/ai-guide.controller.ts create mode 100644 src/modules/ai-guide/ai-guide.module.ts create mode 100644 src/modules/ai-guide/ai-guide.service.ts create mode 100644 src/modules/ai-guide/dto/ai-query.dto.ts create mode 100644 src/modules/ai-guide/dto/ar-content-query.dto.ts create mode 100755 src/modules/analytics/analytics.controller.ts create mode 100755 src/modules/analytics/analytics.module.ts create mode 100755 src/modules/analytics/analytics.service.ts create mode 100755 src/modules/analytics/dto/create-review.dto.ts create mode 100755 src/modules/auth/auth.controller.ts create mode 100755 src/modules/auth/auth.module.ts create mode 100755 src/modules/auth/auth.service.ts create mode 100755 src/modules/auth/dto/auth-response.dto.ts create mode 100755 src/modules/auth/dto/login.dto.ts create mode 100755 src/modules/auth/dto/register.dto.ts create mode 100755 src/modules/auth/strategies/jwt.strategy.ts create mode 100644 src/modules/availability-management/availability-management.controller.ts create mode 100644 src/modules/availability-management/availability-management.module.ts create mode 100644 src/modules/availability-management/availability-management.service.ts create mode 100644 src/modules/availability-management/dto/get-availability.dto.ts create mode 100644 src/modules/availability-management/dto/update-availability.dto.ts create mode 100644 src/modules/channel-management/channel-management.controller.ts create mode 100644 src/modules/channel-management/channel-management.module.ts create mode 100644 src/modules/channel-management/channel-management.service.ts create mode 100644 src/modules/channel-management/dto/connect-channel.dto.ts create mode 100644 src/modules/channel-management/dto/create-channel.dto.ts create mode 100644 src/modules/channel-management/dto/update-channel.dto.ts create mode 100755 src/modules/commerce/commerce.controller.ts create mode 100755 src/modules/commerce/commerce.module.ts create mode 100755 src/modules/commerce/commerce.service.ts create mode 100755 src/modules/commerce/dto/create-establishment.dto.ts create mode 100755 src/modules/commerce/dto/create-reservation.dto.ts create mode 100755 src/modules/commerce/dto/update-establishment.dto.ts create mode 100755 src/modules/commerce/dto/update-reservation.dto.ts create mode 100755 src/modules/communication/communication.module.ts create mode 100755 src/modules/communication/email.service.ts create mode 100755 src/modules/communication/whatsapp.controller.ts create mode 100755 src/modules/communication/whatsapp.service.ts create mode 100644 src/modules/finance/commissions/commissions.controller.spec.ts create mode 100644 src/modules/finance/commissions/commissions.controller.ts create mode 100644 src/modules/finance/dashboard/dashboard.controller.spec.ts create mode 100644 src/modules/finance/dashboard/dashboard.controller.ts create mode 100644 src/modules/finance/finance.controller.spec.ts create mode 100644 src/modules/finance/finance.controller.ts create mode 100644 src/modules/finance/finance.module.ts create mode 100644 src/modules/finance/finance.service.spec.ts create mode 100644 src/modules/finance/finance.service.ts create mode 100644 src/modules/finance/reports/reports.controller.spec.ts create mode 100644 src/modules/finance/reports/reports.controller.ts create mode 100644 src/modules/flight-management/dto/book-flight.dto.ts create mode 100644 src/modules/flight-management/dto/search-flight.dto.ts create mode 100644 src/modules/flight-management/flight-management.controller.ts create mode 100644 src/modules/flight-management/flight-management.module.ts create mode 100644 src/modules/flight-management/flight-management.service.ts create mode 100644 src/modules/geolocation/dto/geofence.dto.ts create mode 100644 src/modules/geolocation/dto/location-update.dto.ts create mode 100644 src/modules/geolocation/geolocation.controller.ts create mode 100644 src/modules/geolocation/geolocation.module.ts create mode 100644 src/modules/geolocation/geolocation.service.ts create mode 100755 src/modules/hotel/dto/create-hotel-checkin.dto.ts create mode 100755 src/modules/hotel/dto/room-service-request.dto.ts create mode 100644 src/modules/hotel/hotel.controller.ts create mode 100644 src/modules/hotel/hotel.module.ts create mode 100755 src/modules/hotel/hotel.service.ts create mode 100644 src/modules/iot-tourism/dto/device-reading.dto.ts create mode 100644 src/modules/iot-tourism/dto/wearable-sync.dto.ts create mode 100644 src/modules/iot-tourism/iot-tourism.controller.ts create mode 100644 src/modules/iot-tourism/iot-tourism.module.ts create mode 100644 src/modules/iot-tourism/iot-tourism.service.ts create mode 100644 src/modules/listings/dto/create-listing.dto.ts create mode 100644 src/modules/listings/dto/get-listings-filter.dto.ts create mode 100644 src/modules/listings/dto/update-listing.dto.ts create mode 100644 src/modules/listings/listings.controller.ts create mode 100644 src/modules/listings/listings.module.ts create mode 100644 src/modules/listings/listings.service.ts create mode 100755 src/modules/notifications/dto/create-notification.dto.ts create mode 100755 src/modules/notifications/notifications.controller.ts create mode 100755 src/modules/notifications/notifications.module.ts create mode 100755 src/modules/notifications/notifications.service.ts create mode 100755 src/modules/payments/dto/create-payment.dto.ts create mode 100644 src/modules/payments/dto/process-payment.dto.ts create mode 100755 src/modules/payments/payments.controller.ts create mode 100755 src/modules/payments/payments.module.ts create mode 100755 src/modules/payments/payments.service.ts create mode 100644 src/modules/personalization/dto/recommendation-request.dto.ts create mode 100644 src/modules/personalization/dto/update-preferences.dto.ts create mode 100644 src/modules/personalization/personalization.controller.ts create mode 100644 src/modules/personalization/personalization.module.ts create mode 100644 src/modules/personalization/personalization.service.ts create mode 100755 src/modules/restaurant/dto/create-menu-item.dto.ts create mode 100755 src/modules/restaurant/dto/create-order.dto.ts create mode 100755 src/modules/restaurant/dto/create-table.dto.ts create mode 100755 src/modules/restaurant/dto/update-order-status.dto.ts create mode 100755 src/modules/restaurant/restaurant.controller.ts create mode 100755 src/modules/restaurant/restaurant.module.ts create mode 100755 src/modules/restaurant/restaurant.service.ts create mode 100644 src/modules/reviews/dto/create-advanced-review.dto.ts create mode 100644 src/modules/reviews/dto/create-review.dto.ts create mode 100644 src/modules/reviews/dto/establishment-response.dto.ts create mode 100644 src/modules/reviews/dto/mark-helpfulness.dto.ts create mode 100644 src/modules/reviews/dto/review-helpfulness.dto.ts create mode 100644 src/modules/reviews/dto/review-response.dto.ts create mode 100644 src/modules/reviews/dto/update-review.dto.ts create mode 100644 src/modules/reviews/reviews.controller.ts create mode 100644 src/modules/reviews/reviews.module.ts create mode 100644 src/modules/reviews/reviews.service.ts create mode 100755 src/modules/security/dto/create-emergency-alert.dto.ts create mode 100755 src/modules/security/dto/create-incident.dto.ts create mode 100755 src/modules/security/dto/update-incident.dto.ts create mode 100755 src/modules/security/security.controller.ts create mode 100755 src/modules/security/security.module.ts create mode 100755 src/modules/security/security.service.ts create mode 100644 src/modules/social-commerce/dto/create-campaign.dto.ts create mode 100644 src/modules/social-commerce/dto/create-influencer-profile.dto.ts create mode 100644 src/modules/social-commerce/social-commerce.controller.ts create mode 100644 src/modules/social-commerce/social-commerce.module.ts create mode 100644 src/modules/social-commerce/social-commerce.service.ts create mode 100644 src/modules/sustainability/dto/carbon-offset.dto.ts create mode 100644 src/modules/sustainability/dto/track-activity.dto.ts create mode 100644 src/modules/sustainability/sustainability.controller.ts create mode 100644 src/modules/sustainability/sustainability.module.ts create mode 100644 src/modules/sustainability/sustainability.service.ts create mode 100755 src/modules/tourism/dto/create-destination.dto.ts create mode 100755 src/modules/tourism/dto/create-place.dto.ts create mode 100755 src/modules/tourism/dto/create-tour-guide.dto.ts create mode 100755 src/modules/tourism/dto/update-destination.dto.ts create mode 100755 src/modules/tourism/dto/update-place.dto.ts create mode 100755 src/modules/tourism/dto/update-tour-guide.dto.ts create mode 100755 src/modules/tourism/tourism.controller.ts create mode 100755 src/modules/tourism/tourism.module.ts create mode 100755 src/modules/tourism/tourism.service.ts create mode 100755 src/modules/upload/upload.controller.ts create mode 100755 src/modules/upload/upload.module.ts create mode 100755 src/modules/upload/upload.service.ts create mode 100755 src/modules/users/dto/create-user.dto.ts create mode 100755 src/modules/users/dto/update-user.dto.ts create mode 100755 src/modules/users/users.controller.ts create mode 100755 src/modules/users/users.module.ts create mode 100755 src/modules/users/users.service.ts create mode 100644 src/modules/vehicle-management/dto/create-vehicle.dto.ts create mode 100644 src/modules/vehicle-management/dto/update-vehicle.dto.ts create mode 100644 src/modules/vehicle-management/dto/vehicle-availability-query.dto.ts create mode 100644 src/modules/vehicle-management/vehicle-management.controller.ts create mode 100644 src/modules/vehicle-management/vehicle-management.module.ts create mode 100644 src/modules/vehicle-management/vehicle-management.service.ts create mode 100644 test.txt create mode 100755 test/app.e2e-spec.ts create mode 100755 test/jest-e2e.json create mode 100755 tsconfig.build.json create mode 100755 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..4b56acf --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.prettierrc b/.prettierrc new file mode 100755 index 0000000..dcb7279 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..b90fbc6 --- /dev/null +++ b/ecosystem.config.js @@ -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' + }] +}; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100755 index 0000000..caebf6e --- /dev/null +++ b/eslint.config.mjs @@ -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' + }, + }, +); \ No newline at end of file diff --git a/nest-cli.json b/nest-cli.json new file mode 100755 index 0000000..f9aa683 --- /dev/null +++ b/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100755 index 0000000..cdb6d4f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,15124 @@ +{ + "name": "karibeo-api", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "karibeo-api", + "version": "0.0.1", + "license": "UNLICENSED", + "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" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.8.tgz", + "integrity": "sha512-kcxUHKf5Hi98r4gAvMP3ntJV8wuQ3/i6wuU9RcMP0UKUt2Rer5Ryis3MPqT92jvVVwg6lhrLIhXsFuWJMiYjXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.8.tgz", + "integrity": "sha512-QsmFuYdAyeCyg9WF/AJBhFXDUfCwmDFTEbsv5t5KPSP6slhk0GoLNZApniiFytU2siRlSxVNpve2uATyYuAYkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.8", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.8.tgz", + "integrity": "sha512-RFnlyu4Ld8I4xvu/eqrhjbQ6kQTr27w79omMiTbQcQZvP3E6oUyZdBjobyih4Np+1VVQrbdEeNz76daP2iUDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.8", + "@angular-devkit/schematics": "19.2.8", + "@inquirer/prompts": "7.3.2", + "ansi-colors": "4.1.3", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.835.0.tgz", + "integrity": "sha512-htwcnVcCCXswbL/DSeuFIVd3f627On4Y1tSFlMZ9OmSC2+r9OTlUaHP8ugCCdx4Zofx2t4N/H2Cikd+l8vyvJw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.835.0", + "@aws-sdk/credential-provider-node": "3.835.0", + "@aws-sdk/middleware-bucket-endpoint": "3.830.0", + "@aws-sdk/middleware-expect-continue": "3.821.0", + "@aws-sdk/middleware-flexible-checksums": "3.835.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-location-constraint": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-sdk-s3": "3.835.0", + "@aws-sdk/middleware-ssec": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.835.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/signature-v4-multi-region": "3.835.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.835.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.5.3", + "@smithy/eventstream-serde-browser": "^4.0.4", + "@smithy/eventstream-serde-config-resolver": "^4.1.2", + "@smithy/eventstream-serde-node": "^4.0.4", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-blob-browser": "^4.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/hash-stream-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/md5-js": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.12", + "@smithy/middleware-retry": "^4.1.13", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.20", + "@smithy/util-defaults-mode-node": "^4.0.20", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.5", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "license": "MIT" + }, + "node_modules/@aws-sdk/client-s3/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.835.0.tgz", + "integrity": "sha512-4J19IcBKU5vL8yw/YWEvbwEGcmCli0rpRyxG53v0K5/3weVPxVBbKfkWcjWVQ4qdxNz2uInfbTde4BRBFxWllQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.835.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.835.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.835.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.5.3", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.12", + "@smithy/middleware-retry": "^4.1.13", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.20", + "@smithy/util-defaults-mode-node": "^4.0.20", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.835.0.tgz", + "integrity": "sha512-7mnf4xbaLI8rkDa+w6fUU48dG6yDuOgLXEPe4Ut3SbMp1ceJBPMozNHbCwkiyHk3HpxZYf8eVy0wXhJMrxZq5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/core": "^3.5.3", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.4", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.835.0.tgz", + "integrity": "sha512-U9LFWe7+ephNyekpUbzT7o6SmJTmn6xkrPkE0D7pbLojnPVi/8SZKyjtgQGIsAv+2kFkOCqMOIYUKd/0pE7uew==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.835.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.835.0.tgz", + "integrity": "sha512-jCdNEsQklil7frDm/BuVKl4ubVoQHRbV6fnkOjmxAJz0/v7cR8JP0jBGlqKKzh3ROh5/vo1/5VUZbCTLpc9dSg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.835.0", + "@aws-sdk/types": "3.821.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.4", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.835.0.tgz", + "integrity": "sha512-nqF6rYRAnJedmvDfrfKygzyeADcduDvtvn7GlbQQbXKeR2l7KnCdhuxHa0FALLvspkHiBx7NtInmvnd5IMuWsw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.835.0", + "@aws-sdk/credential-provider-env": "3.835.0", + "@aws-sdk/credential-provider-http": "3.835.0", + "@aws-sdk/credential-provider-process": "3.835.0", + "@aws-sdk/credential-provider-sso": "3.835.0", + "@aws-sdk/credential-provider-web-identity": "3.835.0", + "@aws-sdk/nested-clients": "3.835.0", + "@aws-sdk/types": "3.821.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.835.0.tgz", + "integrity": "sha512-77B8elyZlaEd7vDYyCnYtVLuagIBwuJ0AQ98/36JMGrYX7TT8UVAhiDAfVe0NdUOMORvDNFfzL06VBm7wittYw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.835.0", + "@aws-sdk/credential-provider-http": "3.835.0", + "@aws-sdk/credential-provider-ini": "3.835.0", + "@aws-sdk/credential-provider-process": "3.835.0", + "@aws-sdk/credential-provider-sso": "3.835.0", + "@aws-sdk/credential-provider-web-identity": "3.835.0", + "@aws-sdk/types": "3.821.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.835.0.tgz", + "integrity": "sha512-qXkTt5pAhSi2Mp9GdgceZZFo/cFYrA735efqi/Re/nf0lpqBp8mRM8xv+iAaPHV4Q10q0DlkbEidT1DhxdT/+w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.835.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.835.0.tgz", + "integrity": "sha512-jAiEMryaPFXayYGszrc7NcgZA/zrrE3QvvvUBh/Udasg+9Qp5ZELdJCm/p98twNyY9n5i6Ex6VgvdxZ7+iEheQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.835.0", + "@aws-sdk/core": "3.835.0", + "@aws-sdk/token-providers": "3.835.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.835.0.tgz", + "integrity": "sha512-zfleEFXDLlcJ7cyfS4xSyCRpd8SVlYZfH3rp0pg2vPYKbnmXVE0r+gPIYXl4L+Yz4A2tizYl63nKCNdtbxadog==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.835.0", + "@aws-sdk/nested-clients": "3.835.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/lib-storage": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.835.0.tgz", + "integrity": "sha512-bPQ5ncMOLsOJwEEYEpr1xeKp1ZrH3CMD7ipB/JnVvUCux7MVXzN1Zn29ELIR0QJTkNBMPI7wLkNsrc7MgljACA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.12", + "@smithy/smithy-client": "^4.4.4", + "buffer": "5.6.0", + "events": "3.3.0", + "stream-browserify": "3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.835.0" + } + }, + "node_modules/@aws-sdk/lib-storage/node_modules/buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.830.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.830.0.tgz", + "integrity": "sha512-ElVeCReZSH5Ds+/pkL5ebneJjuo8f49e9JXV1cYizuH0OAOQfYaBU9+M+7+rn61pTttOFE8W//qKzrXBBJhfMg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-arn-parser": "3.804.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.821.0.tgz", + "integrity": "sha512-zAOoSZKe1njOrtynvK6ZORU57YGv5I7KP4+rwOvUN3ZhJbQ7QPf8gKtFUCYAPRMegaXCKF/ADPtDZBAmM+zZ9g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.835.0.tgz", + "integrity": "sha512-9ezorQYlr5cQY28zWAReFhNKUTaXsi3TMvXIagMRrSeWtQ7R6TCYnt91xzHRCmFR2kp3zLI+dfoeH+wF3iCKUw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.835.0", + "@aws-sdk/types": "3.821.0", + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.821.0.tgz", + "integrity": "sha512-xSMR+sopSeWGx5/4pAGhhfMvGBHioVBbqGvDs6pG64xfNwM5vq5s5v6D04e2i+uSTj4qGa71dLUs5I0UzAK3sw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.821.0.tgz", + "integrity": "sha512-sKrm80k0t3R0on8aA/WhWFoMaAl4yvdk+riotmMElLUpcMcRXAd1+600uFVrxJqZdbrKQ0mjX0PjT68DlkYXLg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.821.0.tgz", + "integrity": "sha512-0cvI0ipf2tGx7fXYEEN5fBeZDz2RnHyb9xftSgUsEq7NBxjV0yTZfLJw6Za5rjE6snC80dRN8+bTNR1tuG89zA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.821.0.tgz", + "integrity": "sha512-efmaifbhBoqKG3bAoEfDdcM8hn1psF+4qa7ykWuYmfmah59JBeqHLfz5W9m9JoTwoKPkFcVLWZxnyZzAnVBOIg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.835.0.tgz", + "integrity": "sha512-oPebxpVf9smInHhevHh3APFZagGU+4RPwXEWv9YtYapFvsMq+8QXFvOfxfVZ/mwpe0JVG7EiJzL9/9Kobmts8Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.835.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-arn-parser": "3.804.0", + "@smithy/core": "^3.5.3", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.4", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.821.0.tgz", + "integrity": "sha512-YYi1Hhr2AYiU/24cQc8HIB+SWbQo6FBkMYojVuz/zgrtkFmALxENGF/21OPg7f/QWd+eadZJRxCjmRwh5F2Cxg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.835.0.tgz", + "integrity": "sha512-2gmAYygeE/gzhyF2XlkcbMLYFTbNfV61n+iCFa/ZofJHXYE+RxSyl5g4kujLEs7bVZHmjQZJXhprVSkGccq3/w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.835.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@smithy/core": "^3.5.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.835.0.tgz", + "integrity": "sha512-UtmOO0U5QkicjCEv+B32qqRAnS7o2ZkZhC+i3ccH1h3fsfaBshpuuNBwOYAzRCRBeKW5fw3ANFrV/+2FTp4jWg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.835.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.835.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.835.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.5.3", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.12", + "@smithy/middleware-retry": "^4.1.13", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.20", + "@smithy/util-defaults-mode-node": "^4.0.20", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.821.0.tgz", + "integrity": "sha512-t8og+lRCIIy5nlId0bScNpCkif8sc0LhmtaKsbm0ZPm3sCa/WhCbSZibjbZ28FNjVCV+p0D9RYZx0VDDbtWyjw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.835.0.tgz", + "integrity": "sha512-q4cJDf+6Ba8Oy5uWBgheErchkGz6uMdUN+wbDqVhWqP0SI6CKe62vnLb4t8oTRE4ErmJkL3ECLLszYMPXgE3dA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "3.835.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-format-url": "3.821.0", + "@smithy/middleware-endpoint": "^4.1.12", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.835.0.tgz", + "integrity": "sha512-rEtJH4dIwJYlXXe5rIH+uTCQmd2VIjuaoHlDY3Dr4nxF6po6U7vKsLfybIU2tgflGVqoqYQnXsfW/kj/Rh+/ow==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.835.0", + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.835.0.tgz", + "integrity": "sha512-zN1P3BE+Rv7w7q/CDA8VCQox6SE9QTn0vDtQ47AHA3eXZQQgYzBqgoLgJxR9rKKBIRGZqInJa/VRskLL95VliQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.835.0", + "@aws-sdk/nested-clients": "3.835.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.821.0.tgz", + "integrity": "sha512-Znroqdai1a90TlxGaJ+FK1lwC0fHpo97Xjsp5UKGR5JODYm7f9+/fF17ebO1KdoBr/Rm0UIFiF5VmI8ts9F1eA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.804.0.tgz", + "integrity": "sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.828.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.828.0.tgz", + "integrity": "sha512-RvKch111SblqdkPzg3oCIdlGxlQs+k+P7Etory9FmxPHyPDvsP1j1c74PmgYqtzzMWmoXTjd+c9naUHh9xG8xg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "@smithy/util-endpoints": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.821.0.tgz", + "integrity": "sha512-h+xqmPToxDrZ0a7rxE1a8Oh4zpWfZe9oiQUphGtfiGFA6j75UiURH5J3MmGHa/G4t15I3iLLbYtUXxvb1i7evg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", + "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.821.0.tgz", + "integrity": "sha512-irWZHyM0Jr1xhC+38OuZ7JB6OXMLPZlj48thElpsO1ZSLRkLZx5+I7VV6k3sp2yZ7BYbKz/G2ojSv4wdm7XTLw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.835.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.835.0.tgz", + "integrity": "sha512-gY63QZ4W5w9JYHYuqvUxiVGpn7IbCt1ODPQB0ZZwGGr3WRmK+yyZxCtFjbYhEQDQLgTWpf8YgVxgQLv2ps0PJg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.835.0", + "@aws-sdk/types": "3.821.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.821.0.tgz", + "integrity": "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", + "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", + "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", + "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", + "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz", + "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz", + "integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz", + "integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz", + "integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", + "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", + "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", + "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", + "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", + "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", + "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", + "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", + "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", + "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz", + "integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz", + "integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz", + "integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz", + "integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz", + "integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz", + "integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz", + "integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz", + "integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz", + "integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz", + "integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.8.tgz", + "integrity": "sha512-d/QAsnwuHX2OPolxvYcgSj7A9DO9H6gVOy2DvBTx+P2LH2iRTo/RSGV3iwCzW024nP9hw98KIuDmdyhZQj1UQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.12.tgz", + "integrity": "sha512-dpq+ielV9/bqgXRUbNH//KsY6WEw9DrGPmipkpmgC1Y46cwuBTNx7PXFWTjc3MQ+urcc0QxoVHcMI0FW4Ok0hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.13", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz", + "integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.13.tgz", + "integrity": "sha512-WbicD9SUQt/K8O5Vyk9iC2ojq5RHoCLK6itpp2fHsWe44VxxcA9z3GTWlvjSTGmMQpZr+lbVmrxdHcumJoLbMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.15.tgz", + "integrity": "sha512-4Y+pbr/U9Qcvf+N/goHzPEXiHH8680lM3Dr3Y9h9FFw4gHS+zVpbj8LfbKWIb/jayIB4aSO4pWiBTrBYWkvi5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", + "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.12.tgz", + "integrity": "sha512-xJ6PFZpDjC+tC1P8ImGprgcsrzQRsUh9aH3IZixm1lAZFK49UGHxM3ltFfuInN2kPYNfyoPRh+tU4ftsjPLKqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.15.tgz", + "integrity": "sha512-xWg+iYfqdhRiM55MvqiTCleHzszpoigUpN5+t1OMcRkJrUrw7va3AzXaxvS+Ak7Gny0j2mFSTv2JJj8sMtbV2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.15.tgz", + "integrity": "sha512-75CT2p43DGEnfGTaqFpbDC2p2EEMrq0S+IRrf9iJvYreMy5mAWj087+mdKyLHapUEPLjN10mNvABpGbk8Wdraw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.4.1.tgz", + "integrity": "sha512-UlmM5FVOZF0gpoe1PT/jN4vk8JmpIWBlMvTL8M+hlvPmzN89K6z03+IFmyeu/oFCenwdwHDr2gky7nIGSEVvlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.5", + "@inquirer/confirm": "^5.1.9", + "@inquirer/editor": "^4.2.10", + "@inquirer/expand": "^4.0.12", + "@inquirer/input": "^4.1.9", + "@inquirer/number": "^3.0.12", + "@inquirer/password": "^4.0.12", + "@inquirer/rawlist": "^4.0.12", + "@inquirer/search": "^3.0.12", + "@inquirer/select": "^4.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.3.tgz", + "integrity": "sha512-7XrV//6kwYumNDSsvJIPeAqa8+p7GJh7H5kRuxirct2cgOcSWwwNGoXDRgpNFbY/MG2vQ4ccIWCi8+IXXyFMZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.15.tgz", + "integrity": "sha512-YBMwPxYBrADqyvP4nNItpwkBnGGglAvCLVW8u4pRmmvOsHUtCAUIMbUrLX5B3tFL1/WsLGdQ2HNzkqswMs5Uaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.3.tgz", + "integrity": "sha512-OAGhXU0Cvh0PhLz9xTF/kx6g6x+sP+PcyTiLvCrewI99P3BBeexD+VbuwkNDvqGkk3y2h5ZiWLeRP7BFlhkUDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", + "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "license": "MIT" + }, + "node_modules/@napi-rs/nice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz", + "integrity": "sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.0.1", + "@napi-rs/nice-android-arm64": "1.0.1", + "@napi-rs/nice-darwin-arm64": "1.0.1", + "@napi-rs/nice-darwin-x64": "1.0.1", + "@napi-rs/nice-freebsd-x64": "1.0.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.0.1", + "@napi-rs/nice-linux-arm64-gnu": "1.0.1", + "@napi-rs/nice-linux-arm64-musl": "1.0.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.0.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.0.1", + "@napi-rs/nice-linux-s390x-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-musl": "1.0.1", + "@napi-rs/nice-win32-arm64-msvc": "1.0.1", + "@napi-rs/nice-win32-ia32-msvc": "1.0.1", + "@napi-rs/nice-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz", + "integrity": "sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz", + "integrity": "sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz", + "integrity": "sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz", + "integrity": "sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz", + "integrity": "sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nestjs/cli": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.7.tgz", + "integrity": "sha512-svrP8j1R0/lQVJ8ZI3BlDtuZxmkvVJokUJSB04sr6uibunk2wHeVDDVLZvYBUorCdGU/RHJl1IufhqUBM91vAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.8", + "@angular-devkit/schematics": "19.2.8", + "@angular-devkit/schematics-cli": "19.2.8", + "@inquirer/prompts": "7.4.1", + "@nestjs/schematics": "^11.0.1", + "ansis": "3.17.0", + "chokidar": "4.0.3", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.1.0", + "glob": "11.0.1", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tree-kill": "1.2.2", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.8.3", + "webpack": "5.99.6", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 20.11" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@nestjs/cli/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/@nestjs/cli/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nestjs/cli/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nestjs/cli/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/cli/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/cli/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/cli/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.99.6", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.6.tgz", + "integrity": "sha512-TJOLrJ6oeccsGWPl7ujCYuc0pIq2cNsuD6GZDma8i5o5Npvcco/z+NKvZSFsP0/x6SShVb0+X2JK/JHUjKY9dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.3.tgz", + "integrity": "sha512-ogEK+GriWodIwCw6buQ1rpcH4Kx+G7YQ9EwuPySI3rS05pSdtQ++UhucjusSI9apNidv+QURBztJkRecwwJQXg==", + "license": "MIT", + "dependencies": { + "file-type": "21.0.0", + "iterare": "1.2.1", + "load-esm": "1.0.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz", + "integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.7", + "dotenv-expand": "12.0.1", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/core": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.3.tgz", + "integrity": "sha512-5lTni0TCh8x7bXETRD57pQFnKnEg1T6M+VLE7wAmyQRIecKQU+2inRGZD+A4v2DC1I04eA0WffP0GKLxjOKlzw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nuxt/opencollective": "0.4.1", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.2.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/jwt": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz", + "integrity": "sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.7", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", + "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/passport": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", + "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "passport": "^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, + "node_modules/@nestjs/platform-express": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.3.tgz", + "integrity": "sha512-hEDNMlaPiBO72fxxX/CuRQL3MEhKRc/sIYGVoXjrnw6hTxZdezvvM6A95UaLsYknfmcZZa/CdG1SMBZOu9agHQ==", + "license": "MIT", + "dependencies": { + "cors": "2.8.5", + "express": "5.1.0", + "multer": "2.0.1", + "path-to-regexp": "8.2.0", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0" + } + }, + "node_modules/@nestjs/schedule": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.1.tgz", + "integrity": "sha512-v3yO6cSPAoBSSyH67HWnXHzuhPhSNZhRmLY38JvCt2sqY8sPMOODpcU1D79iUMFf7k16DaMEbL4Mgx61ZhiC8Q==", + "license": "MIT", + "dependencies": { + "cron": "4.3.3" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.5.tgz", + "integrity": "sha512-T50SCNyqCZ/fDssaOD7meBKLZ87ebRLaJqZTJPvJKjlib1VYhMOCwXYsr7bjMPmuPgiQHOwvppz77xN/m6GM7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.6", + "@angular-devkit/schematics": "19.2.6", + "comment-json": "4.2.5", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.6.tgz", + "integrity": "sha512-WFgiYhrDMq83UNaGRAneIM7CYYdBozD+yYA9BjoU8AgBLKtrvn6S8ZcjKAk5heoHtY/u8pEb0mwDTz9gxFmJZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.6.tgz", + "integrity": "sha512-YTAxNnT++5eflx19OUHmOWu597/TbTel+QARiZCv1xQw99+X8DCKKOUXtqBRd53CAHlREDI33Rn/JLY3NYgMLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.6", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@nestjs/schematics/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@nestjs/schematics/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@nestjs/swagger": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.0.tgz", + "integrity": "sha512-5wolt8GmpNcrQv34tIPUtPoV1EeFbCetm40Ij3+M0FNNnf2RJ3FyWfuQvI8SBlcJyfaounYVTKzKHreFXsUyOg==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.15.1", + "@nestjs/mapped-types": "2.1.0", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "8.2.0", + "swagger-ui-dist": "5.21.0" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/testing": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.3.tgz", + "integrity": "sha512-CeXG6/eEqgFIkPkmU00y18Dd3DLOIDFhPItzJK1SWckKo6IhcnfoRJzGx75bmuvUMjb51j6An96S/+MJ2ty9jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@nestjs/throttler": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.4.0.tgz", + "integrity": "sha512-osL67i0PUuwU5nqSuJjtUJZMkxAnYB4VldgYUMGzvYRJDCqGRFMWbsbzm/CkUtPLRL30I8T74Xgt/OQxnYokiA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0" + } + }, + "node_modules/@nestjs/typeorm": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz", + "integrity": "sha512-SOeUQl70Lb2OfhGkvnh4KXWlsd+zA08RuuQgT7kKbzivngxzSo1Oc7Usu5VxCxACQC9wc2l9esOHILSJeK7rJA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0", + "typeorm": "^0.3.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nuxt/opencollective": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.1.tgz", + "integrity": "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0", + "npm": ">=5.10.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sendgrid/client": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.5.tgz", + "integrity": "sha512-Jqt8aAuGIpWGa15ZorTWI46q9gbaIdQFA21HIPQQl60rCjzAko75l3D1z7EyjFrNr4MfQ0StusivWh8Rjh10Cg==", + "license": "MIT", + "dependencies": { + "@sendgrid/helpers": "^8.0.0", + "axios": "^1.8.2" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/@sendgrid/helpers": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz", + "integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/@sendgrid/mail": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.5.tgz", + "integrity": "sha512-W+YuMnkVs4+HA/bgfto4VHKcPKLc7NiZ50/NH2pzO6UHCCFuq8/GNB98YJlLEr/ESDyzAaDr7lVE7hoBwFTT3Q==", + "license": "MIT", + "dependencies": { + "@sendgrid/client": "^8.1.5", + "@sendgrid/helpers": "^8.0.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", + "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.0.0.tgz", + "integrity": "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.0.0.tgz", + "integrity": "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.4.tgz", + "integrity": "sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.5.3.tgz", + "integrity": "sha512-xa5byV9fEguZNofCclv6v9ra0FYh5FATQW/da7FQUVTic94DfrN/NvmKZjrMyzbpqfot9ZjBaO8U1UeTbmSLuA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.8", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.6.tgz", + "integrity": "sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.0.4.tgz", + "integrity": "sha512-7XoWfZqWb/QoR/rAU4VSi0mWnO2vu9/ltS6JZ5ZSZv0eovLVfDfu0/AX4ub33RsJTOth3TiFWSHS5YdztvFnig==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.3.1", + "@smithy/util-hex-encoding": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.0.4.tgz", + "integrity": "sha512-3fb/9SYaYqbpy/z/H3yIi0bYKyAa89y6xPmIqwr2vQiUT2St+avRt8UKwsWt9fEdEasc5d/V+QjrviRaX1JRFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.1.2.tgz", + "integrity": "sha512-JGtambizrWP50xHgbzZI04IWU7LdI0nh/wGbqH3sJesYToMi2j/DcoElqyOcqEIG/D4tNyxgRuaqBXWE3zOFhQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.0.4.tgz", + "integrity": "sha512-RD6UwNZ5zISpOWPuhVgRz60GkSIp0dy1fuZmj4RYmqLVRtejFqQ16WmfYDdoSoAjlp1LX+FnZo+/hkdmyyGZ1w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.0.4.tgz", + "integrity": "sha512-UeJpOmLGhq1SLox79QWw/0n2PFX+oPRE1ZyRMxPIaFEfCqWaqpB7BU9C8kpPOGEhLF7AwEqfFbtwNxGy4ReENA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.4.tgz", + "integrity": "sha512-AMtBR5pHppYMVD7z7G+OlHHAcgAN7v0kVKEpHuTO4Gb199Gowh0taYi9oDStFeUhetkeP55JLSVlTW1n9rFtUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.0.4.tgz", + "integrity": "sha512-WszRiACJiQV3QG6XMV44i5YWlkrlsM5Yxgz4jvsksuu7LDXA6wAtypfPajtNTadzpJy3KyJPoWehYpmZGKUFIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.0.0", + "@smithy/chunked-blob-reader-native": "^4.0.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.4.tgz", + "integrity": "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.0.4.tgz", + "integrity": "sha512-wHo0d8GXyVmpmMh/qOR0R7Y46/G1y6OR8U+bSTB4ppEzRxd1xVAQ9xOE9hOc0bSjhz0ujCPAbfNLkLrpa6cevg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.4.tgz", + "integrity": "sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.0.4.tgz", + "integrity": "sha512-uGLBVqcOwrLvGh/v/jw423yWHq/ofUGK1W31M2TNspLQbUV1Va0F5kTxtirkoHawODAZcjXTSGi7JwbnPcDPJg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.4.tgz", + "integrity": "sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.12.tgz", + "integrity": "sha512-Piy/9UOjh5FtEXhybjPwyOHcC/pGHFknl2Gc/q1YbEkngxY6eQwvBvZTNamXpyDAHCuP3h+lymcVcdyO3WdGqQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.5.3", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.13.tgz", + "integrity": "sha512-5ILvPCJevTcGpl7wAvSV9HKbIGS2Wxz505d0b5dP9kmjBhsFm1SAsSLIteMn925hlxPUkOsjcjMyaEiQDr9s4w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/service-error-classification": "^4.0.6", + "@smithy/smithy-client": "^4.4.4", + "@smithy/types": "^4.3.1", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz", + "integrity": "sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.4.tgz", + "integrity": "sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.3.tgz", + "integrity": "sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", + "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.4.tgz", + "integrity": "sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz", + "integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.4.tgz", + "integrity": "sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz", + "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.6.tgz", + "integrity": "sha512-RRoTDL//7xi4tn5FrN2NzH17jbgmnKidUqd4KvquT0954/i6CXXkh1884jBiunq24g9cGtPBEXlU40W6EpNOOg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.4.tgz", + "integrity": "sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.2.tgz", + "integrity": "sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.4.tgz", + "integrity": "sha512-38Ivn1VoArWi+wvJeW6rGl9lcuViYjmGfaZaBgOlFEyoQSIl2Rnr3uOWzwu3FE8NIvHflQVkwbveMQxBAEbd1A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.5.3", + "@smithy/middleware-endpoint": "^4.1.12", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz", + "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.20", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.20.tgz", + "integrity": "sha512-496BbDMx/8kQrvlhT0EsX7JM7yVpK7CACmG3LsqMX9RaJnF7M/OVlfbxoRceUp5o5S0HqBnV8/xGOX7MYCv2Gw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.4", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.20", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.20.tgz", + "integrity": "sha512-QsGHToYvRCoMyJQr/bXLG7L+nXNxICpG5LI1lRL0wkdkvLIxP89r4O+LHLWI9UeLzylxJ7VPnsTR/ADJ+F71/w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.4", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.6.tgz", + "integrity": "sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.4.tgz", + "integrity": "sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.6.tgz", + "integrity": "sha512-+YekoF2CaSMv6zKrA6iI/N9yva3Gzn4L6n35Luydweu5MMPYpiGZlWqehPHDHyNbnyaYlz/WJyYAZnC+loBDZg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.6", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.2.tgz", + "integrity": "sha512-aI+GLi7MJoVxg24/3J1ipwLoYzgkB4kUfogZfnslcYlynj3xsQ0e7vk4TnTro9hhsS5PvX1mwmkRqqHQjwcU7w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.5.tgz", + "integrity": "sha512-4QvC49HTteI1gfemu0I1syWovJgPvGn7CVUoN9ZFkdvr/cCFkrEL7qNCdx/2eICqDWEGnnr68oMdSIPCLAriSQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "license": "MIT" + }, + "node_modules/@swc/cli": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.6.0.tgz", + "integrity": "sha512-Q5FsI3Cw0fGMXhmsg7c08i4EmXCrcl+WnAxb6LYOLHw4JFFC3yzmx9LaXZ7QMbA+JZXbigU2TirI7RAfO0Qlnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/counter": "^0.1.3", + "@xhmikosr/bin-wrapper": "^13.0.5", + "commander": "^8.3.0", + "fast-glob": "^3.2.5", + "minimatch": "^9.0.3", + "piscina": "^4.3.1", + "semver": "^7.3.8", + "slash": "3.0.0", + "source-map": "^0.7.3" + }, + "bin": { + "spack": "bin/spack.js", + "swc": "bin/swc.js", + "swcx": "bin/swcx.js" + }, + "engines": { + "node": ">= 16.14.0" + }, + "peerDependencies": { + "@swc/core": "^1.2.66", + "chokidar": "^4.0.1" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@swc/cli/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@swc/cli/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@swc/cli/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@swc/core": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.6.tgz", + "integrity": "sha512-TEpta6Gi02X1b2yDIzBOIr7dFprvq9jD8RbEVI2OcMrwklbCUx0Dz9TrAnklSOwRvYvH5JjCx8ht9E94oWiG7A==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.23" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.12.6", + "@swc/core-darwin-x64": "1.12.6", + "@swc/core-linux-arm-gnueabihf": "1.12.6", + "@swc/core-linux-arm64-gnu": "1.12.6", + "@swc/core-linux-arm64-musl": "1.12.6", + "@swc/core-linux-x64-gnu": "1.12.6", + "@swc/core-linux-x64-musl": "1.12.6", + "@swc/core-win32-arm64-msvc": "1.12.6", + "@swc/core-win32-ia32-msvc": "1.12.6", + "@swc/core-win32-x64-msvc": "1.12.6" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.6.tgz", + "integrity": "sha512-yLiw+XzG+MilfFh0ON7qt67bfIr7UxB9JprhYReVOmLTBDmDVQSC3T4/vIuc+GwlX08ydnHy0ud4lIjTNW4uWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.12.6.tgz", + "integrity": "sha512-qwg8ux5x5Gd1LmSUtL4s9mbyfzAjr5M6OtjO281dKHwc/GYiSc4j1urb2jNSo9FcMkfT78oAOW2L6HQiWv+j1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.6.tgz", + "integrity": "sha512-pnkqH59JXBZu+MedaykMAC2or7tlUKeya7GKjzub+hkwxBP0ywWoFd+QYEdzp7QSziOt1VIHc4Wb9iZ2EfnzmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.6.tgz", + "integrity": "sha512-h8+Ltx0NSEzIFHetkOYoQ+UQ59unYLuJ4wF6kCpxzS4HskRLjcngr1HgN0F/PRpptnrmJUPVQmfms/vjN8ndAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.6.tgz", + "integrity": "sha512-GZu3MnB/5qtBxKEH46hgVDaplEe4mp3ZmQ1O2UpFCv/u/Ji3Gar5w5g2wHCZoT5AOouAhP1bh7IAEqjG/fbVfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.6.tgz", + "integrity": "sha512-WwJLQFzMW9ufVjM6k3le4HUgBFNunyt2oghjcgn2YjnKj0Ka2LrrBHCxfS7lgFSCQh/shib2wIlKXUnlTEWQJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.6.tgz", + "integrity": "sha512-rVGPNpI/sm8VVAhnB09Z/23OJP3ymouv6F4z4aYDbq/2JIwxqgpnl8gtMYP+Jw3XqabaFNjQmPiL15TvKCQaxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.6.tgz", + "integrity": "sha512-EKDJ1+8vaIlJGMl2yvd2HklV4GNbpKKwNQcUQid6j91tFYz4/aByw+9vh/sDVG7ZNqdmdywSnLRo317UTt0zFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.6.tgz", + "integrity": "sha512-jnULikZkR2fpZgFUQs7NsNIztavM1JdX+8Y+8FsfChBvMvziKxXtvUPGjeVJ8nzU1wgMnaeilJX9vrwuDGkA0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.6.tgz", + "integrity": "sha512-jL2Dcdcc/QZiQnwByP1uIE4k/mTlapzUng7owtLD2tSBBi1d+jPIdXIefdv+nccYJKRA+lKG3rRB6Tk9GrC7Kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz", + "integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", + "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "22.15.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.33.tgz", + "integrity": "sha512-wzoocdnnpSxZ+6CjW4ADCK1jVmd1S/J3ArNWfn8FDDQtRm8dkDg7TA+mvek2wNrfCgwuZxqEOiB9B1XCJ6+dbw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-local": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.38.tgz", + "integrity": "sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", + "integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", + "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/type-utils": "8.35.0", + "@typescript-eslint/utils": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.35.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", + "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", + "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.35.0", + "@typescript-eslint/types": "^8.35.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", + "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", + "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", + "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/utils": "8.35.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", + "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", + "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.35.0", + "@typescript-eslint/tsconfig-utils": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", + "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", + "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.35.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xhmikosr/archive-type": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/archive-type/-/archive-type-7.0.0.tgz", + "integrity": "sha512-sIm84ZneCOJuiy3PpWR5bxkx3HaNt1pqaN+vncUBZIlPZCq8ASZH+hBVdu5H8znR7qYC6sKwx+ie2Q7qztJTxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^19.0.0" + }, + "engines": { + "node": "^14.14.0 || >=16.0.0" + } + }, + "node_modules/@xhmikosr/archive-type/node_modules/file-type": { + "version": "19.6.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-19.6.0.tgz", + "integrity": "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-stream": "^9.0.1", + "strtok3": "^9.0.1", + "token-types": "^6.0.0", + "uint8array-extras": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/@xhmikosr/archive-type/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@xhmikosr/archive-type/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@xhmikosr/archive-type/node_modules/strtok3": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-9.1.1.tgz", + "integrity": "sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.3.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@xhmikosr/bin-check": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@xhmikosr/bin-check/-/bin-check-7.0.3.tgz", + "integrity": "sha512-4UnCLCs8DB+itHJVkqFp9Zjg+w/205/J2j2wNBsCEAm/BuBmtua2hhUOdAMQE47b1c7P9Xmddj0p+X1XVsfHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "isexe": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/bin-wrapper": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@xhmikosr/bin-wrapper/-/bin-wrapper-13.0.5.tgz", + "integrity": "sha512-DT2SAuHDeOw0G5bs7wZbQTbf4hd8pJ14tO0i4cWhRkIJfgRdKmMfkDilpaJ8uZyPA0NVRwasCNAmMJcWA67osw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/bin-check": "^7.0.3", + "@xhmikosr/downloader": "^15.0.1", + "@xhmikosr/os-filter-obj": "^3.0.0", + "bin-version-check": "^5.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress/-/decompress-10.0.1.tgz", + "integrity": "sha512-6uHnEEt5jv9ro0CDzqWlFgPycdE+H+kbJnwyxgZregIMLQ7unQSCNVsYG255FoqU8cP46DyggI7F7LohzEl8Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/decompress-tar": "^8.0.1", + "@xhmikosr/decompress-tarbz2": "^8.0.1", + "@xhmikosr/decompress-targz": "^8.0.1", + "@xhmikosr/decompress-unzip": "^7.0.0", + "graceful-fs": "^4.2.11", + "make-dir": "^4.0.0", + "strip-dirs": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-tar": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-tar/-/decompress-tar-8.0.1.tgz", + "integrity": "sha512-dpEgs0cQKJ2xpIaGSO0hrzz3Kt8TQHYdizHsgDtLorWajuHJqxzot9Hbi0huRxJuAGG2qiHSQkwyvHHQtlE+fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^19.0.0", + "is-stream": "^2.0.1", + "tar-stream": "^3.1.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-tar/node_modules/file-type": { + "version": "19.6.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-19.6.0.tgz", + "integrity": "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-stream": "^9.0.1", + "strtok3": "^9.0.1", + "token-types": "^6.0.0", + "uint8array-extras": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/@xhmikosr/decompress-tar/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@xhmikosr/decompress-tar/node_modules/get-stream/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@xhmikosr/decompress-tar/node_modules/strtok3": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-9.1.1.tgz", + "integrity": "sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.3.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@xhmikosr/decompress-tarbz2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-tarbz2/-/decompress-tarbz2-8.0.2.tgz", + "integrity": "sha512-p5A2r/AVynTQSsF34Pig6olt9CvRj6J5ikIhzUd3b57pUXyFDGtmBstcw+xXza0QFUh93zJsmY3zGeNDlR2AQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/decompress-tar": "^8.0.1", + "file-type": "^19.6.0", + "is-stream": "^2.0.1", + "seek-bzip": "^2.0.0", + "unbzip2-stream": "^1.4.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-tarbz2/node_modules/file-type": { + "version": "19.6.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-19.6.0.tgz", + "integrity": "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-stream": "^9.0.1", + "strtok3": "^9.0.1", + "token-types": "^6.0.0", + "uint8array-extras": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/@xhmikosr/decompress-tarbz2/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@xhmikosr/decompress-tarbz2/node_modules/get-stream/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@xhmikosr/decompress-tarbz2/node_modules/strtok3": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-9.1.1.tgz", + "integrity": "sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.3.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@xhmikosr/decompress-targz": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-targz/-/decompress-targz-8.0.1.tgz", + "integrity": "sha512-mvy5AIDIZjQ2IagMI/wvauEiSNHhu/g65qpdM4EVoYHUJBAmkQWqcPJa8Xzi1aKVTmOA5xLJeDk7dqSjlHq8Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/decompress-tar": "^8.0.1", + "file-type": "^19.0.0", + "is-stream": "^2.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-targz/node_modules/file-type": { + "version": "19.6.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-19.6.0.tgz", + "integrity": "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-stream": "^9.0.1", + "strtok3": "^9.0.1", + "token-types": "^6.0.0", + "uint8array-extras": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/@xhmikosr/decompress-targz/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@xhmikosr/decompress-targz/node_modules/get-stream/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@xhmikosr/decompress-targz/node_modules/strtok3": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-9.1.1.tgz", + "integrity": "sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.3.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@xhmikosr/decompress-unzip": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-unzip/-/decompress-unzip-7.0.0.tgz", + "integrity": "sha512-GQMpzIpWTsNr6UZbISawsGI0hJ4KA/mz5nFq+cEoPs12UybAqZWKbyIaZZyLbJebKl5FkLpsGBkrplJdjvUoSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^19.0.0", + "get-stream": "^6.0.1", + "yauzl": "^3.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-unzip/node_modules/file-type": { + "version": "19.6.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-19.6.0.tgz", + "integrity": "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-stream": "^9.0.1", + "strtok3": "^9.0.1", + "token-types": "^6.0.0", + "uint8array-extras": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/@xhmikosr/decompress-unzip/node_modules/file-type/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@xhmikosr/decompress-unzip/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@xhmikosr/decompress-unzip/node_modules/strtok3": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-9.1.1.tgz", + "integrity": "sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.3.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@xhmikosr/downloader": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@xhmikosr/downloader/-/downloader-15.0.1.tgz", + "integrity": "sha512-fiuFHf3Dt6pkX8HQrVBsK0uXtkgkVlhrZEh8b7VgoDqFf+zrgFBPyrwCqE/3nDwn3hLeNz+BsrS7q3mu13Lp1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/archive-type": "^7.0.0", + "@xhmikosr/decompress": "^10.0.1", + "content-disposition": "^0.5.4", + "defaults": "^3.0.0", + "ext-name": "^5.0.0", + "file-type": "^19.0.0", + "filenamify": "^6.0.0", + "get-stream": "^6.0.1", + "got": "^13.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/downloader/node_modules/file-type": { + "version": "19.6.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-19.6.0.tgz", + "integrity": "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-stream": "^9.0.1", + "strtok3": "^9.0.1", + "token-types": "^6.0.0", + "uint8array-extras": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/@xhmikosr/downloader/node_modules/file-type/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@xhmikosr/downloader/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@xhmikosr/downloader/node_modules/strtok3": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-9.1.1.tgz", + "integrity": "sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.3.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@xhmikosr/os-filter-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/os-filter-obj/-/os-filter-obj-3.0.0.tgz", + "integrity": "sha512-siPY6BD5dQ2SZPl3I0OZBHL27ZqZvLEosObsZRQ1NUB8qcxegwt0T9eKtV96JMFQpIz1elhkzqOg4c/Ri6Dp9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arch": "^3.0.0" + }, + "engines": { + "node": "^14.14.0 || >=16.0.0" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "devOptional": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-3.0.0.tgz", + "integrity": "sha512-AmIAC+Wtm2AU8lGfTtHsw0Y9Qtftx2YXEEtiBP10xFUtMOA+sHHx6OAddyL52mUKh1vsXQ6/w1mVDptZCyUt4Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/bin-version": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz", + "integrity": "sha512-nk5wEsP4RiKjG+vF+uG8lFsEn4d7Y6FVDamzzftSunXOoOcOOkzcWdKVlGgFFwlUQCj63SgnUkLLGF8v7lufhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "find-versions": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bin-version-check": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-5.1.0.tgz", + "integrity": "sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bin-version": "^6.0.0", + "semver": "^7.5.3", + "semver-truncate": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001724", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz", + "integrity": "sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", + "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.11.1", + "validator": "^13.9.0" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/cron": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.3.tgz", + "integrity": "sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.7.0", + "luxon": "~3.7.0" + }, + "engines": { + "node": ">=18.x" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-3.0.0.tgz", + "integrity": "sha512-RsqXDEAALjfRTro+IFNKpcPCt0/Cy2FqHSIlnomiJp9YGadpQnrtbRpSgN2+np21qHcIKiva4fiOQGjS9/qR/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz", + "integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.173", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.173.tgz", + "integrity": "sha512-2bFhXP2zqSfQHugjqJIDFVwa+qIxyNApenmXTp9EjaKtdPrES5Qcn9/aSFy/NaP2E+fWG/zxKu/LBvY36p5VNQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz", + "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.1", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.29.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.0.tgz", + "integrity": "sha512-8qsOYwkkGrahrgoUv76NZi23koqXOGiiEzXMrT8Q7VcYaUISR+5MorIUxfWqYXN0fN/31WbSrxCxFkVQ43wwrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz", + "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.7", + "strtok3": "^10.2.2", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filename-reserved-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", + "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/filenamify": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz", + "integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-versions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", + "integrity": "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-regex": "^4.0.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^4.0.1", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", + "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inspect-with-kind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz", + "integrity": "sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "kind-of": "^6.0.2" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.9.tgz", + "integrity": "sha512-VWwAdNeJgN7jFOD+wN4qx83DTPMVPPAUyx9/TUkBXKLiNkuWWk6anV0439tgdtwaJDrEdqkvdN22iA6J4bUCZg==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-esm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.2.tgz", + "integrity": "sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz", + "integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer-s3": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/multer-s3/-/multer-s3-3.0.1.tgz", + "integrity": "sha512-BFwSO80a5EW4GJRBdUuSHblz2jhVSAze33ZbnGpcfEicoT0iRolx4kWR+AJV07THFRCQ78g+kelKFdjkCCaXeQ==", + "license": "MIT", + "dependencies": { + "@aws-sdk/lib-storage": "^3.46.0", + "file-type": "^3.3.0", + "html-comment-regex": "^1.1.2", + "run-parallel": "^1.1.6" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.0.0" + } + }, + "node_modules/multer-s3/node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.4.0.tgz", + "integrity": "sha512-D9DI/gXHvVmjHS08SVch0Em8G5S1P+QWtU31appcKT/8wFSPRcdHadIFSAntdMMVM5zz+/DL+bL/gz3UDppqtg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.2.tgz", + "integrity": "sha512-Ee/R3SyN4BuynXcnTaekmaVdbDAEiNrHqjQIA37mHU8G9pf7aaAD4ZX3XjBLo6rsdcxA/gtkcNYZLt30ACgynw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/peek-readable": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.4.2.tgz", + "integrity": "sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.2.tgz", + "integrity": "sha512-OtLWF0mKLmpxelOt9BqVq83QV6bTfsS0XLegIeAKqKjurRnRKie1Dc1iL89MugmSLhftxw6NNCyZhm1yQFLMEQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.2", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.6" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.6.tgz", + "integrity": "sha512-uxmJAnmIgmYgnSFzgOf2cqGQBzwnRYcrEgXuFjJNEkpedEIPBSEzxY7ph4uA9k1mI+l/GR0HjPNS6FKNZe8SBQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.2.tgz", + "integrity": "sha512-Ci7jy8PbaWxfsck2dwZdERcDG2A0MG8JoQILs+uZNjABFuBuItAZCWUNz8sXRDMoui24rJw7WlXqgpMdBSN/vQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/piscina": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.9.2.tgz", + "integrity": "sha512-Fq0FERJWFEUpB4eSY59wSNwXD4RYqR+nR/WiEVcZW8IWfVBxJJafcgTEZDQo8k3w0sUarJ8RyVbbUF4GQ2LGbQ==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.0.tgz", + "integrity": "sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/seek-bzip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-2.0.0.tgz", + "integrity": "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^6.0.0" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/seek-bzip/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-regex": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", + "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver-truncate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-3.0.0.tgz", + "integrity": "sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/sharp": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.2.tgz", + "integrity": "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.2", + "@img/sharp-darwin-x64": "0.34.2", + "@img/sharp-libvips-darwin-arm64": "1.1.0", + "@img/sharp-libvips-darwin-x64": "1.1.0", + "@img/sharp-libvips-linux-arm": "1.1.0", + "@img/sharp-libvips-linux-arm64": "1.1.0", + "@img/sharp-libvips-linux-ppc64": "1.1.0", + "@img/sharp-libvips-linux-s390x": "1.1.0", + "@img/sharp-libvips-linux-x64": "1.1.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", + "@img/sharp-libvips-linuxmusl-x64": "1.1.0", + "@img/sharp-linux-arm": "0.34.2", + "@img/sharp-linux-arm64": "0.34.2", + "@img/sharp-linux-s390x": "0.34.2", + "@img/sharp-linux-x64": "0.34.2", + "@img/sharp-linuxmusl-arm64": "0.34.2", + "@img/sharp-linuxmusl-x64": "0.34.2", + "@img/sharp-wasm32": "0.34.2", + "@img/sharp-win32-arm64": "0.34.2", + "@img/sharp-win32-ia32": "0.34.2", + "@img/sharp-win32-x64": "0.34.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-3.0.0.tgz", + "integrity": "sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "inspect-with-kind": "^1.0.5", + "is-plain-obj": "^1.1.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stripe": { + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-18.2.1.tgz", + "integrity": "sha512-GwB1B7WSwEBzW4dilgyJruUYhbGMscrwuyHsPUmSRKrGHZ5poSh2oU9XKdii5BFVJzXHn35geRvGJ6R8bYcp8w==", + "license": "MIT", + "dependencies": { + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + }, + "peerDependencies": { + "@types/node": ">=12.x.x" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/strtok3": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.1.tgz", + "integrity": "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/superagent": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz", + "integrity": "sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz", + "integrity": "sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz", + "integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/terser": { + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", + "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz", + "integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", + "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typeorm": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.25.tgz", + "integrity": "sha512-fTKDFzWXKwAaBdEMU4k661seZewbNYET4r1J/z3Jwf+eAvlzMVpTLKAVcAzg75WwQk7GDmtsmkZ5MfkmXCiFWg==", + "license": "MIT", + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "ansis": "^3.17.0", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "dayjs": "^1.11.13", + "debug": "^4.4.0", + "dedent": "^1.6.0", + "dotenv": "^16.4.7", + "glob": "^10.4.5", + "sha.js": "^2.4.11", + "sql-highlight": "^6.0.0", + "tslib": "^2.8.1", + "uuid": "^11.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", + "@sap/hana-client": "^2.12.25", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "hdb-pool": "^0.1.6", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0", + "reflect-metadata": "^0.1.14 || ^0.2.0", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "hdb-pool": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typeorm/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/typeorm/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/typeorm/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", + "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.35.0", + "@typescript-eslint/parser": "8.35.0", + "@typescript-eslint/utils": "8.35.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/wcwidth/node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack": { + "version": "5.99.9", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz", + "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", + "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100755 index 0000000..cca2858 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts new file mode 100755 index 0000000..d22f389 --- /dev/null +++ b/src/app.controller.spec.ts @@ -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); + }); + + describe('root', () => { + it('should return "Hello World!"', () => { + expect(appController.getHello()).toBe('Hello World!'); + }); + }); +}); diff --git a/src/app.controller.ts b/src/app.controller.ts new file mode 100755 index 0000000..cce879e --- /dev/null +++ b/src/app.controller.ts @@ -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(); + } +} diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100755 index 0000000..789b68d --- /dev/null +++ b/src/app.module.ts @@ -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('app.throttle.ttl') || 60, + limit: configService.get('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 {} diff --git a/src/app.service.ts b/src/app.service.ts new file mode 100755 index 0000000..927d7cc --- /dev/null +++ b/src/app.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + getHello(): string { + return 'Hello World!'; + } +} diff --git a/src/common/decorators/roles.decorator.ts b/src/common/decorators/roles.decorator.ts new file mode 100755 index 0000000..e038e16 --- /dev/null +++ b/src/common/decorators/roles.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); diff --git a/src/common/guards/jwt-auth.guard.ts b/src/common/guards/jwt-auth.guard.ts new file mode 100755 index 0000000..2155290 --- /dev/null +++ b/src/common/guards/jwt-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') {} diff --git a/src/common/guards/roles.guard.ts b/src/common/guards/roles.guard.ts new file mode 100755 index 0000000..7ae1491 --- /dev/null +++ b/src/common/guards/roles.guard.ts @@ -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(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); + } +} \ No newline at end of file diff --git a/src/config/app.config.ts b/src/config/app.config.ts new file mode 100755 index 0000000..d65f93c --- /dev/null +++ b/src/config/app.config.ts @@ -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), + }, +})); diff --git a/src/config/database.config.ts b/src/config/database.config.ts new file mode 100755 index 0000000..c718117 --- /dev/null +++ b/src/config/database.config.ts @@ -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, + }, +})); diff --git a/src/config/integrations/aws.config.ts b/src/config/integrations/aws.config.ts new file mode 100755 index 0000000..2347454 --- /dev/null +++ b/src/config/integrations/aws.config.ts @@ -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, + }, +})); diff --git a/src/config/integrations/communication.config.ts b/src/config/integrations/communication.config.ts new file mode 100755 index 0000000..3fdb76e --- /dev/null +++ b/src/config/integrations/communication.config.ts @@ -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, + }, +})); diff --git a/src/config/integrations/stripe.config.ts b/src/config/integrations/stripe.config.ts new file mode 100755 index 0000000..cabf4b5 --- /dev/null +++ b/src/config/integrations/stripe.config.ts @@ -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, +})); diff --git a/src/config/jwt.config.ts b/src/config/jwt.config.ts new file mode 100755 index 0000000..2c1fb07 --- /dev/null +++ b/src/config/jwt.config.ts @@ -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', + }, +})); diff --git a/src/entities/admin-transaction.entity.ts b/src/entities/admin-transaction.entity.ts new file mode 100644 index 0000000..098aa34 --- /dev/null +++ b/src/entities/admin-transaction.entity.ts @@ -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; +} \ No newline at end of file diff --git a/src/entities/advanced-review.entity.ts b/src/entities/advanced-review.entity.ts new file mode 100644 index 0000000..bd5ef4c --- /dev/null +++ b/src/entities/advanced-review.entity.ts @@ -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 | 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[] | 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; +} diff --git a/src/entities/ai-generated-content.entity.ts b/src/entities/ai-generated-content.entity.ts new file mode 100644 index 0000000..6efae38 --- /dev/null +++ b/src/entities/ai-generated-content.entity.ts @@ -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; + + @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; +} diff --git a/src/entities/ai-guide-interaction.entity.ts b/src/entities/ai-guide-interaction.entity.ts new file mode 100644 index 0000000..bf5f6ad --- /dev/null +++ b/src/entities/ai-guide-interaction.entity.ts @@ -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 | null; + + // Relations + @ManyToOne(() => User) + @JoinColumn({ name: 'user_id' }) + user: User; + + @ManyToOne(() => PlaceOfInterest) + @JoinColumn({ name: 'place_id' }) + place: PlaceOfInterest; +} diff --git a/src/entities/ar-content.entity.ts b/src/entities/ar-content.entity.ts new file mode 100644 index 0000000..54667b8 --- /dev/null +++ b/src/entities/ar-content.entity.ts @@ -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 | null; + + @ApiProperty({ description: 'Content metadata' }) + @Column({ type: 'jsonb', nullable: true }) + metadata: Record | 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; +} diff --git a/src/entities/availability.entity.ts b/src/entities/availability.entity.ts new file mode 100644 index 0000000..ddc6ca8 --- /dev/null +++ b/src/entities/availability.entity.ts @@ -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 | 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; +} diff --git a/src/entities/base.entity.ts b/src/entities/base.entity.ts new file mode 100755 index 0000000..8b67d80 --- /dev/null +++ b/src/entities/base.entity.ts @@ -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; +} diff --git a/src/entities/channel.entity.ts b/src/entities/channel.entity.ts new file mode 100644 index 0000000..5db330c --- /dev/null +++ b/src/entities/channel.entity.ts @@ -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 | null; + + @ApiProperty({ description: 'Channel configuration' }) + @Column({ type: 'jsonb', nullable: true }) + config: Record | 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; +} diff --git a/src/entities/commission-rate.entity.ts b/src/entities/commission-rate.entity.ts new file mode 100644 index 0000000..1f67f2f --- /dev/null +++ b/src/entities/commission-rate.entity.ts @@ -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; +} \ No newline at end of file diff --git a/src/entities/country.entity.ts b/src/entities/country.entity.ts new file mode 100755 index 0000000..6ce5615 --- /dev/null +++ b/src/entities/country.entity.ts @@ -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[]; +} diff --git a/src/entities/creator-campaign.entity.ts b/src/entities/creator-campaign.entity.ts new file mode 100644 index 0000000..cf06656 --- /dev/null +++ b/src/entities/creator-campaign.entity.ts @@ -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; + 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; + actualMetrics: Record; + 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; +} diff --git a/src/entities/destination.entity.ts b/src/entities/destination.entity.ts new file mode 100755 index 0000000..a7c9756 --- /dev/null +++ b/src/entities/destination.entity.ts @@ -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; + + @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; +} diff --git a/src/entities/eco-establishment.entity.ts b/src/entities/eco-establishment.entity.ts new file mode 100644 index 0000000..1b0cc8c --- /dev/null +++ b/src/entities/eco-establishment.entity.ts @@ -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; +} diff --git a/src/entities/emergency-alert.entity.ts b/src/entities/emergency-alert.entity.ts new file mode 100755 index 0000000..d4773b2 --- /dev/null +++ b/src/entities/emergency-alert.entity.ts @@ -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; +} diff --git a/src/entities/establishment.entity.ts b/src/entities/establishment.entity.ts new file mode 100755 index 0000000..088299c --- /dev/null +++ b/src/entities/establishment.entity.ts @@ -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; + + @ApiProperty({ description: 'Images' }) + @Column({ type: 'jsonb', nullable: true }) + images: Record; + + @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; +} diff --git a/src/entities/flight.entity.ts b/src/entities/flight.entity.ts new file mode 100644 index 0000000..37f51c0 --- /dev/null +++ b/src/entities/flight.entity.ts @@ -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; // { 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[] | null; + + @ApiProperty({ description: 'Baggage policy' }) + @Column({ name: 'baggage_policy', type: 'jsonb', nullable: true }) + baggagePolicy: Record | 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; +} diff --git a/src/entities/geofence.entity.ts b/src/entities/geofence.entity.ts new file mode 100644 index 0000000..40eaf8c --- /dev/null +++ b/src/entities/geofence.entity.ts @@ -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 | 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; +} diff --git a/src/entities/hotel-checkin.entity.ts b/src/entities/hotel-checkin.entity.ts new file mode 100755 index 0000000..1bd4898 --- /dev/null +++ b/src/entities/hotel-checkin.entity.ts @@ -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; + + @ApiProperty({ description: 'Room access log' }) + @Column({ name: 'access_log', type: 'jsonb', nullable: true }) + accessLog: Record[]; + + // Relations + @ManyToOne(() => HotelRoom) + @JoinColumn({ name: 'room_id' }) + room: HotelRoom; + + @ManyToOne(() => User) + @JoinColumn({ name: 'guest_id' }) + guest: User; +} diff --git a/src/entities/hotel-room.entity.ts b/src/entities/hotel-room.entity.ts new file mode 100755 index 0000000..0394605 --- /dev/null +++ b/src/entities/hotel-room.entity.ts @@ -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; + + @ApiProperty({ description: 'Is available', example: true }) + @Column({ name: 'is_available', default: true }) + isAvailable: boolean; + + // Relations + @ManyToOne(() => Establishment) + @JoinColumn({ name: 'establishment_id' }) + establishment: Establishment; +} diff --git a/src/entities/hotel-service.entity.ts b/src/entities/hotel-service.entity.ts new file mode 100755 index 0000000..8877a82 --- /dev/null +++ b/src/entities/hotel-service.entity.ts @@ -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[]; + + @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; +} diff --git a/src/entities/incident.entity.ts b/src/entities/incident.entity.ts new file mode 100755 index 0000000..659b60b --- /dev/null +++ b/src/entities/incident.entity.ts @@ -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; +} diff --git a/src/entities/influencer-profile.entity.ts b/src/entities/influencer-profile.entity.ts new file mode 100644 index 0000000..65575e6 --- /dev/null +++ b/src/entities/influencer-profile.entity.ts @@ -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; + interests: string[]; + peakEngagementTimes: string[]; + }; + contentAnalysis: { + topPerformingContentTypes: string[]; + averageEngagementByType: Record; + sentimentAnalysis: { positive: number; neutral: number; negative: number }; + }; + marketValue: { + estimatedReach: number; + estimatedValue: number; + growthPotential: string; + }; + }; + + // Relations + @OneToOne(() => User) + @JoinColumn({ name: 'user_id' }) + user: User; +} diff --git a/src/entities/iot-device.entity.ts b/src/entities/iot-device.entity.ts new file mode 100644 index 0000000..8e54c5d --- /dev/null +++ b/src/entities/iot-device.entity.ts @@ -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; + 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; +} diff --git a/src/entities/itinerary.entity.ts b/src/entities/itinerary.entity.ts new file mode 100755 index 0000000..a7fe4a8 --- /dev/null +++ b/src/entities/itinerary.entity.ts @@ -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; + + @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; +} diff --git a/src/entities/language.entity.ts b/src/entities/language.entity.ts new file mode 100755 index 0000000..653ef03 --- /dev/null +++ b/src/entities/language.entity.ts @@ -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[]; +} diff --git a/src/entities/listing.entity.ts b/src/entities/listing.entity.ts new file mode 100644 index 0000000..84710a5 --- /dev/null +++ b/src/entities/listing.entity.ts @@ -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[] | null; + + @ApiProperty({ description: 'Property rules and policies' }) + @Column({ type: 'jsonb', nullable: true }) + policies: Record | 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 | 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; +} diff --git a/src/entities/location-tracking.entity.ts b/src/entities/location-tracking.entity.ts new file mode 100644 index 0000000..2b4affe --- /dev/null +++ b/src/entities/location-tracking.entity.ts @@ -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 | 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; +} diff --git a/src/entities/menu-item.entity.ts b/src/entities/menu-item.entity.ts new file mode 100755 index 0000000..f2de257 --- /dev/null +++ b/src/entities/menu-item.entity.ts @@ -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; + + @ApiProperty({ description: 'Nutritional info' }) + @Column({ name: 'nutritional_info', type: 'jsonb', nullable: true }) + nutritionalInfo: Record; + + @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; +} diff --git a/src/entities/notification.entity.ts b/src/entities/notification.entity.ts new file mode 100755 index 0000000..5fccf9c --- /dev/null +++ b/src/entities/notification.entity.ts @@ -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; + + @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; +} diff --git a/src/entities/order-item.entity.ts b/src/entities/order-item.entity.ts new file mode 100755 index 0000000..cb589ae --- /dev/null +++ b/src/entities/order-item.entity.ts @@ -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; +} diff --git a/src/entities/order.entity.ts b/src/entities/order.entity.ts new file mode 100755 index 0000000..603bf12 --- /dev/null +++ b/src/entities/order.entity.ts @@ -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; +} diff --git a/src/entities/place-of-interest.entity.ts b/src/entities/place-of-interest.entity.ts new file mode 100755 index 0000000..1dbfff8 --- /dev/null +++ b/src/entities/place-of-interest.entity.ts @@ -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; + + @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; + + @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; + + @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; +} diff --git a/src/entities/product.entity.ts b/src/entities/product.entity.ts new file mode 100755 index 0000000..5a2db76 --- /dev/null +++ b/src/entities/product.entity.ts @@ -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; + + @ApiProperty({ description: 'Product specifications' }) + @Column({ type: 'jsonb', nullable: true }) + specifications: Record; + + @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; +} diff --git a/src/entities/reservation.entity.ts b/src/entities/reservation.entity.ts new file mode 100755 index 0000000..2fee990 --- /dev/null +++ b/src/entities/reservation.entity.ts @@ -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; +} diff --git a/src/entities/review-helpfulness.entity.ts b/src/entities/review-helpfulness.entity.ts new file mode 100644 index 0000000..4100c19 --- /dev/null +++ b/src/entities/review-helpfulness.entity.ts @@ -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; +} diff --git a/src/entities/review.entity.ts b/src/entities/review.entity.ts new file mode 100755 index 0000000..7b6394c --- /dev/null +++ b/src/entities/review.entity.ts @@ -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; + + @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; +} diff --git a/src/entities/role.entity.ts b/src/entities/role.entity.ts new file mode 100755 index 0000000..9d34a79 --- /dev/null +++ b/src/entities/role.entity.ts @@ -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; + + @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[]; +} diff --git a/src/entities/security-officer.entity.ts b/src/entities/security-officer.entity.ts new file mode 100755 index 0000000..87b95ea --- /dev/null +++ b/src/entities/security-officer.entity.ts @@ -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; +} diff --git a/src/entities/settlement.entity.ts b/src/entities/settlement.entity.ts new file mode 100644 index 0000000..3381ec9 --- /dev/null +++ b/src/entities/settlement.entity.ts @@ -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; +} \ No newline at end of file diff --git a/src/entities/smart-tourism-data.entity.ts b/src/entities/smart-tourism-data.entity.ts new file mode 100644 index 0000000..5016083 --- /dev/null +++ b/src/entities/smart-tourism-data.entity.ts @@ -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; +} diff --git a/src/entities/sustainability-tracking.entity.ts b/src/entities/sustainability-tracking.entity.ts new file mode 100644 index 0000000..5bc4ed8 --- /dev/null +++ b/src/entities/sustainability-tracking.entity.ts @@ -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; +} diff --git a/src/entities/table.entity.ts b/src/entities/table.entity.ts new file mode 100755 index 0000000..c4117b5 --- /dev/null +++ b/src/entities/table.entity.ts @@ -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; +} diff --git a/src/entities/taxi-driver.entity.ts b/src/entities/taxi-driver.entity.ts new file mode 100755 index 0000000..4f7671d --- /dev/null +++ b/src/entities/taxi-driver.entity.ts @@ -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; +} diff --git a/src/entities/tour-guide.entity.ts b/src/entities/tour-guide.entity.ts new file mode 100755 index 0000000..1b4f364 --- /dev/null +++ b/src/entities/tour-guide.entity.ts @@ -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; + + @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; +} diff --git a/src/entities/transaction.entity.ts b/src/entities/transaction.entity.ts new file mode 100755 index 0000000..d8e314e --- /dev/null +++ b/src/entities/transaction.entity.ts @@ -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; + + @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; +} diff --git a/src/entities/ugc-content.entity.ts b/src/entities/ugc-content.entity.ts new file mode 100644 index 0000000..8dbd1ee --- /dev/null +++ b/src/entities/ugc-content.entity.ts @@ -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; +} diff --git a/src/entities/user-personalization.entity.ts b/src/entities/user-personalization.entity.ts new file mode 100644 index 0000000..dbe7a98 --- /dev/null +++ b/src/entities/user-personalization.entity.ts @@ -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; // { italian: 0.8, local: 0.9, asian: 0.3 } + activityScores: Record; // { adventure: 0.7, cultural: 0.9, nightlife: 0.2 } + priceSegments: Record; // { budget: 0.2, mid: 0.8, luxury: 0.3 } + accommodationScores: Record; + seasonalScores: Record; + }; + + @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; +} diff --git a/src/entities/user-preferences.entity.ts b/src/entities/user-preferences.entity.ts new file mode 100755 index 0000000..001ae97 --- /dev/null +++ b/src/entities/user-preferences.entity.ts @@ -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; + + // Relations + @OneToOne(() => User, user => user.preferences) + @JoinColumn({ name: 'user_id' }) + user: User; + + @ManyToOne(() => Language) + @JoinColumn({ name: 'language_code', referencedColumnName: 'code' }) + language: Language; +} diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts new file mode 100755 index 0000000..a27854b --- /dev/null +++ b/src/entities/user.entity.ts @@ -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}`; + } +} diff --git a/src/entities/vehicle.entity.ts b/src/entities/vehicle.entity.ts new file mode 100644 index 0000000..fd67b74 --- /dev/null +++ b/src/entities/vehicle.entity.ts @@ -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[] | 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 | null; + + @ApiProperty({ description: 'Maintenance records' }) + @Column({ name: 'maintenance_records', type: 'jsonb', nullable: true }) + maintenanceRecords: Record[] | 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; +} diff --git a/src/entities/wearable-device.entity.ts b/src/entities/wearable-device.entity.ts new file mode 100644 index 0000000..d9e1569 --- /dev/null +++ b/src/entities/wearable-device.entity.ts @@ -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; +} diff --git a/src/main.ts b/src/main.ts new file mode 100755 index 0000000..ea319ca --- /dev/null +++ b/src/main.ts @@ -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('app.name') || 'Karibeo API') + .setDescription(configService.get('app.description') || 'Tourism API') + .setVersion(configService.get('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(); diff --git a/src/modules/ai-generator/ai-generator.controller.ts b/src/modules/ai-generator/ai-generator.controller.ts new file mode 100644 index 0000000..ab38051 --- /dev/null +++ b/src/modules/ai-generator/ai-generator.controller.ts @@ -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'], + }, + ], + }; + } +} diff --git a/src/modules/ai-generator/ai-generator.module.ts b/src/modules/ai-generator/ai-generator.module.ts new file mode 100644 index 0000000..5679a58 --- /dev/null +++ b/src/modules/ai-generator/ai-generator.module.ts @@ -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 {} diff --git a/src/modules/ai-generator/ai-generator.service.ts b/src/modules/ai-generator/ai-generator.service.ts new file mode 100644 index 0000000..004422f --- /dev/null +++ b/src/modules/ai-generator/ai-generator.service.ts @@ -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, + @InjectRepository(PlaceOfInterest) + private readonly placeRepository: Repository, + @InjectRepository(Establishment) + private readonly establishmentRepository: Repository, + 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 { + 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 { + 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 { + 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 { + 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']; + } +} diff --git a/src/modules/ai-generator/dto/generate-content.dto.ts b/src/modules/ai-generator/dto/generate-content.dto.ts new file mode 100644 index 0000000..be6804e --- /dev/null +++ b/src/modules/ai-generator/dto/generate-content.dto.ts @@ -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; +} diff --git a/src/modules/ai-generator/dto/improve-content.dto.ts b/src/modules/ai-generator/dto/improve-content.dto.ts new file mode 100644 index 0000000..d100ddf --- /dev/null +++ b/src/modules/ai-generator/dto/improve-content.dto.ts @@ -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; +} diff --git a/src/modules/ai-guide/ai-guide.controller.ts b/src/modules/ai-guide/ai-guide.controller.ts new file mode 100644 index 0000000..f162519 --- /dev/null +++ b/src/modules/ai-guide/ai-guide.controller.ts @@ -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); + } +} diff --git a/src/modules/ai-guide/ai-guide.module.ts b/src/modules/ai-guide/ai-guide.module.ts new file mode 100644 index 0000000..d97039f --- /dev/null +++ b/src/modules/ai-guide/ai-guide.module.ts @@ -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 {} diff --git a/src/modules/ai-guide/ai-guide.service.ts b/src/modules/ai-guide/ai-guide.service.ts new file mode 100644 index 0000000..d05be81 --- /dev/null +++ b/src/modules/ai-guide/ai-guide.service.ts @@ -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, + @InjectRepository(ARContent) + private readonly arContentRepository: Repository, + @InjectRepository(PlaceOfInterest) + private readonly placeRepository: Repository, + 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 { + // 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 { + 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 { + // 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 { + // 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): Promise { + const interaction = this.interactionRepository.create(interactionData); + await this.interactionRepository.save(interaction); + } + + // AR CONTENT MANAGEMENT + async getNearbyARContent(queryDto: ARContentQueryDto): Promise { + // 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 { + 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) })), + }; + } +} diff --git a/src/modules/ai-guide/dto/ai-query.dto.ts b/src/modules/ai-guide/dto/ai-query.dto.ts new file mode 100644 index 0000000..f5c1d20 --- /dev/null +++ b/src/modules/ai-guide/dto/ai-query.dto.ts @@ -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; +} diff --git a/src/modules/ai-guide/dto/ar-content-query.dto.ts b/src/modules/ai-guide/dto/ar-content-query.dto.ts new file mode 100644 index 0000000..65c2495 --- /dev/null +++ b/src/modules/ai-guide/dto/ar-content-query.dto.ts @@ -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; +} diff --git a/src/modules/analytics/analytics.controller.ts b/src/modules/analytics/analytics.controller.ts new file mode 100755 index 0000000..99c9c92 --- /dev/null +++ b/src/modules/analytics/analytics.controller.ts @@ -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(); + } +} diff --git a/src/modules/analytics/analytics.module.ts b/src/modules/analytics/analytics.module.ts new file mode 100755 index 0000000..de6f137 --- /dev/null +++ b/src/modules/analytics/analytics.module.ts @@ -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 {} diff --git a/src/modules/analytics/analytics.service.ts b/src/modules/analytics/analytics.service.ts new file mode 100755 index 0000000..4431d52 --- /dev/null +++ b/src/modules/analytics/analytics.service.ts @@ -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, + ) {} + + async createReview(createReviewDto: CreateReviewDto): Promise { + // 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; + }> { + 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 = {}; + 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, + }; + } +} diff --git a/src/modules/analytics/dto/create-review.dto.ts b/src/modules/analytics/dto/create-review.dto.ts new file mode 100755 index 0000000..1c9fae6 --- /dev/null +++ b/src/modules/analytics/dto/create-review.dto.ts @@ -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; +} diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts new file mode 100755 index 0000000..c9c8382 --- /dev/null +++ b/src/modules/auth/auth.controller.ts @@ -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 { + 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 { + 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 { + 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 { + return req.user; + } +} \ No newline at end of file diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts new file mode 100755 index 0000000..054813c --- /dev/null +++ b/src/modules/auth/auth.module.ts @@ -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('JWT_SECRET'), + signOptions: { + expiresIn: configService.get('JWT_EXPIRES_IN') || '24h', + }, + }), + inject: [ConfigService], + }), + ], + controllers: [AuthController], + providers: [AuthService, JwtStrategy], + exports: [AuthService, JwtStrategy, PassportModule], +}) +export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts new file mode 100755 index 0000000..d5bc701 --- /dev/null +++ b/src/modules/auth/auth.service.ts @@ -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, + @InjectRepository(Role) + private readonly roleRepository: Repository, + private readonly jwtService: JwtService, + ) {} + + async register(registerDto: RegisterDto): Promise { + 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 { + 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 { + return this.userRepository.findOne({ + where: { id: userId, isActive: true }, + relations: ['country', 'role', 'preferredLanguageEntity'], + }); + } + + async refresh(refreshToken: string): Promise { + 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 { + 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); + } +} \ No newline at end of file diff --git a/src/modules/auth/dto/auth-response.dto.ts b/src/modules/auth/dto/auth-response.dto.ts new file mode 100755 index 0000000..45abe50 --- /dev/null +++ b/src/modules/auth/dto/auth-response.dto.ts @@ -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; +} diff --git a/src/modules/auth/dto/login.dto.ts b/src/modules/auth/dto/login.dto.ts new file mode 100755 index 0000000..1ecc827 --- /dev/null +++ b/src/modules/auth/dto/login.dto.ts @@ -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; +} diff --git a/src/modules/auth/dto/register.dto.ts b/src/modules/auth/dto/register.dto.ts new file mode 100755 index 0000000..d537029 --- /dev/null +++ b/src/modules/auth/dto/register.dto.ts @@ -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; +} diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts new file mode 100755 index 0000000..f3d481c --- /dev/null +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -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('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; + } +} diff --git a/src/modules/availability-management/availability-management.controller.ts b/src/modules/availability-management/availability-management.controller.ts new file mode 100644 index 0000000..bccbf56 --- /dev/null +++ b/src/modules/availability-management/availability-management.controller.ts @@ -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, +}); +} +} diff --git a/src/modules/availability-management/availability-management.module.ts b/src/modules/availability-management/availability-management.module.ts new file mode 100644 index 0000000..3b36a91 --- /dev/null +++ b/src/modules/availability-management/availability-management.module.ts @@ -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 {} diff --git a/src/modules/availability-management/availability-management.service.ts b/src/modules/availability-management/availability-management.service.ts new file mode 100644 index 0000000..01af247 --- /dev/null +++ b/src/modules/availability-management/availability-management.service.ts @@ -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, + ) {} + + /** + * Retrieves availability for a given resource within a date range. + */ + async getAvailability(queryDto: GetAvailabilityDto): Promise { + 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 { + 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): 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 { + 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 { + 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); + } +} diff --git a/src/modules/availability-management/dto/get-availability.dto.ts b/src/modules/availability-management/dto/get-availability.dto.ts new file mode 100644 index 0000000..15f182a --- /dev/null +++ b/src/modules/availability-management/dto/get-availability.dto.ts @@ -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; +} diff --git a/src/modules/availability-management/dto/update-availability.dto.ts b/src/modules/availability-management/dto/update-availability.dto.ts new file mode 100644 index 0000000..4235187 --- /dev/null +++ b/src/modules/availability-management/dto/update-availability.dto.ts @@ -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; +@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; +} diff --git a/src/modules/channel-management/channel-management.controller.ts b/src/modules/channel-management/channel-management.controller.ts new file mode 100644 index 0000000..3823f95 --- /dev/null +++ b/src/modules/channel-management/channel-management.controller.ts @@ -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); + } +} diff --git a/src/modules/channel-management/channel-management.module.ts b/src/modules/channel-management/channel-management.module.ts new file mode 100644 index 0000000..8bf351c --- /dev/null +++ b/src/modules/channel-management/channel-management.module.ts @@ -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 {} diff --git a/src/modules/channel-management/channel-management.service.ts b/src/modules/channel-management/channel-management.service.ts new file mode 100644 index 0000000..9c79caf --- /dev/null +++ b/src/modules/channel-management/channel-management.service.ts @@ -0,0 +1,171 @@ +import { Injectable, NotFoundException, BadRequestException, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Channel } from '../../entities/channel.entity'; +import { CreateChannelDto, ChannelType } from './dto/create-channel.dto'; +import { UpdateChannelDto } from './dto/update-channel.dto'; +import { NotificationsService } from '../notifications/notifications.service'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { NotificationType, NotificationCategory } from '../notifications/dto/create-notification.dto'; +import { ConnectChannelDto } from './dto/connect-channel.dto'; + +@Injectable() +export class ChannelManagementService { + private readonly logger = new Logger(ChannelManagementService.name); + + constructor( + @InjectRepository(Channel) + private readonly channelRepository: Repository, + private readonly notificationsService: NotificationsService, + ) {} + + async createChannel(createChannelDto: CreateChannelDto): Promise { + const channel = this.channelRepository.create({ + ...createChannelDto, + status: 'disconnected', + lastSync: null, + propertiesSynced: 0, + lastError: null, + isActive: true, + } as Partial); // Cast explícito + return this.channelRepository.save(channel); + } + + async findAllChannels(): Promise { + return this.channelRepository.find(); + } + + async findChannelById(id: string): Promise { + const channel = await this.channelRepository.findOne({ where: { id } }); + if (!channel) { + throw new NotFoundException(`Channel with ID "${id}" not found.`); + } + return channel; + } + + async updateChannel(id: string, updateChannelDto: UpdateChannelDto): Promise { + const channel = await this.findChannelById(id); + await this.channelRepository.update(id, updateChannelDto as Partial); // Cast explícito + return this.findChannelById(id); + } + + async deleteChannel(id: string): Promise { + const result = await this.channelRepository.delete(id); + if (result.affected === 0) { + throw new NotFoundException(`Channel with ID "${id}" not found.`); + } + } + + async connectChannel(connectChannelDto: ConnectChannelDto): Promise { + let channel = await this.channelRepository.findOne({ + where: { + name: connectChannelDto.name, + provider: connectChannelDto.provider, + type: connectChannelDto.type, + }, + }); + + if (channel) { + channel.credentials = connectChannelDto.credentials; + channel.status = 'connected'; + channel.lastError = null; + channel.isActive = true; + await this.channelRepository.save(channel); + } else { + channel = this.channelRepository.create({ + name: connectChannelDto.name, + type: connectChannelDto.type, + provider: connectChannelDto.provider, + credentials: connectChannelDto.credentials, + status: 'connected', + isActive: true, + } as Partial); // Cast explícito + await this.channelRepository.save(channel); + } + + await this.syncChannel(channel.id); + + return channel; + } + + async disconnectChannel(id: string): Promise { + const channel = await this.findChannelById(id); + channel.status = 'disconnected'; + channel.isActive = false; + channel.lastError = 'Manually disconnected'; + return this.channelRepository.save(channel); + } + + async syncChannel(id: string): Promise { + this.logger.log(`Initiating manual sync for channel ID: ${id}`); + const channel = await this.findChannelById(id); + + try { + channel.status = 'syncing'; + channel.lastError = null; + await this.channelRepository.save(channel); + + const syncedPropertiesCount = Math.floor(Math.random() * 10) + 1; + + channel.lastSync = new Date(); + channel.status = 'connected'; + channel.propertiesSynced = syncedPropertiesCount; + + await this.channelRepository.save(channel); + + await this.notificationsService.createNotification({ + userId: 'system', + type: NotificationType.PUSH, + category: NotificationCategory.SYSTEM, + title: `Channel Sync Success: ${channel.name}`, + message: `Synchronization for ${channel.name} completed successfully. ${syncedPropertiesCount} properties updated.`, + data: { channelId: channel.id, status: 'success' }, + }); + + this.logger.log(`Channel ID ${id} synced successfully. Synced ${syncedPropertiesCount} properties.`); + return channel; + + } catch (error: any) { + this.logger.error(`Error syncing channel ID ${id}: ${error.message}`, error.stack); + channel.status = 'error'; + channel.lastError = error.message; + await this.channelRepository.save(channel); + + await this.notificationsService.createNotification({ + userId: 'system', + type: NotificationType.PUSH, + category: NotificationCategory.ERROR, + title: `Channel Sync Failed: ${channel.name}`, + message: `Synchronization for ${channel.name} failed: ${error.message}.`, + data: { channelId: channel.id, status: 'failed', error: error.message }, + }); + + throw new BadRequestException(`Failed to synchronize channel: ${error.message}`); + } + } + + // Automatic synchronization every 'syncFrequency' hours for active channels + // @Cron(CronExpression.EVERY_HOUR) // Example: run every hour + async handleAutomaticChannelSync() { + this.logger.log('Running automatic channel synchronization...'); + const activeChannels = await this.channelRepository.find({ + where: { autoSync: true, isActive: true, status: 'connected' }, + }); + + for (const channel of activeChannels) { + if (channel.lastSync) { + const nextSyncTime = new Date(channel.lastSync.getTime() + channel.syncFrequency * 3600 * 1000); + if (new Date() < nextSyncTime) { + continue; + } + } + + this.logger.log(`Attempting automatic sync for channel: ${channel.name} (ID: ${channel.id})`); + try { + await this.syncChannel(channel.id); + } catch (error) { + this.logger.error(`Automatic sync failed for channel ${channel.name}: ${error.message}`); + } + } + } +} diff --git a/src/modules/channel-management/dto/connect-channel.dto.ts b/src/modules/channel-management/dto/connect-channel.dto.ts new file mode 100644 index 0000000..4566074 --- /dev/null +++ b/src/modules/channel-management/dto/connect-channel.dto.ts @@ -0,0 +1,29 @@ +import { IsString, IsNotEmpty, IsObject, IsOptional } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { ChannelType } from './create-channel.dto'; + +export class ConnectChannelDto { + @ApiProperty({ description: 'Channel name', example: 'Booking.com' }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty({ description: 'Channel type', enum: ChannelType }) + @IsNotEmpty() + type: ChannelType; + + @ApiProperty({ description: 'Channel provider', example: 'booking' }) + @IsString() + @IsNotEmpty() + provider: string; + + @ApiProperty({ description: 'API credentials for the channel (e.g., API Key, Secret)' }) + @IsObject() + @IsNotEmpty() + credentials: Record; + + @ApiProperty({ description: 'ID of the establishment to connect (optional, for specific mappings)' }) + @IsString() + @IsOptional() + establishmentId?: string; +} diff --git a/src/modules/channel-management/dto/create-channel.dto.ts b/src/modules/channel-management/dto/create-channel.dto.ts new file mode 100644 index 0000000..ae8e7d8 --- /dev/null +++ b/src/modules/channel-management/dto/create-channel.dto.ts @@ -0,0 +1,40 @@ +import { IsString, IsNotEmpty, IsEnum, IsOptional, IsNumber, Min, IsBoolean, IsObject } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +export enum ChannelType { +HOTEL = 'hotel', +RESTAURANT = 'restaurant', +VEHICLE = 'vehicle', +FLIGHT = 'flight', +TOUR = 'tour', +} +export class CreateChannelDto { +@ApiProperty({ description: 'Channel name', example: 'Booking.com' }) +@IsString() +@IsNotEmpty() +name: string; +@ApiProperty({ description: 'Channel type', enum: ChannelType }) +@IsEnum(ChannelType) +@IsNotEmpty() +type: ChannelType; +@ApiProperty({ description: 'Channel provider', example: 'booking' }) +@IsString() +@IsNotEmpty() +provider: string; +@ApiProperty({ description: 'API credentials for the channel (e.g., API Key, Secret)' }) +@IsObject() +@IsNotEmpty() +credentials: Record; +@ApiPropertyOptional({ description: 'Channel configuration settings' }) +@IsObject() +@IsOptional() +config?: Record; +@ApiPropertyOptional({ description: 'Sync frequency in hours', example: 24 }) +@IsNumber() +@Min(1) +@IsOptional() +syncFrequency?: number; +@ApiPropertyOptional({ description: 'Enable automatic synchronization', example: true }) +@IsBoolean() +@IsOptional() +autoSync?: boolean; +} diff --git a/src/modules/channel-management/dto/update-channel.dto.ts b/src/modules/channel-management/dto/update-channel.dto.ts new file mode 100644 index 0000000..04e7007 --- /dev/null +++ b/src/modules/channel-management/dto/update-channel.dto.ts @@ -0,0 +1,3 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateChannelDto } from './create-channel.dto'; +export class UpdateChannelDto extends PartialType(CreateChannelDto) {} diff --git a/src/modules/commerce/commerce.controller.ts b/src/modules/commerce/commerce.controller.ts new file mode 100755 index 0000000..96213cd --- /dev/null +++ b/src/modules/commerce/commerce.controller.ts @@ -0,0 +1,166 @@ +import { + Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, Request, ParseBoolPipe +} from '@nestjs/common'; +import { + ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam +} from '@nestjs/swagger'; +import { CommerceService } from './commerce.service'; +import { CreateEstablishmentDto } from './dto/create-establishment.dto'; +import { UpdateEstablishmentDto } from './dto/update-establishment.dto'; +import { CreateReservationDto } from './dto/create-reservation.dto'; +import { UpdateReservationDto } from './dto/update-reservation.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; +import { Establishment } from '../../entities/establishment.entity'; +import { Reservation } from '../../entities/reservation.entity'; + +@ApiTags('Commerce') +@Controller('commerce') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class CommerceController { + constructor(private readonly commerceService: CommerceService) {} + + // ESTABLISHMENTS ENDPOINTS + @Post('establishments') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Register a new establishment' }) + @ApiResponse({ status: 201, description: 'Establishment created successfully', type: Establishment }) + createEstablishment(@Body() createEstablishmentDto: CreateEstablishmentDto) { + return this.commerceService.createEstablishment(createEstablishmentDto); + } + + @Get('establishments') + @ApiOperation({ summary: 'Get all establishments with filters' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ name: 'type', required: false, type: String, description: 'Filter by type (restaurant, hotel, store)' }) + @ApiQuery({ name: 'category', required: false, type: String, description: 'Filter by category' }) + @ApiQuery({ name: 'isVerified', required: false, type: Boolean, description: 'Filter by verification status' }) + findAllEstablishments( + @Query('page') page?: number, + @Query('limit') limit?: number, + @Query('type') type?: string, + @Query('category') category?: string, + @Query('isVerified', ParseBoolPipe) isVerified?: boolean, + ) { + return this.commerceService.findAllEstablishments(page, limit, type, category, isVerified); + } + + @Get('establishments/search') + @ApiOperation({ summary: 'Search establishments by name or description' }) + @ApiQuery({ name: 'q', type: String, description: 'Search query' }) + @ApiQuery({ name: 'type', required: false, type: String, description: 'Filter by type' }) + searchEstablishments( + @Query('q') query: string, + @Query('type') type?: string, + ) { + return this.commerceService.searchEstablishments(query, type); + } + + @Get('establishments/:id') + @ApiOperation({ summary: 'Get establishment by ID' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ status: 200, type: Establishment }) + findOneEstablishment(@Param('id') id: string) { + return this.commerceService.findOneEstablishment(id); + } + + @Patch('establishments/:id') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Update establishment' }) + @ApiParam({ name: 'id', type: 'string' }) + updateEstablishment( + @Param('id') id: string, + @Body() updateEstablishmentDto: UpdateEstablishmentDto, + @Request() req, + ) { + return this.commerceService.updateEstablishment(id, updateEstablishmentDto, req.user.id); + } + + @Delete('establishments/:id') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Deactivate establishment' }) + @ApiParam({ name: 'id', type: 'string' }) + removeEstablishment(@Param('id') id: string, @Request() req) { + return this.commerceService.removeEstablishment(id, req.user.id); + } + + // RESERVATIONS ENDPOINTS + @Post('reservations') + @ApiOperation({ summary: 'Create a new reservation' }) + @ApiResponse({ status: 201, description: 'Reservation created successfully', type: Reservation }) + createReservation(@Body() createReservationDto: CreateReservationDto) { + return this.commerceService.createReservation(createReservationDto); + } + + @Get('reservations') + @ApiOperation({ summary: 'Get reservations with filters' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ name: 'userId', required: false, type: String, description: 'Filter by user ID' }) + @ApiQuery({ name: 'establishmentId', required: false, type: String, description: 'Filter by establishment ID' }) + @ApiQuery({ name: 'status', required: false, type: String, description: 'Filter by status' }) + findAllReservations( + @Query('page') page?: number, + @Query('limit') limit?: number, + @Query('userId') userId?: string, + @Query('establishmentId') establishmentId?: string, + @Query('status') status?: string, + ) { + return this.commerceService.findAllReservations(page, limit, userId, establishmentId, status); + } + + @Get('reservations/my') + @ApiOperation({ summary: 'Get current user reservations' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ name: 'status', required: false, type: String }) + getMyReservations( + @Request() req, + @Query('page') page?: number, + @Query('limit') limit?: number, + @Query('status') status?: string, + ) { + return this.commerceService.findAllReservations(page, limit, req.user.id, undefined, status); + } + + @Get('reservations/:id') + @ApiOperation({ summary: 'Get reservation by ID' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ status: 200, type: Reservation }) + findOneReservation(@Param('id') id: string) { + return this.commerceService.findOneReservation(id); + } + + @Patch('reservations/:id') + @ApiOperation({ summary: 'Update reservation' }) + @ApiParam({ name: 'id', type: 'string' }) + updateReservation( + @Param('id') id: string, + @Body() updateReservationDto: UpdateReservationDto, + @Request() req, + ) { + return this.commerceService.updateReservation(id, updateReservationDto, req.user.id); + } + + @Patch('reservations/:id/cancel') + @ApiOperation({ summary: 'Cancel reservation' }) + @ApiParam({ name: 'id', type: 'string' }) + cancelReservation(@Param('id') id: string, @Request() req) { + return this.commerceService.cancelReservation(id, req.user.id); + } + + // STATISTICS + @Get('stats') + @UseGuards(RolesGuard) + @Roles('admin') + @ApiOperation({ summary: 'Get commerce statistics (Admin only)' }) + getCommerceStats() { + return this.commerceService.getCommerceStats(); + } +} diff --git a/src/modules/commerce/commerce.module.ts b/src/modules/commerce/commerce.module.ts new file mode 100755 index 0000000..895397f --- /dev/null +++ b/src/modules/commerce/commerce.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CommerceService } from './commerce.service'; +import { CommerceController } from './commerce.controller'; +import { Establishment } from '../../entities/establishment.entity'; +import { Reservation } from '../../entities/reservation.entity'; +import { Product } from '../../entities/product.entity'; +import { HotelRoom } from '../../entities/hotel-room.entity'; +import { Transaction } from '../../entities/transaction.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + Establishment, + Reservation, + Product, + HotelRoom, + Transaction, + ]), + ], + controllers: [CommerceController], + providers: [CommerceService], + exports: [CommerceService], +}) +export class CommerceModule {} diff --git a/src/modules/commerce/commerce.service.ts b/src/modules/commerce/commerce.service.ts new file mode 100755 index 0000000..a388e12 --- /dev/null +++ b/src/modules/commerce/commerce.service.ts @@ -0,0 +1,227 @@ +import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Establishment } from '../../entities/establishment.entity'; +import { Reservation } from '../../entities/reservation.entity'; +import { Product } from '../../entities/product.entity'; +import { HotelRoom } from '../../entities/hotel-room.entity'; +import { Transaction } from '../../entities/transaction.entity'; +import { CreateEstablishmentDto } from './dto/create-establishment.dto'; +import { UpdateEstablishmentDto } from './dto/update-establishment.dto'; +import { CreateReservationDto } from './dto/create-reservation.dto'; +import { UpdateReservationDto } from './dto/update-reservation.dto'; + +@Injectable() +export class CommerceService { + constructor( + @InjectRepository(Establishment) + private readonly establishmentRepository: Repository, + @InjectRepository(Reservation) + private readonly reservationRepository: Repository, + @InjectRepository(Product) + private readonly productRepository: Repository, + @InjectRepository(HotelRoom) + private readonly hotelRoomRepository: Repository, + @InjectRepository(Transaction) + private readonly transactionRepository: Repository, + ) {} + + // Establishments CRUD + async createEstablishment(createEstablishmentDto: CreateEstablishmentDto): Promise { + const establishment = this.establishmentRepository.create(createEstablishmentDto); + return this.establishmentRepository.save(establishment); + } + + async findAllEstablishments( + page: number = 1, + limit: number = 10, + type?: string, + category?: string, + isVerified?: boolean + ): Promise<{ + establishments: Establishment[]; + total: number; + page: number; + limit: number; + }> { + const query = this.establishmentRepository.createQueryBuilder('establishment') + .leftJoinAndSelect('establishment.owner', 'owner') + .where('establishment.isActive = :active', { active: true }); + + if (type) { + query.andWhere('establishment.type = :type', { type }); + } + + if (category) { + query.andWhere('establishment.category = :category', { category }); + } + + if (isVerified !== undefined) { + query.andWhere('establishment.isVerified = :isVerified', { isVerified }); + } + + const [establishments, total] = await query + .skip((page - 1) * limit) + .take(limit) + .orderBy('establishment.rating', 'DESC') + .getManyAndCount(); + + return { establishments, total, page, limit }; + } + + async findOneEstablishment(id: string): Promise { + const establishment = await this.establishmentRepository.findOne({ + where: { id, isActive: true }, + relations: ['owner'], + }); + + if (!establishment) { + throw new NotFoundException(`Establishment with ID ${id} not found`); + } + + return establishment; + } + + async updateEstablishment(id: string, updateEstablishmentDto: UpdateEstablishmentDto, userId: string): Promise { + const establishment = await this.findOneEstablishment(id); + + // Check if user owns the establishment or is admin + if (establishment.userId !== userId) { + throw new ForbiddenException('You can only update your own establishment'); + } + + await this.establishmentRepository.update(id, updateEstablishmentDto); + return this.findOneEstablishment(id); + } + + async removeEstablishment(id: string, userId: string): Promise { + const establishment = await this.findOneEstablishment(id); + + if (establishment.userId !== userId) { + throw new ForbiddenException('You can only delete your own establishment'); + } + + await this.establishmentRepository.update(id, { isActive: false }); + } + + async searchEstablishments(query: string, type?: string): Promise { + const searchQuery = this.establishmentRepository.createQueryBuilder('establishment') + .leftJoinAndSelect('establishment.owner', 'owner') + .where('establishment.isActive = :active', { active: true }) + .andWhere('(establishment.name ILIKE :query OR establishment.description ILIKE :query)', + { query: `%${query}%` }); + + if (type) { + searchQuery.andWhere('establishment.type = :type', { type }); + } + + return searchQuery + .orderBy('establishment.rating', 'DESC') + .limit(20) + .getMany(); + } + + // Reservations CRUD + async createReservation(createReservationDto: CreateReservationDto): Promise { + const reservation = this.reservationRepository.create(createReservationDto); + return this.reservationRepository.save(reservation); + } + + async findAllReservations( + page: number = 1, + limit: number = 10, + userId?: string, + establishmentId?: string, + status?: string + ): Promise<{ + reservations: Reservation[]; + total: number; + page: number; + limit: number; + }> { + const query = this.reservationRepository.createQueryBuilder('reservation') + .leftJoinAndSelect('reservation.establishment', 'establishment') + .leftJoinAndSelect('reservation.user', 'user'); + + if (userId) { + query.andWhere('reservation.userId = :userId', { userId }); + } + + if (establishmentId) { + query.andWhere('reservation.establishmentId = :establishmentId', { establishmentId }); + } + + if (status) { + query.andWhere('reservation.status = :status', { status }); + } + + const [reservations, total] = await query + .skip((page - 1) * limit) + .take(limit) + .orderBy('reservation.createdAt', 'DESC') + .getManyAndCount(); + + return { reservations, total, page, limit }; + } + + async findOneReservation(id: string): Promise { + const reservation = await this.reservationRepository.findOne({ + where: { id }, + relations: ['establishment', 'user'], + }); + + if (!reservation) { + throw new NotFoundException(`Reservation with ID ${id} not found`); + } + + return reservation; + } + + async updateReservation(id: string, updateReservationDto: UpdateReservationDto, userId: string): Promise { + const reservation = await this.findOneReservation(id); + + // Check if user owns the reservation or the establishment + if (reservation.userId !== userId && reservation.establishment.userId !== userId) { + throw new ForbiddenException('You can only update your own reservations'); + } + + await this.reservationRepository.update(id, updateReservationDto); + return this.findOneReservation(id); + } + + async cancelReservation(id: string, userId: string): Promise { + const reservation = await this.findOneReservation(id); + + if (reservation.userId !== userId && reservation.establishment.userId !== userId) { + throw new ForbiddenException('You can only cancel your own reservations'); + } + + await this.reservationRepository.update(id, { status: 'cancelled' }); + return this.findOneReservation(id); + } + + // Statistics + async getCommerceStats(): Promise<{ + establishments: number; + reservations: number; + products: number; + revenue: number; + }> { + const [establishments, reservations, products] = await Promise.all([ + this.establishmentRepository.count({ where: { isActive: true } }), + this.reservationRepository.count(), + this.productRepository.count({ where: { isActive: true } }), + ]); + + // Calculate total revenue from completed transactions + const revenueResult = await this.transactionRepository + .createQueryBuilder('transaction') + .select('SUM(transaction.amount)', 'total') + .where('transaction.status = :status', { status: 'completed' }) + .getRawOne(); + + const revenue = parseFloat(revenueResult.total) || 0; + + return { establishments, reservations, products, revenue }; + } +} diff --git a/src/modules/commerce/dto/create-establishment.dto.ts b/src/modules/commerce/dto/create-establishment.dto.ts new file mode 100755 index 0000000..917c22f --- /dev/null +++ b/src/modules/commerce/dto/create-establishment.dto.ts @@ -0,0 +1,74 @@ +import { IsString, IsOptional, IsBoolean, IsArray, IsEmail } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateEstablishmentDto { + @ApiProperty({ description: 'Owner user ID' }) + @IsString() + userId: string; + + @ApiProperty({ description: 'Establishment type', example: 'restaurant' }) + @IsString() + type: string; + + @ApiProperty({ description: 'Establishment name', example: 'La Casita Restaurant' }) + @IsString() + name: string; + + @ApiPropertyOptional({ description: 'Description' }) + @IsOptional() + @IsString() + description?: string; + + @ApiPropertyOptional({ description: 'Category', example: 'caribbean-cuisine' }) + @IsOptional() + @IsString() + category?: string; + + @ApiPropertyOptional({ description: 'Address' }) + @IsOptional() + @IsString() + address?: string; + + @ApiPropertyOptional({ description: 'Coordinates (lat,lng)' }) + @IsOptional() + @IsString() + coordinates?: string; + + @ApiPropertyOptional({ description: 'Phone number' }) + @IsOptional() + @IsString() + phone?: string; + + @ApiPropertyOptional({ description: 'Email' }) + @IsOptional() + @IsEmail() + email?: string; + + @ApiPropertyOptional({ description: 'Website' }) + @IsOptional() + @IsString() + website?: string; + + @ApiPropertyOptional({ description: 'Business hours' }) + @IsOptional() + businessHours?: Record; + + @ApiPropertyOptional({ description: 'Images' }) + @IsOptional() + images?: Record; + + @ApiPropertyOptional({ description: 'Amenities' }) + @IsOptional() + @IsArray() + amenities?: string[]; + + @ApiPropertyOptional({ description: 'Is verified', example: false }) + @IsOptional() + @IsBoolean() + isVerified?: boolean; + + @ApiPropertyOptional({ description: 'Is active', example: true }) + @IsOptional() + @IsBoolean() + isActive?: boolean; +} diff --git a/src/modules/commerce/dto/create-reservation.dto.ts b/src/modules/commerce/dto/create-reservation.dto.ts new file mode 100755 index 0000000..c746d67 --- /dev/null +++ b/src/modules/commerce/dto/create-reservation.dto.ts @@ -0,0 +1,51 @@ +import { IsString, IsOptional, IsNumber, IsDateString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateReservationDto { + @ApiProperty({ description: 'Establishment ID' }) + @IsString() + establishmentId: string; + + @ApiProperty({ description: 'User ID' }) + @IsString() + userId: string; + + @ApiProperty({ description: 'Reservation type', example: 'room' }) + @IsString() + type: string; + + @ApiPropertyOptional({ description: 'Reference ID (room, table, etc.)' }) + @IsOptional() + @IsString() + referenceId?: string; + + @ApiPropertyOptional({ description: 'Check-in date', example: '2025-07-01' }) + @IsOptional() + @IsDateString() + checkInDate?: string; + + @ApiPropertyOptional({ description: 'Check-out date', example: '2025-07-03' }) + @IsOptional() + @IsDateString() + checkOutDate?: string; + + @ApiPropertyOptional({ description: 'Check-in time', example: '15:00' }) + @IsOptional() + @IsString() + checkInTime?: string; + + @ApiPropertyOptional({ description: 'Number of guests', example: 2 }) + @IsOptional() + @IsNumber() + guestsCount?: number; + + @ApiPropertyOptional({ description: 'Special requests' }) + @IsOptional() + @IsString() + specialRequests?: string; + + @ApiPropertyOptional({ description: 'Total amount', example: 240.00 }) + @IsOptional() + @IsNumber() + totalAmount?: number; +} diff --git a/src/modules/commerce/dto/update-establishment.dto.ts b/src/modules/commerce/dto/update-establishment.dto.ts new file mode 100755 index 0000000..c9a8389 --- /dev/null +++ b/src/modules/commerce/dto/update-establishment.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateEstablishmentDto } from './create-establishment.dto'; + +export class UpdateEstablishmentDto extends PartialType(CreateEstablishmentDto) {} diff --git a/src/modules/commerce/dto/update-reservation.dto.ts b/src/modules/commerce/dto/update-reservation.dto.ts new file mode 100755 index 0000000..cd80b20 --- /dev/null +++ b/src/modules/commerce/dto/update-reservation.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateReservationDto } from './create-reservation.dto'; + +export class UpdateReservationDto extends PartialType(CreateReservationDto) {} diff --git a/src/modules/communication/communication.module.ts b/src/modules/communication/communication.module.ts new file mode 100755 index 0000000..bccc4d9 --- /dev/null +++ b/src/modules/communication/communication.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { EmailService } from './email.service'; +import { WhatsAppService } from './whatsapp.service'; +import { WhatsAppController } from './whatsapp.controller'; + +@Module({ + controllers: [WhatsAppController], + providers: [EmailService, WhatsAppService], + exports: [EmailService, WhatsAppService], +}) +export class CommunicationModule {} diff --git a/src/modules/communication/email.service.ts b/src/modules/communication/email.service.ts new file mode 100755 index 0000000..0ed7349 --- /dev/null +++ b/src/modules/communication/email.service.ts @@ -0,0 +1,225 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as sgMail from '@sendgrid/mail'; + +interface EmailTemplate { + to: string; + subject: string; + html: string; + text?: string; + templateData?: Record; +} + +@Injectable() +export class EmailService { + constructor(private configService: ConfigService) { + const apiKey = this.configService.get('communication.sendgrid.apiKey'); + if (!apiKey) { + throw new Error('SendGrid API key is not configured'); + } + sgMail.setApiKey(apiKey); + } + + async sendEmail(emailData: EmailTemplate): Promise { + try { + const fromEmail = this.configService.get('communication.sendgrid.fromEmail') || 'noreply@karibeo.com'; + const fromName = this.configService.get('communication.sendgrid.fromName') || 'Karibeo'; + + const msg = { + to: emailData.to, + from: { + email: fromEmail, + name: fromName, + }, + subject: emailData.subject, + html: emailData.html, + text: emailData.text || this.stripHtml(emailData.html), + }; + + await sgMail.send(msg); + } catch (error) { + throw new BadRequestException(`Email sending failed: ${error.message}`); + } + } + + async sendBookingConfirmation( + email: string, + userName: string, + bookingDetails: { + establishmentName: string; + checkIn: string; + checkOut: string; + totalAmount: number; + confirmationNumber: string; + } + ): Promise { + const html = ` + + + + + + +
+
+

🏝️ Karibeo - Booking Confirmed

+
+
+

Hello ${userName}!

+

Your booking has been confirmed! Get ready for an amazing experience in the Caribbean.

+ +
+

Booking Details

+

Establishment: ${bookingDetails.establishmentName}

+

Check-in: ${bookingDetails.checkIn}

+

Check-out: ${bookingDetails.checkOut}

+

Total Amount: $${bookingDetails.totalAmount}

+

Confirmation Number: ${bookingDetails.confirmationNumber}

+
+ +

We're excited to welcome you to the Dominican Republic/Puerto Rico!

+
+ +
+ + + `; + + await this.sendEmail({ + to: email, + subject: `🏝️ Booking Confirmed - ${bookingDetails.establishmentName}`, + html, + }); + } + + async sendSecurityAlert( + email: string, + userName: string, + alertDetails: { + type: string; + location: string; + timestamp: string; + message: string; + } + ): Promise { + const html = ` + + + + + + +
+
+

🚨 Karibeo Security Alert

+
+
+

Hello ${userName},

+

This is an important security notification regarding your current location.

+ +
+

Alert Details

+

Type: ${alertDetails.type}

+

Location: ${alertDetails.location}

+

Time: ${alertDetails.timestamp}

+

Message: ${alertDetails.message}

+
+ +

For immediate assistance, contact POLITUR or use the emergency button in your Karibeo app.

+
+ +
+ + + `; + + await this.sendEmail({ + to: email, + subject: `🚨 Security Alert - ${alertDetails.type}`, + html, + }); + } + + async sendWelcomeEmail(email: string, userName: string): Promise { + const html = ` + + + + + + +
+
+

🏝️ Welcome to Karibeo!

+

Your Caribbean Adventure Starts Here

+
+
+

Hello ${userName}!

+

Welcome to Karibeo, your ultimate companion for exploring the Dominican Republic and Puerto Rico!

+ +
+
+

🏨 Book Accommodations

+

Find and book the perfect place to stay

+
+
+

🗺️ Discover Places

+

Explore hidden gems and popular attractions

+
+
+

👨‍🏫 Tour Guides

+

Connect with local expert guides

+
+
+

🚗 Transportation

+

Safe and reliable taxi services

+
+
+ +

Start exploring now and make unforgettable memories in the Caribbean!

+
+ +
+ + + `; + + await this.sendEmail({ + to: email, + subject: '🏝️ Welcome to Karibeo - Your Caribbean Adventure Awaits!', + html, + }); + } + + private stripHtml(html: string): string { + return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); + } +} diff --git a/src/modules/communication/whatsapp.controller.ts b/src/modules/communication/whatsapp.controller.ts new file mode 100755 index 0000000..8960040 --- /dev/null +++ b/src/modules/communication/whatsapp.controller.ts @@ -0,0 +1,44 @@ +import { Controller, Post, Get, Body, Query, Res, HttpStatus } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger'; +import { Response } from 'express'; +import { WhatsAppService } from './whatsapp.service'; + +@ApiTags('WhatsApp') +@Controller('whatsapp') +export class WhatsAppController { + constructor(private readonly whatsAppService: WhatsAppService) {} + + @Get('webhook') + @ApiOperation({ summary: 'WhatsApp webhook verification' }) + @ApiQuery({ name: 'hub.mode', description: 'Webhook mode' }) + @ApiQuery({ name: 'hub.verify_token', description: 'Verification token' }) + @ApiQuery({ name: 'hub.challenge', description: 'Challenge string' }) + async verifyWebhook( + @Query('hub.mode') mode: string, + @Query('hub.verify_token') token: string, + @Query('hub.challenge') challenge: string, + @Res() res: Response, + ) { + const verification = await this.whatsAppService.verifyWebhook(mode, token, challenge); + + if (verification) { + res.status(HttpStatus.OK).send(verification); + } else { + res.status(HttpStatus.FORBIDDEN).send('Forbidden'); + } + } + + @Post('webhook') + @ApiOperation({ summary: 'WhatsApp webhook endpoint' }) + @ApiResponse({ status: 200, description: 'Webhook processed successfully' }) + async handleWebhook(@Body() body: any, @Res() res: Response) { + await this.whatsAppService.handleIncomingMessage(body); + res.status(HttpStatus.OK).send('OK'); + } + + @Post('send-test') + @ApiOperation({ summary: 'Send test WhatsApp message' }) + async sendTestMessage(@Body() body: { to: string; message: string }) { + return this.whatsAppService.sendTextMessage(body.to, body.message); + } +} diff --git a/src/modules/communication/whatsapp.service.ts b/src/modules/communication/whatsapp.service.ts new file mode 100755 index 0000000..1677b5b --- /dev/null +++ b/src/modules/communication/whatsapp.service.ts @@ -0,0 +1,232 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import axios from 'axios'; + +interface WhatsAppMessage { + to: string; + type: 'text' | 'template' | 'location' | 'image'; + content: any; +} + +@Injectable() +export class WhatsAppService { + private apiUrl: string; + private accessToken: string; + + constructor(private configService: ConfigService) { + this.apiUrl = this.configService.get('communication.whatsapp.apiUrl') || ''; + this.accessToken = this.configService.get('communication.whatsapp.accessToken') || ''; + + if (!this.apiUrl || !this.accessToken) { + console.warn('WhatsApp API credentials not configured'); + } + } + + async sendTextMessage(to: string, message: string): Promise { + try { + const payload = { + messaging_product: 'whatsapp', + to: this.formatPhoneNumber(to), + type: 'text', + text: { + body: message, + }, + }; + + await this.makeRequest(payload); + } catch (error) { + throw new BadRequestException(`WhatsApp message failed: ${error.message}`); + } + } + + async sendBookingConfirmation( + to: string, + bookingDetails: { + userName: string; + establishmentName: string; + checkIn: string; + checkOut: string; + confirmationNumber: string; + } + ): Promise { + const message = `🏝️ *Karibeo - Booking Confirmed!* + +Hello ${bookingDetails.userName}! + +Your Caribbean adventure is confirmed! ✅ + +📍 *${bookingDetails.establishmentName}* +📅 Check-in: ${bookingDetails.checkIn} +📅 Check-out: ${bookingDetails.checkOut} +🎫 Confirmation: ${bookingDetails.confirmationNumber} + +We can't wait to welcome you to paradise! 🌴 + +Need help? Just reply to this message.`; + + await this.sendTextMessage(to, message); + } + + async sendSecurityAlert( + to: string, + alertDetails: { + userName: string; + type: string; + location: string; + timestamp: string; + } + ): Promise { + const message = `🚨 *KARIBEO SECURITY ALERT* + +${alertDetails.userName}, this is an important safety notification. + +⚠️ Alert Type: ${alertDetails.type} +📍 Location: ${alertDetails.location} +🕐 Time: ${alertDetails.timestamp} + +For immediate assistance: +- Contact POLITUR +- Use emergency button in Karibeo app +- Reply to this message + +Your safety is our priority! 🛡️`; + + await this.sendTextMessage(to, message); + } + + async sendTaxiUpdate( + to: string, + taxiDetails: { + driverName: string; + vehicleInfo: string; + estimatedArrival: string; + trackingLink?: string; + } + ): Promise { + const message = `🚗 *Your Karibeo Taxi is on the way!* + +Driver: ${taxiDetails.driverName} +Vehicle: ${taxiDetails.vehicleInfo} +ETA: ${taxiDetails.estimatedArrival} + +${taxiDetails.trackingLink ? `Track your ride: ${taxiDetails.trackingLink}` : ''} + +Safe travels! 🛣️`; + + await this.sendTextMessage(to, message); + } + + async sendLocationMessage(to: string, latitude: number, longitude: number, name?: string): Promise { + try { + const payload = { + messaging_product: 'whatsapp', + to: this.formatPhoneNumber(to), + type: 'location', + location: { + latitude, + longitude, + name: name || 'Shared Location', + }, + }; + + await this.makeRequest(payload); + } catch (error) { + throw new BadRequestException(`WhatsApp location message failed: ${error.message}`); + } + } + + async sendImageMessage(to: string, imageUrl: string, caption?: string): Promise { + try { + const payload = { + messaging_product: 'whatsapp', + to: this.formatPhoneNumber(to), + type: 'image', + image: { + link: imageUrl, + caption: caption || '', + }, + }; + + await this.makeRequest(payload); + } catch (error) { + throw new BadRequestException(`WhatsApp image message failed: ${error.message}`); + } + } + + async verifyWebhook(mode: string, token: string, challenge: string): Promise { + const verifyToken = this.configService.get('communication.whatsapp.verifyToken'); + + if (mode === 'subscribe' && token === verifyToken) { + return challenge; + } + + return null; + } + + async handleIncomingMessage(body: any): Promise { + try { + if (body.entry && body.entry[0] && body.entry[0].changes) { + const changes = body.entry[0].changes[0]; + + if (changes.value && changes.value.messages) { + const message = changes.value.messages[0]; + const from = message.from; + const messageBody = message.text?.body || ''; + + // Handle incoming message logic here + console.log(`Received WhatsApp message from ${from}: ${messageBody}`); + + // Auto-reply with help information + await this.sendHelpMessage(from); + } + } + } catch (error) { + console.error('Error processing WhatsApp webhook:', error); + } + } + + private async sendHelpMessage(to: string): Promise { + const helpMessage = `👋 *Hello from Karibeo!* + +Thank you for contacting us! Here's how we can help: + +🏨 *Bookings*: Reply "booking" for reservation help +🚨 *Emergency*: Reply "emergency" for immediate assistance +🗺️ *Places*: Reply "places" for tourist attractions +🚗 *Transport*: Reply "taxi" for transportation help + +Or visit our app for instant assistance! 📱 + +How can we make your Caribbean experience amazing today? 🌴`; + + await this.sendTextMessage(to, helpMessage); + } + + private formatPhoneNumber(phone: string): string { + // Remove any non-digit characters and ensure it starts with country code + const cleaned = phone.replace(/\D/g, ''); + + // If it doesn't start with a country code, assume it's US/DR/PR (+1) + if (!cleaned.startsWith('1') && cleaned.length === 10) { + return `1${cleaned}`; + } + + return cleaned; + } + + private async makeRequest(payload: any): Promise { + try { + const response = await axios.post(`${this.apiUrl}/messages`, payload, { + headers: { + 'Authorization': `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json', + }, + }); + + return response.data; + } catch (error) { + console.error('WhatsApp API Error:', error.response?.data || error.message); + throw error; + } + } +} diff --git a/src/modules/finance/commissions/commissions.controller.spec.ts b/src/modules/finance/commissions/commissions.controller.spec.ts new file mode 100644 index 0000000..4f3abb7 --- /dev/null +++ b/src/modules/finance/commissions/commissions.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CommissionsController } from './commissions.controller'; + +describe('CommissionsController', () => { + let controller: CommissionsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [CommissionsController], + }).compile(); + + controller = module.get(CommissionsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/modules/finance/commissions/commissions.controller.ts b/src/modules/finance/commissions/commissions.controller.ts new file mode 100644 index 0000000..3c2e149 --- /dev/null +++ b/src/modules/finance/commissions/commissions.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('commissions') +export class CommissionsController {} diff --git a/src/modules/finance/dashboard/dashboard.controller.spec.ts b/src/modules/finance/dashboard/dashboard.controller.spec.ts new file mode 100644 index 0000000..d3f16de --- /dev/null +++ b/src/modules/finance/dashboard/dashboard.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DashboardController } from './dashboard.controller'; + +describe('DashboardController', () => { + let controller: DashboardController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [DashboardController], + }).compile(); + + controller = module.get(DashboardController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/modules/finance/dashboard/dashboard.controller.ts b/src/modules/finance/dashboard/dashboard.controller.ts new file mode 100644 index 0000000..7ad17c5 --- /dev/null +++ b/src/modules/finance/dashboard/dashboard.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('dashboard') +export class DashboardController {} diff --git a/src/modules/finance/finance.controller.spec.ts b/src/modules/finance/finance.controller.spec.ts new file mode 100644 index 0000000..78b12d4 --- /dev/null +++ b/src/modules/finance/finance.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FinanceController } from './finance.controller'; + +describe('FinanceController', () => { + let controller: FinanceController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [FinanceController], + }).compile(); + + controller = module.get(FinanceController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/modules/finance/finance.controller.ts b/src/modules/finance/finance.controller.ts new file mode 100644 index 0000000..7f2b925 --- /dev/null +++ b/src/modules/finance/finance.controller.ts @@ -0,0 +1,234 @@ +import { + Controller, + Get, + Post, + Patch, + Body, + Param, + Query, + UseGuards, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiQuery, +} from '@nestjs/swagger'; +import { FinanceService } from './finance.service'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; + +@ApiTags('Finance Management') +@Controller('finance') +@UseGuards(JwtAuthGuard, RolesGuard) +@ApiBearerAuth('JWT-auth') +export class FinanceController { + constructor(private readonly financeService: FinanceService) {} + + // Dashboard Endpoints + @Get('dashboard/overview') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Get financial dashboard overview' }) + @ApiQuery({ name: 'period', required: false, enum: ['week', 'month', 'quarter', 'year'] }) + async getDashboardOverview(@Query('period') period = 'month') { + return this.financeService.getDashboardOverview(period); + } + + @Get('dashboard/revenue-by-period') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Get revenue breakdown by time period' }) + @ApiQuery({ name: 'startDate', required: true }) + @ApiQuery({ name: 'endDate', required: true }) + @ApiQuery({ name: 'groupBy', required: false, enum: ['day', 'week', 'month'] }) + async getRevenueByPeriod( + @Query('startDate') startDate: string, + @Query('endDate') endDate: string, + @Query('groupBy') groupBy = 'day', + ) { + return this.financeService.getRevenueByPeriod(startDate, endDate, groupBy); + } + + @Get('dashboard/commission-summary') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Get commission summary by service type' }) + @ApiQuery({ name: 'period', required: false }) + async getCommissionSummary(@Query('period') period = 'month') { + return this.financeService.getCommissionSummary(period); + } + + // Transactions Endpoints + @Get('transactions') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Get all financial transactions' }) + @ApiQuery({ name: 'page', required: false }) + @ApiQuery({ name: 'limit', required: false }) + @ApiQuery({ name: 'serviceType', required: false }) + @ApiQuery({ name: 'status', required: false }) + @ApiQuery({ name: 'startDate', required: false }) + @ApiQuery({ name: 'endDate', required: false }) + async getTransactions( + @Query('page') page = 1, + @Query('limit') limit = 20, + @Query('serviceType') serviceType?: string, + @Query('status') status?: string, + @Query('startDate') startDate?: string, + @Query('endDate') endDate?: string, + ) { + return this.financeService.getTransactions({ + page: Number(page), + limit: Number(limit), + serviceType, + status, + startDate, + endDate, + }); + } + + @Get('transactions/:id') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Get transaction details' }) + async getTransactionDetails(@Param('id') id: string) { + return this.financeService.getTransactionDetails(id); + } + + @Get('transactions/by-merchant/:merchantId') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Get transactions by merchant' }) + async getTransactionsByMerchant(@Param('merchantId') merchantId: string) { + return this.financeService.getTransactionsByMerchant(merchantId); + } + + // Commissions Endpoints + @Get('commissions/rates') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Get current commission rates' }) + async getCommissionRates() { + return this.financeService.getCommissionRates(); + } + + @Post('commissions/rates') + @Roles('super_admin') + @ApiOperation({ summary: 'Create new commission rate' }) + async createCommissionRate(@Body() rateData: any) { + return this.financeService.createCommissionRate(rateData); + } + + @Patch('commissions/rates/:serviceType') + @Roles('super_admin') + @ApiOperation({ summary: 'Update commission rate for service type' }) + async updateCommissionRate( + @Param('serviceType') serviceType: string, + @Body() updateData: any, + ) { + return this.financeService.updateCommissionRate(serviceType, updateData); + } + + @Get('commissions/history') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Get commission rate change history' }) + async getCommissionHistory() { + return this.financeService.getCommissionHistory(); + } + + // Reports Endpoints + @Get('reports/financial-summary') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Get comprehensive financial summary' }) + @ApiQuery({ name: 'startDate', required: true }) + @ApiQuery({ name: 'endDate', required: true }) + async getFinancialSummary( + @Query('startDate') startDate: string, + @Query('endDate') endDate: string, + ) { + return this.financeService.getFinancialSummary(startDate, endDate); + } + + @Get('reports/merchant-performance') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Get merchant performance report' }) + @ApiQuery({ name: 'period', required: false }) + @ApiQuery({ name: 'serviceType', required: false }) + async getMerchantPerformance( + @Query('period') period = 'month', + @Query('serviceType') serviceType?: string, + ) { + return this.financeService.getMerchantPerformance(period, serviceType); + } + + @Get('reports/commission-breakdown') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Get detailed commission breakdown' }) + @ApiQuery({ name: 'startDate', required: true }) + @ApiQuery({ name: 'endDate', required: true }) + async getCommissionBreakdown( + @Query('startDate') startDate: string, + @Query('endDate') endDate: string, + ) { + return this.financeService.getCommissionBreakdown(startDate, endDate); + } + + @Post('reports/export') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Export financial reports' }) + async exportReports(@Body() exportData: any) { + return this.financeService.exportReports(exportData); + } + + // Settlements Endpoints + @Get('settlements/pending') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Get pending settlements' }) + async getPendingSettlements() { + return this.financeService.getPendingSettlements(); + } + + @Post('settlements/process') + @Roles('super_admin') + @ApiOperation({ summary: 'Process settlement payment to merchant' }) + async processSettlement(@Body() settlementData: any) { + return this.financeService.processSettlement(settlementData); + } + + @Get('settlements/history') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Get settlement history' }) + @ApiQuery({ name: 'merchantId', required: false }) + @ApiQuery({ name: 'period', required: false }) + async getSettlementHistory( + @Query('merchantId') merchantId?: string, + @Query('period') period?: string, + ) { + return this.financeService.getSettlementHistory(merchantId, period); + } + + // Refunds Endpoints + @Get('refunds') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Get refunds list' }) + @ApiQuery({ name: 'status', required: false }) + @ApiQuery({ name: 'startDate', required: false }) + @ApiQuery({ name: 'endDate', required: false }) + async getRefunds( + @Query('status') status?: string, + @Query('startDate') startDate?: string, + @Query('endDate') endDate?: string, + ) { + return this.financeService.getRefunds(status, startDate, endDate); + } + + @Post('refunds/process') + @Roles('super_admin') + @ApiOperation({ summary: 'Process administrative refund' }) + async processRefund(@Body() refundData: any) { + return this.financeService.processAdminRefund(refundData); + } + + @Get('refunds/:id/details') + @Roles('super_admin', 'admin') + @ApiOperation({ summary: 'Get refund details' }) + async getRefundDetails(@Param('id') id: string) { + return this.financeService.getRefundDetails(id); + } +} \ No newline at end of file diff --git a/src/modules/finance/finance.module.ts b/src/modules/finance/finance.module.ts new file mode 100644 index 0000000..9c8e21d --- /dev/null +++ b/src/modules/finance/finance.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { FinanceController } from './finance.controller'; +import { FinanceService } from './finance.service'; +import { CommissionRate } from '../../entities/commission-rate.entity'; +import { AdminTransaction } from '../../entities/admin-transaction.entity'; +import { Settlement } from '../../entities/settlement.entity'; +import { User } from '../../entities/user.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + CommissionRate, + AdminTransaction, + Settlement, + User, + ]), + ], + controllers: [FinanceController], + providers: [FinanceService], + exports: [FinanceService], +}) +export class FinanceModule {} diff --git a/src/modules/finance/finance.service.spec.ts b/src/modules/finance/finance.service.spec.ts new file mode 100644 index 0000000..6358290 --- /dev/null +++ b/src/modules/finance/finance.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FinanceService } from './finance.service'; + +describe('FinanceService', () => { + let service: FinanceService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [FinanceService], + }).compile(); + + service = module.get(FinanceService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/modules/finance/finance.service.ts b/src/modules/finance/finance.service.ts new file mode 100644 index 0000000..2ec29f2 --- /dev/null +++ b/src/modules/finance/finance.service.ts @@ -0,0 +1,486 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, Between } from 'typeorm'; +import { CommissionRate } from '../../entities/commission-rate.entity'; +import { AdminTransaction } from '../../entities/admin-transaction.entity'; +import { Settlement } from '../../entities/settlement.entity'; +import { User } from '../../entities/user.entity'; + +@Injectable() +export class FinanceService { + constructor( + @InjectRepository(CommissionRate) + private readonly commissionRateRepository: Repository, + @InjectRepository(AdminTransaction) + private readonly adminTransactionRepository: Repository, + @InjectRepository(Settlement) + private readonly settlementRepository: Repository, + @InjectRepository(User) + private readonly userRepository: Repository, + ) { } + + // Dashboard Methods + async getDashboardOverview(period: string) { + const { startDate, endDate } = this.getPeriodDates(period); + + const totalRevenue = await this.adminTransactionRepository + .createQueryBuilder('transaction') + .select('SUM(transaction.grossAmount)', 'total') + .where('transaction.createdAt BETWEEN :startDate AND :endDate', { + startDate, + endDate, + }) + .getRawOne(); + + const totalCommissions = await this.adminTransactionRepository + .createQueryBuilder('transaction') + .select('SUM(transaction.commissionAmount)', 'total') + .where('transaction.createdAt BETWEEN :startDate AND :endDate', { + startDate, + endDate, + }) + .getRawOne(); + + const transactionCount = await this.adminTransactionRepository + .createQueryBuilder('transaction') + .where('transaction.createdAt BETWEEN :startDate AND :endDate', { + startDate, + endDate, + }) + .getCount(); + + const pendingSettlements = await this.settlementRepository + .createQueryBuilder('settlement') + .where('settlement.status = :status', { status: 'pending' }) + .getCount(); + + return { + totalRevenue: Number(totalRevenue.total) || 0, + totalCommissions: Number(totalCommissions.total) || 0, + netRevenue: Number(totalRevenue.total) - Number(totalCommissions.total) || 0, + transactionCount, + pendingSettlements, + period, + }; + } + + async getRevenueByPeriod(startDate: string, endDate: string, groupBy: string) { + const formatMap = { + day: 'YYYY-MM-DD', + week: 'YYYY-WW', + month: 'YYYY-MM', + }; + + const format = formatMap[groupBy] || 'YYYY-MM-DD'; + + const results = await this.adminTransactionRepository + .createQueryBuilder('transaction') + .select([ + `TO_CHAR(transaction.createdAt, '${format}') as period`, + 'SUM(transaction.grossAmount) as grossAmount', + 'SUM(transaction.commissionAmount) as commissionAmount', + 'SUM(transaction.netAmount) as netAmount', + 'COUNT(*) as transactionCount', + ]) + .where('transaction.createdAt BETWEEN :startDate AND :endDate', { + startDate, + endDate, + }) + .groupBy('period') + .orderBy('period', 'ASC') + .getRawMany(); + + return results.map(result => ({ + period: result.period, + grossAmount: Number(result.grossAmount), + commissionAmount: Number(result.commissionAmount), + netAmount: Number(result.netAmount), + transactionCount: Number(result.transactionCount), + })); + } + + async getCommissionSummary(period: string) { + const { startDate, endDate } = this.getPeriodDates(period); + + const results = await this.adminTransactionRepository + .createQueryBuilder('transaction') + .select([ + 'transaction.serviceType as serviceType', + 'AVG(transaction.commissionRate) as avgCommissionRate', + 'SUM(transaction.commissionAmount) as totalCommission', + 'SUM(transaction.grossAmount) as totalGross', + 'COUNT(*) as transactionCount', + ]) + .where('transaction.createdAt BETWEEN :startDate AND :endDate', { + startDate, + endDate, + }) + .groupBy('transaction.serviceType') + .getRawMany(); + + return results.map(result => ({ + serviceType: result.serviceType, + avgCommissionRate: Number(result.avgCommissionRate), + totalCommission: Number(result.totalCommission), + totalGross: Number(result.totalGross), + transactionCount: Number(result.transactionCount), + })); + } + + // Transaction Methods + async getTransactions(filters: any) { + const { + page, + limit, + serviceType, + status, + startDate, + endDate, + } = filters; + + const queryBuilder = this.adminTransactionRepository + .createQueryBuilder('transaction') + .leftJoinAndSelect('transaction.merchant', 'merchant') + .orderBy('transaction.createdAt', 'DESC'); + + if (serviceType) { + queryBuilder.andWhere('transaction.serviceType = :serviceType', { + serviceType, + }); + } + + if (status) { + queryBuilder.andWhere('transaction.status = :status', { status }); + } + + if (startDate && endDate) { + queryBuilder.andWhere('transaction.createdAt BETWEEN :startDate AND :endDate', { + startDate, + endDate, + }); + } + + const [transactions, total] = await queryBuilder + .skip((page - 1) * limit) + .take(limit) + .getManyAndCount(); + + return { + data: transactions, + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }; + } + + async getTransactionDetails(id: string) { + const transaction = await this.adminTransactionRepository.findOne({ + where: { id }, + relations: ['merchant'], + }); + + if (!transaction) { + throw new NotFoundException('Transaction not found'); + } + + return transaction; + } + + async getTransactionsByMerchant(merchantId: string) { + return this.adminTransactionRepository.find({ + where: { merchantId }, + relations: ['merchant'], + order: { createdAt: 'DESC' }, + }); + } + + // Commission Methods + async getCommissionRates() { + return this.commissionRateRepository.find({ + where: { active: true }, + order: { serviceType: 'ASC' }, + }); + } + + async createCommissionRate(rateData: any) { + const commissionRate = this.commissionRateRepository.create(rateData); + return this.commissionRateRepository.save(commissionRate); + } + + async updateCommissionRate(serviceType: string, updateData: any) { + const result = await this.commissionRateRepository.update( + { serviceType, active: true }, + { ...updateData, updatedAt: new Date() }, + ); + + if (result.affected === 0) { + throw new NotFoundException('Commission rate not found'); + } + + return this.commissionRateRepository.findOne({ + where: { serviceType, active: true }, + }); + } + + async getCommissionHistory() { + return this.commissionRateRepository.find({ + relations: ['updatedByUser'], + order: { updatedAt: 'DESC' }, + }); + } + + // Settlement Methods + async getPendingSettlements() { + return this.settlementRepository.find({ + where: { status: 'pending' }, + relations: ['merchant'], + order: { createdAt: 'DESC' }, + }); + } + + async processSettlement(settlementData: any) { + // TODO: Implement Stripe transfer logic + const settlement = await this.settlementRepository.findOne({ + where: { id: settlementData.settlementId }, + }); + + if (!settlement) { + throw new NotFoundException('Settlement not found'); + } + + // Update settlement status + settlement.status = 'processing'; + settlement.processedAt = new Date(); + + return this.settlementRepository.save(settlement); + } + + async getSettlementHistory(merchantId?: string, period?: string) { + const queryBuilder = this.settlementRepository + .createQueryBuilder('settlement') + .leftJoinAndSelect('settlement.merchant', 'merchant') + .orderBy('settlement.createdAt', 'DESC'); + + if (merchantId) { + queryBuilder.andWhere('settlement.merchantId = :merchantId', { + merchantId, + }); + } + + if (period) { + const { startDate, endDate } = this.getPeriodDates(period); + queryBuilder.andWhere('settlement.createdAt BETWEEN :startDate AND :endDate', { + startDate, + endDate, + }); + } + + return queryBuilder.getMany(); + } + + // Refund Methods + async getRefunds(status?: string, startDate?: string, endDate?: string) { + const queryBuilder = this.adminTransactionRepository + .createQueryBuilder('transaction') + .leftJoinAndSelect('transaction.merchant', 'merchant') + .where('transaction.status = :refundStatus', { refundStatus: 'refunded' }); + + if (startDate && endDate) { + queryBuilder.andWhere('transaction.createdAt BETWEEN :startDate AND :endDate', { + startDate, + endDate, + }); + } + + return queryBuilder.getMany(); + } + + async processAdminRefund(refundData: any) { + // TODO: Implement Stripe refund logic + const transaction = await this.adminTransactionRepository.findOne({ + where: { id: refundData.transactionId }, + }); + + if (!transaction) { + throw new NotFoundException('Transaction not found'); + } + + transaction.status = 'refunded'; + return this.adminTransactionRepository.save(transaction); + } + + async getRefundDetails(id: string) { + return this.getTransactionDetails(id); + } + + // Report Methods + async getFinancialSummary(startDate: string, endDate: string) { + // Usar fechas específicas en lugar de 'custom' + const startDateObj = new Date(startDate); + const endDateObj = new Date(endDate); + + const totalRevenue = await this.adminTransactionRepository + .createQueryBuilder('transaction') + .select('SUM(transaction.grossAmount)', 'total') + .where('transaction.createdAt BETWEEN :startDate AND :endDate', { + startDate: startDateObj, + endDate: endDateObj, + }) + .getRawOne(); + + const totalCommissions = await this.adminTransactionRepository + .createQueryBuilder('transaction') + .select('SUM(transaction.commissionAmount)', 'total') + .where('transaction.createdAt BETWEEN :startDate AND :endDate', { + startDate: startDateObj, + endDate: endDateObj, + }) + .getRawOne(); + + const commissionSummary = await this.getCommissionSummaryByDates(startDateObj, endDateObj); + const revenueByPeriod = await this.getRevenueByPeriod(startDate, endDate, 'day'); + + return { + overview: { + totalRevenue: Number(totalRevenue.total) || 0, + totalCommissions: Number(totalCommissions.total) || 0, + netRevenue: (Number(totalRevenue.total) || 0) - (Number(totalCommissions.total) || 0), + period: { startDate, endDate }, + }, + commissionSummary, + revenueByPeriod, + }; + } + async getMerchantPerformance(period: string, serviceType?: string) { + const { startDate, endDate } = this.getPeriodDates(period); + + const queryBuilder = this.adminTransactionRepository + .createQueryBuilder('transaction') + .leftJoinAndSelect('transaction.merchant', 'merchant') + .select([ + 'merchant.id as merchantId', + 'merchant.firstName as firstName', + 'merchant.lastName as lastName', + 'merchant.email as email', + 'transaction.serviceType as serviceType', + 'SUM(transaction.grossAmount) as totalGross', + 'SUM(transaction.commissionAmount) as totalCommission', + 'SUM(transaction.netAmount) as totalNet', + 'COUNT(*) as transactionCount', + ]) + .where('transaction.createdAt BETWEEN :startDate AND :endDate', { + startDate, + endDate, + }) + .groupBy('merchant.id, transaction.serviceType'); + + if (serviceType) { + queryBuilder.andWhere('transaction.serviceType = :serviceType', { + serviceType, + }); + } + + const results = await queryBuilder.getRawMany(); + + return results.map(result => ({ + merchantId: result.merchantId, + merchantName: `${result.firstName} ${result.lastName}`, + email: result.email, + serviceType: result.serviceType, + totalGross: Number(result.totalGross), + totalCommission: Number(result.totalCommission), + totalNet: Number(result.totalNet), + transactionCount: Number(result.transactionCount), + })); + } + + async getCommissionBreakdown(startDate: string, endDate: string) { + const results = await this.adminTransactionRepository + .createQueryBuilder('transaction') + .leftJoinAndSelect('transaction.merchant', 'merchant') + .select([ + 'transaction.serviceType as serviceType', + 'merchant.id as merchantId', + 'merchant.firstName as firstName', + 'merchant.lastName as lastName', + 'SUM(transaction.commissionAmount) as totalCommission', + 'AVG(transaction.commissionRate) as avgRate', + 'COUNT(*) as transactionCount', + ]) + .where('transaction.createdAt BETWEEN :startDate AND :endDate', { + startDate, + endDate, + }) + .groupBy('transaction.serviceType, merchant.id') + .getRawMany(); + + return results.map(result => ({ + serviceType: result.serviceType, + merchantId: result.merchantId, + merchantName: `${result.firstName} ${result.lastName}`, + totalCommission: Number(result.totalCommission), + avgRate: Number(result.avgRate), + transactionCount: Number(result.transactionCount), + })); + } + + async exportReports(exportData: any) { + // TODO: Implement export functionality (PDF/Excel) + return { message: 'Export functionality to be implemented' }; + } + + // Helper Methods + private getPeriodDates(period: string) { + const now = new Date(); + let startDate: Date; + let endDate = new Date(); + + switch (period) { + case 'week': + startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + break; + case 'month': + startDate = new Date(now.getFullYear(), now.getMonth(), 1); + break; + case 'quarter': + const quarter = Math.floor(now.getMonth() / 3); + startDate = new Date(now.getFullYear(), quarter * 3, 1); + break; + case 'year': + startDate = new Date(now.getFullYear(), 0, 1); + break; + default: + startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + } + + return { startDate, endDate }; + } + + + private async getCommissionSummaryByDates(startDate: Date, endDate: Date) { + const results = await this.adminTransactionRepository + .createQueryBuilder('transaction') + .select([ + 'transaction.serviceType as serviceType', + 'AVG(transaction.commissionRate) as avgCommissionRate', + 'SUM(transaction.commissionAmount) as totalCommission', + 'SUM(transaction.grossAmount) as totalGross', + 'COUNT(*) as transactionCount', + ]) + .where('transaction.createdAt BETWEEN :startDate AND :endDate', { + startDate, + endDate, + }) + .groupBy('transaction.serviceType') + .getRawMany(); + + return results.map(result => ({ + serviceType: result.serviceType, + avgCommissionRate: Number(result.avgCommissionRate), + totalCommission: Number(result.totalCommission), + totalGross: Number(result.totalGross), + transactionCount: Number(result.transactionCount), + })); + } +} \ No newline at end of file diff --git a/src/modules/finance/reports/reports.controller.spec.ts b/src/modules/finance/reports/reports.controller.spec.ts new file mode 100644 index 0000000..f3c142b --- /dev/null +++ b/src/modules/finance/reports/reports.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ReportsController } from './reports.controller'; + +describe('ReportsController', () => { + let controller: ReportsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ReportsController], + }).compile(); + + controller = module.get(ReportsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/modules/finance/reports/reports.controller.ts b/src/modules/finance/reports/reports.controller.ts new file mode 100644 index 0000000..1bd79ed --- /dev/null +++ b/src/modules/finance/reports/reports.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('reports') +export class ReportsController {} diff --git a/src/modules/flight-management/dto/book-flight.dto.ts b/src/modules/flight-management/dto/book-flight.dto.ts new file mode 100644 index 0000000..203e05f --- /dev/null +++ b/src/modules/flight-management/dto/book-flight.dto.ts @@ -0,0 +1,64 @@ +import { IsString, IsNotEmpty, IsNumber, Min, ValidateNested, IsArray, IsEmail } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; +class PassengerDto { +@ApiProperty({ example: 'John' }) +@IsString() +@IsNotEmpty() +firstName: string; +@ApiProperty({ example: 'Doe' }) +@IsString() +@IsNotEmpty() +lastName: string; +@ApiProperty({ example: '1990-01-01' }) +@IsString() // Use string for date if not performing date operations here +@IsNotEmpty() +dateOfBirth: string; +@ApiProperty({ example: 'male' }) +@IsString() +@IsNotEmpty() +gender: string; +@ApiProperty({ example: 'US' }) +@IsString() +@IsNotEmpty() +nationality: string; +@ApiProperty({ example: 'P1234567' }) +@IsString() +@IsNotEmpty() +passportNumber: string; +} +export class BookFlightDto { +@ApiProperty({ description: 'ID of the selected flight', example: 'uuid-flight-123' }) +@IsString() +@IsNotEmpty() +flightId: string; +@ApiProperty({ description: 'Selected cabin class', example: 'economy' }) +@IsString() +@IsNotEmpty() +cabinClass: string; +@ApiProperty({ description: 'Total price of the booking', example: 450.75 }) +@IsNumber() +@Min(0) +totalPrice: number; +@ApiProperty({ description: 'Currency of the total price', example: 'USD' }) +@IsString() +@IsNotEmpty() +currency: string; +@ApiProperty({ description: 'Contact email for the booking', example: 'passenger@example.com' }) +@IsEmail() +@IsNotEmpty() +contactEmail: string; +@ApiProperty({ description: 'Contact phone number for the booking', example: '+18091234567' }) +@IsString() +@IsNotEmpty() +contactPhone: string; +@ApiProperty({ type: [PassengerDto], description: 'List of passengers for the flight' }) +@IsArray() +@ValidateNested({ each: true }) +@Type(() => PassengerDto) +passengers: PassengerDto[]; +@ApiProperty({ description: 'Payment method ID (e.g., Stripe token)', example: 'pm_123abc' }) +@IsString() +@IsNotEmpty() +paymentMethodId: string; +} diff --git a/src/modules/flight-management/dto/search-flight.dto.ts b/src/modules/flight-management/dto/search-flight.dto.ts new file mode 100644 index 0000000..29503cf --- /dev/null +++ b/src/modules/flight-management/dto/search-flight.dto.ts @@ -0,0 +1,47 @@ +import { IsString, IsNotEmpty, IsDateString, IsOptional, IsNumber, Min } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +export class SearchFlightDto { +@ApiProperty({ description: 'Origin airport code (IATA)', example: 'JFK' }) +@IsString() +@IsNotEmpty() +origin: string; +@ApiProperty({ description: 'Destination airport code (IATA)', example: 'SDQ' }) +@IsString() +@IsNotEmpty() +destination: string; +@ApiProperty({ description: 'Departure date (YYYY-MM-DD)', example: '2025-11-15' }) +@IsDateString() +@IsNotEmpty() +departureDate: string; +@ApiPropertyOptional({ description: 'Return date (YYYY-MM-DD) for round trip', example: '2025-11-20' }) +@IsDateString() +@IsOptional() +returnDate?: string; +@ApiPropertyOptional({ description: 'Number of adults', example: 1 }) +@IsNumber() +@Min(0) +@IsOptional() +adults?: number; +@ApiPropertyOptional({ description: 'Number of children', example: 0 }) +@IsNumber() +@Min(0) +@IsOptional() +children?: number; +@ApiPropertyOptional({ description: 'Number of infants', example: 0 }) +@IsNumber() +@Min(0) +@IsOptional() +infants?: number; +@ApiPropertyOptional({ description: 'Cabin class (economy, business, first)', example: 'economy' }) +@IsString() +@IsOptional() +cabinClass?: string; +@ApiPropertyOptional({ description: 'Max price' }) +@IsOptional() +@IsNumber() +maxPrice?: number; +@ApiPropertyOptional({ description: 'Preferred airline code', example: 'AA' }) +@IsOptional() +@IsString() +airlineCode?: string; +} diff --git a/src/modules/flight-management/flight-management.controller.ts b/src/modules/flight-management/flight-management.controller.ts new file mode 100644 index 0000000..5943241 --- /dev/null +++ b/src/modules/flight-management/flight-management.controller.ts @@ -0,0 +1,44 @@ +import { + Controller, Get, Post, Body, Query, UseGuards, Request +} from '@nestjs/common'; +import { + ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery +} from '@nestjs/swagger'; +import { FlightManagementService } from './flight-management.service'; +import { SearchFlightDto } from './dto/search-flight.dto'; +import { BookFlightDto } from './dto/book-flight.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { Flight } from '../../entities/flight.entity'; +import { Transaction } from '../../entities/transaction.entity'; + +@ApiTags('Flight Management') +@ApiBearerAuth('JWT-auth') +@UseGuards(JwtAuthGuard) +@Controller('api/v1/flights') +export class FlightManagementController { + constructor(private readonly flightManagementService: FlightManagementService) {} + + @Get('search') + @ApiOperation({ summary: 'Search for flights based on parameters' }) + @ApiQuery({ name: 'origin', type: String, example: 'JFK' }) + @ApiQuery({ name: 'destination', type: String, example: 'SDQ' }) + @ApiQuery({ name: 'departureDate', type: String, format: 'date', example: '2025-11-15' }) + @ApiQuery({ name: 'returnDate', type: String, format: 'date', required: false, example: '2025-11-20' }) + @ApiQuery({ name: 'adults', type: Number, required: false, example: 1 }) + @ApiQuery({ name: 'children', type: Number, required: false, example: 0 }) + @ApiQuery({ name: 'infants', type: Number, required: false, example: 0 }) + @ApiQuery({ name: 'cabinClass', type: String, required: false, example: 'economy' }) + @ApiQuery({ name: 'maxPrice', type: Number, required: false }) + @ApiQuery({ name: 'airlineCode', type: String, required: false, example: 'AA' }) + @ApiResponse({ status: 200, type: [Flight] }) + searchFlights(@Query() searchFlightDto: SearchFlightDto) { + return this.flightManagementService.searchFlights(searchFlightDto); + } + + @Post('book') + @ApiOperation({ summary: 'Book a flight with reservation data' }) + @ApiResponse({ status: 201, description: 'Flight booked successfully', type: Transaction }) + bookFlight(@Body() bookFlightDto: BookFlightDto, @Request() req) { + return this.flightManagementService.bookFlight(bookFlightDto, req.user.id); + } +} diff --git a/src/modules/flight-management/flight-management.module.ts b/src/modules/flight-management/flight-management.module.ts new file mode 100644 index 0000000..f7e7b5f --- /dev/null +++ b/src/modules/flight-management/flight-management.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { FlightManagementService } from './flight-management.service'; +import { FlightManagementController } from './flight-management.controller'; +import { Flight } from '../../entities/flight.entity'; +import { User } from '../../entities/user.entity'; +import { Transaction } from '../../entities/transaction.entity'; +import { PaymentsModule } from '../payments/payments.module'; +import { NotificationsModule } from '../notifications/notifications.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + Flight, + User, + Transaction, + ]), + PaymentsModule, + NotificationsModule, + ], + controllers: [FlightManagementController], + providers: [FlightManagementService], + exports: [FlightManagementService], +}) +export class FlightManagementModule {} diff --git a/src/modules/flight-management/flight-management.service.ts b/src/modules/flight-management/flight-management.service.ts new file mode 100644 index 0000000..aee8635 --- /dev/null +++ b/src/modules/flight-management/flight-management.service.ts @@ -0,0 +1,141 @@ +import { Injectable, BadRequestException, NotFoundException, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Flight } from '../../entities/flight.entity'; +import { SearchFlightDto } from './dto/search-flight.dto'; +import { BookFlightDto } from './dto/book-flight.dto'; +import { User } from '../../entities/user.entity'; +import { Transaction } from '../../entities/transaction.entity'; +import { PaymentsService } from '../payments/payments.service'; +import { NotificationsService } from '../notifications/notifications.service'; +import { NotificationCategory, NotificationType } from '../notifications/dto/create-notification.dto'; +import { ProcessPaymentDto } from '../payments/dto/process-payment.dto'; + +@Injectable() +export class FlightManagementService { + private readonly logger = new Logger(FlightManagementService.name); + + constructor( + @InjectRepository(Flight) + private readonly flightRepository: Repository, + @InjectRepository(User) + private readonly userRepository: Repository, + @InjectRepository(Transaction) + private readonly transactionRepository: Repository, + private readonly paymentsService: PaymentsService, + private readonly notificationsService: NotificationsService, + ) {} + + /** + * Simulates searching for flights based on criteria. + * In a real-world scenario, this would integrate with a Flight GDS (Global Distribution System) API + * like Amadeus, Sabre, Travelport, or direct airline APIs. + */ + async searchFlights(searchFlightDto: SearchFlightDto): Promise { + this.logger.log(`Searching flights: ${JSON.stringify(searchFlightDto)}`); + + if (new Date(searchFlightDto.departureDate) < new Date()) { + throw new BadRequestException('Departure date cannot be in the past.'); + } + if (searchFlightDto.returnDate && new Date(searchFlightDto.returnDate) < new Date(searchFlightDto.departureDate)) { + throw new BadRequestException('Return date cannot be before departure date.'); + } + + const query = this.flightRepository.createQueryBuilder('flight') + .where('flight.originCode = :origin', { origin: searchFlightDto.origin.toUpperCase() }) + .andWhere('flight.destinationCode = :destination', { destination: searchFlightDto.destination.toUpperCase() }) + .andWhere('DATE(flight.departureTime) = :departureDate', { departureDate: searchFlightDto.departureDate }); + + if (searchFlightDto.airlineCode) { + query.andWhere('flight.airlineCode = :airlineCode', { airlineCode: searchFlightDto.airlineCode.toUpperCase() }); + } + + const flights = await query.getMany(); + + return flights.filter(f => { + if (searchFlightDto.adults) { + const selectedClass = f.seatClasses[searchFlightDto.cabinClass || 'economy']; + if (!selectedClass || selectedClass.available < searchFlightDto.adults) { + return false; + } + } + return true; + }); + } + + /** + * Simulates booking a flight. + * In a real-world scenario, this would involve a multi-step booking process + * with external flight APIs, payment gateways, and reservation systems. + */ + async bookFlight(bookFlightDto: BookFlightDto, userId: string): Promise { + this.logger.log(`Booking flight: ${JSON.stringify(bookFlightDto)} for user ${userId}`); + + const flight = await this.flightRepository.findOne({ where: { id: bookFlightDto.flightId } }); + if (!flight) { + throw new NotFoundException(`Flight with ID "${bookFlightDto.flightId}" not found.`); + } + + const user = await this.userRepository.findOne({ where: { id: userId } }); + if (!user) { + throw new NotFoundException(`User with ID "${userId}" not found.`); + } + + const requestedSeats = bookFlightDto.passengers.length; + const selectedClass = flight.seatClasses[bookFlightDto.cabinClass]; + if (!selectedClass || selectedClass.available < requestedSeats) { + throw new BadRequestException(`Not enough seats available in ${bookFlightDto.cabinClass} for this flight.`); + } + + const paymentResult = await this.paymentsService.processPayment({ + userId, + amount: bookFlightDto.totalPrice, + currency: bookFlightDto.currency, + paymentMethodId: bookFlightDto.paymentMethodId, + description: `Flight booking for ${flight.flightNumber}`, + metadata: { flightId: flight.id, passengers: bookFlightDto.passengers.length }, + } as ProcessPaymentDto); + + if (!paymentResult.success) { + throw new BadRequestException(`Payment failed: ${paymentResult.message}`); + } + + const transaction = this.transactionRepository.create({ + userId, + entityType: 'flight_booking', + entityId: flight.id, + amount: bookFlightDto.totalPrice, + currency: bookFlightDto.currency, + status: 'completed', + paymentDetails: paymentResult.transactionId, + metadata: { + flightNumber: flight.flightNumber, + origin: flight.originCode, + destination: flight.destinationCode, + departureTime: flight.departureTime, + passengers: bookFlightDto.passengers, + contactEmail: bookFlightDto.contactEmail, + }, + } as Partial); + await this.transactionRepository.save(transaction); + + if (selectedClass) { + selectedClass.available -= requestedSeats; + flight.seatsBooked += requestedSeats; + await this.flightRepository.save(flight); + } + + await this.notificationsService.createNotification({ + userId, + type: NotificationType.EMAIL, + category: NotificationCategory.BOOKING, + title: 'Flight Booking Confirmed!', + message: `Your flight ${flight.airlineCode}${flight.flightNumber} from ${flight.originCity} to ${flight.destinationCity} on ${flight.departureTime.toLocaleDateString()} has been successfully booked. Your transaction ID is ${transaction.id}.`, + recipientEmail: bookFlightDto.contactEmail, + data: { flightId: flight.id, transactionId: transaction.id }, + }); + + this.logger.log(`Flight ${flight.flightNumber} booked successfully for user ${userId}. Transaction ID: ${transaction.id}`); + return transaction; + } +} diff --git a/src/modules/geolocation/dto/geofence.dto.ts b/src/modules/geolocation/dto/geofence.dto.ts new file mode 100644 index 0000000..63c95ad --- /dev/null +++ b/src/modules/geolocation/dto/geofence.dto.ts @@ -0,0 +1,52 @@ +import { IsNumber, IsString, IsOptional, IsEnum, Min } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum GeofenceType { + TOURIST_ZONE = 'tourist-zone', + SAFETY_ALERT = 'safety-alert', + ATTRACTION = 'attraction', + RESTRICTED_AREA = 'restricted-area', + PICKUP_ZONE = 'pickup-zone' +} + +export class CreateGeofenceDto { + @ApiProperty({ description: 'Geofence name', example: 'Zona Colonial Safety Zone' }) + @IsString() + name: string; + + @ApiProperty({ description: 'Center latitude' }) + @IsNumber() + latitude: number; + + @ApiProperty({ description: 'Center longitude' }) + @IsNumber() + longitude: number; + + @ApiProperty({ description: 'Radius in meters', example: 500 }) + @IsNumber() + @Min(1) + radius: number; + + @ApiProperty({ description: 'Geofence type', enum: GeofenceType }) + @IsEnum(GeofenceType) + type: GeofenceType; + + @ApiPropertyOptional({ description: 'Description' }) + @IsOptional() + @IsString() + description?: string; + + @ApiPropertyOptional({ description: 'Alert message for entry' }) + @IsOptional() + @IsString() + entryMessage?: string; + + @ApiPropertyOptional({ description: 'Alert message for exit' }) + @IsOptional() + @IsString() + exitMessage?: string; + + @ApiPropertyOptional({ description: 'Additional metadata' }) + @IsOptional() + metadata?: Record; +} diff --git a/src/modules/geolocation/dto/location-update.dto.ts b/src/modules/geolocation/dto/location-update.dto.ts new file mode 100644 index 0000000..41f67b5 --- /dev/null +++ b/src/modules/geolocation/dto/location-update.dto.ts @@ -0,0 +1,32 @@ +import { IsNumber, IsOptional, IsString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class LocationUpdateDto { + @ApiProperty({ description: 'User latitude' }) + @IsNumber() + latitude: number; + + @ApiProperty({ description: 'User longitude' }) + @IsNumber() + longitude: number; + + @ApiPropertyOptional({ description: 'Accuracy in meters' }) + @IsOptional() + @IsNumber() + accuracy?: number; + + @ApiPropertyOptional({ description: 'Speed in km/h' }) + @IsOptional() + @IsNumber() + speed?: number; + + @ApiPropertyOptional({ description: 'Heading in degrees' }) + @IsOptional() + @IsNumber() + heading?: number; + + @ApiPropertyOptional({ description: 'Activity type', example: 'walking' }) + @IsOptional() + @IsString() + activity?: string; // walking, driving, stationary, etc. +} diff --git a/src/modules/geolocation/geolocation.controller.ts b/src/modules/geolocation/geolocation.controller.ts new file mode 100644 index 0000000..9b953d7 --- /dev/null +++ b/src/modules/geolocation/geolocation.controller.ts @@ -0,0 +1,211 @@ +import { + Controller, Get, Post, Body, Query, UseGuards, Request +} from '@nestjs/common'; +import { + ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery +} from '@nestjs/swagger'; +import { GeolocationService } from './geolocation.service'; +import { CreateGeofenceDto } from './dto/geofence.dto'; +import { LocationUpdateDto } from './dto/location-update.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; +import { Geofence } from '../../entities/geofence.entity'; + +// ===================================================================================== +// CORRECCIÓN: Se eliminó @UseGuards(JwtAuthGuard) de aquí para permitir que algunos +// endpoints (como safety/zones) sean públicos. La seguridad se aplicará ahora a cada +// método individualmente. +// ===================================================================================== +@ApiTags('Geolocation') +@Controller('geolocation') +export class GeolocationController { + constructor(private readonly geolocationService: GeolocationService) { } + + // GEOFENCE MANAGEMENT (REQUIERE ADMIN) + @Post('geofences') + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin') + @ApiBearerAuth('JWT-auth') + @ApiOperation({ summary: 'Create geofence (Admin only)' }) + @ApiResponse({ status: 201, description: 'Geofence created successfully', type: Geofence }) + createGeofence(@Body() createGeofenceDto: CreateGeofenceDto) { + return this.geolocationService.createGeofence(createGeofenceDto); + } + + @Get('geofences') + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin') + @ApiBearerAuth('JWT-auth') + @ApiOperation({ summary: 'Get all active geofences (Admin only)' }) + @ApiResponse({ status: 200, type: [Geofence] }) + getActiveGeofences() { + return this.geolocationService.getActiveGeofences(); + } + + // LOCATION TRACKING (REQUIERE USUARIO) + @Post('location/update') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth('JWT-auth') + @ApiOperation({ summary: 'Update user location' }) + @ApiResponse({ status: 200, description: 'Location updated successfully' }) + updateLocation(@Body() locationDto: LocationUpdateDto, @Request() req) { + return this.geolocationService.updateUserLocation(req.user.id, locationDto); + } + + @Post('geofences/check') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth('JWT-auth') + @ApiOperation({ summary: 'Check geofence triggers for location' }) + checkGeofences(@Body() body: { latitude: number; longitude: number }, @Request() req) { + return this.geolocationService.checkGeofenceEntry( + req.user.id, + body.latitude, + body.longitude, + ); + } + + // SMART NAVIGATION (PÚBLICO) + @Post('navigation/route') + @ApiOperation({ summary: 'Get optimized route with attractions' }) + @ApiResponse({ status: 200, description: 'Optimized route generated' }) + getOptimizedRoute(@Body() body: { + startLat: number; + startLng: number; + endLat: number; + endLng: number; + travelMode?: string; + includeAttractions?: boolean; + }) { + return this.geolocationService.getOptimizedRoute( + body.startLat, + body.startLng, + body.endLat, + body.endLng, + body.travelMode, + body.includeAttractions, + ); + } + + @Get('nearby/attractions') + @ApiOperation({ summary: 'Get nearby attractions' }) + @ApiQuery({ name: 'latitude', type: Number }) + @ApiQuery({ name: 'longitude', type: Number }) + @ApiQuery({ name: 'radius', required: false, type: Number, description: 'Radius in meters' }) + async getNearbyAttractions( + @Query('latitude') latitude: number, + @Query('longitude') longitude: number, + @Query('radius') radius?: number, + ) { + return this.geolocationService['getNearbyAttractions'](latitude, longitude, radius); + } + + // SMART SUGGESTIONS (REQUIERE USUARIO) + @Post('suggestions/smart') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth('JWT-auth') + @ApiOperation({ summary: 'Get location-based smart suggestions' }) + async getSmartSuggestions(@Body() locationDto: LocationUpdateDto, @Request() req) { + const nearbyAttractions = await this.geolocationService['getNearbyAttractions']( + locationDto.latitude, + locationDto.longitude, + ); + + const suggestions = await this.geolocationService['generateSmartSuggestions']( + req.user.id, + locationDto, + nearbyAttractions, + ); + + return { + suggestions, + nearbyAttractions, + contextInfo: { + currentTime: new Date(), + activity: locationDto.activity, + speed: locationDto.speed, + }, + }; + } + + // EMERGENCY FEATURES (REQUIERE USUARIO) + @Post('emergency/panic-button') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth('JWT-auth') + @ApiOperation({ summary: 'Trigger emergency panic button' }) + async triggerPanicButton(@Body() body: { + latitude: number; + longitude: number; + message?: string; + }, @Request() req) { + return { + success: true, + message: 'Emergency alert sent successfully', + estimatedResponseTime: '5-10 minutes', + emergencyContact: '+1-809-200-7000', + }; + } + + // ANALYTICS (REQUIERE ADMIN) + @Get('analytics') + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin') + @ApiBearerAuth('JWT-auth') + @ApiOperation({ summary: 'Get location analytics (Admin only)' }) + @ApiQuery({ name: 'timeframe', required: false, type: String, description: 'Time period (7d, 30d, 90d)' }) + getLocationAnalytics(@Query('timeframe') timeframe?: string) { + return this.geolocationService.getLocationAnalytics(timeframe); + } + + // SAFETY FEATURES (PÚBLICO CON MEJORA PARA USUARIOS AUTENTICADOS) + @Get('safety/zones') + @ApiOperation({ summary: 'Get safety information for area' }) + @ApiQuery({ name: 'latitude', type: Number }) + @ApiQuery({ name: 'longitude', type: Number }) + async getSafetyInfo( + @Query('latitude') latitude: number, + @Query('longitude') longitude: number, + @Request() req, // Se usa para detectar si hay un usuario logueado + ) { + // ============================================================================== + // CORRECCIÓN CLAVE: El servicio de notificaciones solo se llama si hay un + // usuario real con un ID válido. Si no hay token, req.user será undefined. + // ============================================================================== + let geofenceCheck: { + triggeredGeofences: Geofence[]; + alerts: Array<{ type: string; message: string; geofence: Geofence }>; + } = { + triggeredGeofences: [], + alerts: [], + }; // Valor por defecto para usuarios anónimos + + if (req.user && req.user.id) { + geofenceCheck = await this.geolocationService.checkGeofenceEntry( + req.user.id, + latitude, + longitude, + ); + } + + const safetyTips = [ + 'Stay in well-lit, populated areas', + 'Keep your belongings secure', + 'Use official transportation services', + 'Have emergency contacts readily available', + ]; + + const emergencyContacts = [ + { name: 'POLITUR (Tourist Police)', number: '+1-809-200-7000' }, + { name: 'General Emergency', number: '911' }, + { name: 'Medical Emergency', number: '+1-809-688-4411' }, + ]; + + return { + safetyLevel: 'moderate', + geofenceAlerts: geofenceCheck.alerts, + safetyTips, + emergencyContacts, + nearbyPoliturStations: [], + }; + } +} \ No newline at end of file diff --git a/src/modules/geolocation/geolocation.module.ts b/src/modules/geolocation/geolocation.module.ts new file mode 100644 index 0000000..5472e6d --- /dev/null +++ b/src/modules/geolocation/geolocation.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { GeolocationService } from './geolocation.service'; +import { GeolocationController } from './geolocation.controller'; +import { Geofence } from '../../entities/geofence.entity'; +import { LocationTracking } from '../../entities/location-tracking.entity'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { NotificationsModule } from '../notifications/notifications.module'; +import { SecurityModule } from '../security/security.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + Geofence, + LocationTracking, + PlaceOfInterest, + ]), + NotificationsModule, + SecurityModule, + ], + controllers: [GeolocationController], + providers: [GeolocationService], + exports: [GeolocationService], +}) +export class GeolocationModule {} diff --git a/src/modules/geolocation/geolocation.service.ts b/src/modules/geolocation/geolocation.service.ts new file mode 100644 index 0000000..736d5c2 --- /dev/null +++ b/src/modules/geolocation/geolocation.service.ts @@ -0,0 +1,400 @@ +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 { Geofence } from '../../entities/geofence.entity'; +import { LocationTracking } from '../../entities/location-tracking.entity'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { CreateGeofenceDto } from './dto/geofence.dto'; +import { LocationUpdateDto } from './dto/location-update.dto'; +import { NotificationsService } from '../notifications/notifications.service'; +import { SecurityService } from '../security/security.service'; +import { NotificationType, NotificationCategory } from '../notifications/dto/create-notification.dto'; + +@Injectable() +export class GeolocationService { + constructor( + @InjectRepository(Geofence) + private readonly geofenceRepository: Repository, + @InjectRepository(LocationTracking) + private readonly locationTrackingRepository: Repository, + @InjectRepository(PlaceOfInterest) + private readonly placeRepository: Repository, + private readonly configService: ConfigService, + private readonly notificationsService: NotificationsService, + private readonly securityService: SecurityService, + ) { } + + // GEOFENCING MANAGEMENT + async createGeofence(createGeofenceDto: CreateGeofenceDto): Promise { + const geofence = this.geofenceRepository.create({ + ...createGeofenceDto, + centerCoordinates: `(${createGeofenceDto.longitude},${createGeofenceDto.latitude})`, + }); + + return this.geofenceRepository.save(geofence); + } + + async getActiveGeofences(): Promise { + return this.geofenceRepository.find({ + where: { isActive: true }, + order: { createdAt: 'DESC' }, + }); + } + + async checkGeofenceEntry( + userId: string, + latitude: number, + longitude: number, + ): Promise<{ + triggeredGeofences: Geofence[]; + alerts: Array<{ type: string; message: string; geofence: Geofence }>; + }> { + // In production, use PostGIS for accurate geospatial calculations + // For now, simulate geofence detection + const activeGeofences = await this.getActiveGeofences(); + const triggeredGeofences: Geofence[] = []; + const alerts: Array<{ type: string; message: string; geofence: Geofence }> = []; + + for (const geofence of activeGeofences) { + const distance = this.calculateDistance( + latitude, + longitude, + this.extractLatFromPoint(geofence.centerCoordinates), + this.extractLngFromPoint(geofence.centerCoordinates) + ); + + if (distance <= geofence.radius) { + triggeredGeofences.push(geofence); + + // Increment entry count + await this.geofenceRepository.increment({ id: geofence.id }, 'entryCount', 1); + + // Handle different geofence types + switch (geofence.type) { + case 'safety-alert': + alerts.push({ + type: 'warning', + message: geofence.entryMessage || 'You have entered a safety alert zone. Please be cautious.', + geofence, + }); + break; + + case 'tourist-zone': + alerts.push({ + type: 'info', + message: geofence.entryMessage || `Welcome to ${geofence.name}! Explore the attractions around you.`, + geofence, + }); + break; + + case 'attraction': + alerts.push({ + type: 'attraction', + message: geofence.entryMessage || `You're near ${geofence.name}. Check out what's available!`, + geofence, + }); + break; + + case 'restricted-area': + alerts.push({ + type: 'restriction', + message: geofence.entryMessage || 'You have entered a restricted area. Please respect local regulations.', + geofence, + }); + break; + + case 'pickup-zone': + alerts.push({ + type: 'service', + message: geofence.entryMessage || 'You are in a designated pickup zone for taxis and tours.', + geofence, + }); + break; + } + + // Send notification + await this.notificationsService.createNotification({ + userId, + type: NotificationType.PUSH, + category: NotificationCategory.SECURITY, + title: `📍 ${geofence.name}`, + message: alerts[alerts.length - 1]?.message || 'Location alert', + data: { geofenceId: geofence.id, type: geofence.type }, + }); + } + } + + return { triggeredGeofences, alerts }; + } + + // LOCATION TRACKING + async updateUserLocation( + userId: string, + locationDto: LocationUpdateDto, + ): Promise<{ + success: boolean; + geofenceAlerts: any[]; + nearbyAttractions: PlaceOfInterest[]; + smartSuggestions: string[]; + }> { + // Save location tracking + const tracking = this.locationTrackingRepository.create({ + userId, + coordinates: `(${locationDto.longitude},${locationDto.latitude})`, + accuracy: locationDto.accuracy, + speed: locationDto.speed, + heading: locationDto.heading, + activity: locationDto.activity, + }); + + await this.locationTrackingRepository.save(tracking); + + // Check geofences + const geofenceCheck = await this.checkGeofenceEntry( + userId, + locationDto.latitude, + locationDto.longitude, + ); + + // Get nearby attractions + const nearbyAttractions = await this.getNearbyAttractions( + locationDto.latitude, + locationDto.longitude, + 1000, // 1km radius + ); + + // Generate smart suggestions based on location and activity + const smartSuggestions = await this.generateSmartSuggestions( + userId, + locationDto, + nearbyAttractions, + ); + + return { + success: true, + geofenceAlerts: geofenceCheck.alerts, + nearbyAttractions, + smartSuggestions, + }; + } + + private async getNearbyAttractions( + latitude: number, + longitude: number, + radiusMeters: number = 1000, + ): Promise { + // In production, use PostGIS ST_DWithin for accurate distance queries + return this.placeRepository.find({ + where: { active: true }, + order: { rating: 'DESC' }, + take: 10, + }); + } + + private async generateSmartSuggestions( + userId: string, + location: LocationUpdateDto, + nearbyAttractions: PlaceOfInterest[], + ): Promise { + const suggestions: string[] = []; + const currentHour = new Date().getHours(); + + // Time-based suggestions + if (currentHour >= 6 && currentHour <= 10) { + suggestions.push("Good morning! Start your day with a visit to a nearby historic site."); + } else if (currentHour >= 11 && currentHour <= 14) { + suggestions.push("It's lunch time! Find authentic Dominican restaurants nearby."); + } else if (currentHour >= 15 && currentHour <= 18) { + suggestions.push("Perfect time for sightseeing! Explore attractions around you."); + } else if (currentHour >= 19 && currentHour <= 23) { + suggestions.push("Evening entertainment awaits! Check out local restaurants and nightlife."); + } + + // Activity-based suggestions + if (location.activity === 'walking') { + suggestions.push("Great walking weather! Discover hidden gems on foot."); + } else if (location.activity === 'stationary') { + suggestions.push("Take a moment to explore what's around you."); + } + + // Speed-based suggestions + if (location.speed && location.speed > 30) { + suggestions.push("Traveling fast? Don't miss scenic viewpoints along your route."); + } + + // Attraction-based suggestions + if (nearbyAttractions.length > 0) { + const topAttraction = nearbyAttractions[0]; + suggestions.push(`${topAttraction.name} is nearby (${topAttraction.rating}/5 stars). Worth a visit!`); + } + + return suggestions.slice(0, 3); // Return top 3 suggestions + } + + // SMART NAVIGATION + async getOptimizedRoute( + startLat: number, + startLng: number, + endLat: number, + endLng: number, + travelMode: string = 'walking', + includeAttractions: boolean = true, + ): Promise<{ + route: any; + duration: number; + distance: number; + waypoints: PlaceOfInterest[]; + weatherInfo: any; + safetyTips: string[]; + }> { + // In production, integrate with Google Maps Directions API + const mockRoute = { + steps: [ + { instruction: 'Head north on Calle Las Damas', distance: '200m', duration: '3 min' }, + { instruction: 'Turn right at Plaza de Armas', distance: '150m', duration: '2 min' }, + { instruction: 'Continue straight to destination', distance: '100m', duration: '1 min' }, + ], + }; + + const waypoints = includeAttractions + ? await this.getWaypointAttractions(startLat, startLng, endLat, endLng) + : []; + + const weatherInfo = await this.getRouteWeatherInfo(startLat, startLng); + const safetyTips = this.generateRouteSafetyTips(travelMode, new Date().getHours()); + + return { + route: mockRoute, + duration: 6, // minutes + distance: 450, // meters + waypoints, + weatherInfo, + safetyTips, + }; + } + + private async getWaypointAttractions( + startLat: number, + startLng: number, + endLat: number, + endLng: number, + ): Promise { + // Find attractions along the route + return this.placeRepository.find({ + where: { active: true }, + order: { rating: 'DESC' }, + take: 3, + }); + } + + private async getRouteWeatherInfo(lat: number, lng: number): Promise { + // In production, integrate with weather API + return { + temperature: 28, + condition: 'sunny', + humidity: 75, + recommendation: 'Perfect weather for walking! Stay hydrated.', + }; + } + + private generateRouteSafetyTips(travelMode: string, hour: number): string[] { + const tips: string[] = [ + 'Stay aware of your surroundings', + 'Keep your belongings secure', + ]; + + if (travelMode === 'walking') { + tips.push('Use sidewalks and well-lit paths'); + if (hour >= 20 || hour <= 6) { + tips.push('Consider using a taxi for night travel'); + } + } + + if (travelMode === 'driving') { + tips.push('Follow local traffic laws'); + tips.push('Use GPS navigation'); + } + + return tips; + } + + // ANALYTICS + async getLocationAnalytics(timeframe: string = '7d'): Promise<{ + totalUsers: number; + activeUsers: number; + popularZones: Array<{ name: string; visits: number }>; + geofenceStats: Array<{ name: string; entries: number; type: string }>; + heatmapData: Array<{ lat: number; lng: number; intensity: number }>; + }> { + const days = timeframe === '7d' ? 7 : timeframe === '30d' ? 30 : 90; + const totalUsers = await this.locationTrackingRepository + .createQueryBuilder('tracking') + .select('COUNT(DISTINCT tracking.userId)', 'count') + .where(`tracking.createdAt >= NOW() - INTERVAL '${days} days'`) + .getRawOne(); + + const activeUsers = await this.locationTrackingRepository + .createQueryBuilder('tracking') + .select('COUNT(DISTINCT tracking.userId)', 'count') + .where(`tracking.createdAt >= NOW() - INTERVAL '1 days'`) + .getRawOne(); + + const geofenceStats = await this.geofenceRepository.find({ + where: { isActive: true }, + order: { entryCount: 'DESC' }, + take: 10, + }); + + // Simplified heatmap data (in production, aggregate actual location data) + const heatmapData = [ + { lat: 18.4861, lng: -69.9312, intensity: 0.8 }, // Santo Domingo + { lat: 18.5204, lng: -68.7340, intensity: 0.6 }, // Punta Cana + { lat: 19.7933, lng: -70.6928, intensity: 0.5 }, // Puerto Plata + ]; + + return { + totalUsers: parseInt(totalUsers.count), + activeUsers: parseInt(activeUsers.count), + popularZones: [], // TODO: Implement zone analysis + geofenceStats: geofenceStats.map(g => ({ + name: g.name, + entries: g.entryCount, + type: g.type, + })), + heatmapData, + }; + } + + // UTILITY METHODS + private calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number { + const R = 6371000; // Earth's radius in meters + const dLat = this.toRadians(lat2 - lat1); + const dLng = this.toRadians(lng2 - lng1); + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; + } + + private toRadians(degrees: number): number { + return degrees * (Math.PI / 180); + } + + private extractLatFromPoint(point: any): number { + if (point && typeof point === 'object' && point.y !== undefined) { + return point.y; // latitude + } + return 0; + } + + + private extractLngFromPoint(point: any): number { + if (point && typeof point === 'object' && point.x !== undefined) { + return point.x; // longitude + } + return 0; + } +} diff --git a/src/modules/hotel/dto/create-hotel-checkin.dto.ts b/src/modules/hotel/dto/create-hotel-checkin.dto.ts new file mode 100755 index 0000000..74ecb1d --- /dev/null +++ b/src/modules/hotel/dto/create-hotel-checkin.dto.ts @@ -0,0 +1,38 @@ +import { IsString, IsDateString, IsNumber, IsOptional, Min } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateHotelCheckinDto { + @ApiProperty({ description: 'Room ID' }) + @IsString() + roomId: string; + + @ApiProperty({ description: 'Guest user ID' }) + @IsString() + guestId: string; + + @ApiProperty({ description: 'Reservation ID' }) + @IsString() + reservationId: string; + + @ApiProperty({ description: 'Check-in date', example: '2025-07-01' }) + @IsDateString() + checkinDate: string; + + @ApiProperty({ description: 'Check-out date', example: '2025-07-03' }) + @IsDateString() + checkoutDate: string; + + @ApiProperty({ description: 'Number of guests', example: 2 }) + @IsNumber() + @Min(1) + guestCount: number; + + @ApiPropertyOptional({ description: 'Special requests' }) + @IsOptional() + @IsString() + specialRequests?: string; + + @ApiPropertyOptional({ description: 'Guest preferences' }) + @IsOptional() + guestPreferences?: Record; +} diff --git a/src/modules/hotel/dto/room-service-request.dto.ts b/src/modules/hotel/dto/room-service-request.dto.ts new file mode 100755 index 0000000..5ae2e6a --- /dev/null +++ b/src/modules/hotel/dto/room-service-request.dto.ts @@ -0,0 +1,61 @@ +import { IsString, IsOptional, IsEnum, IsArray, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum ServiceType { + HOUSEKEEPING = 'housekeeping', + ROOM_SERVICE = 'room-service', + MAINTENANCE = 'maintenance', + CONCIERGE = 'concierge', + LAUNDRY = 'laundry' +} + +export class ServiceItemDto { + @ApiProperty({ description: 'Service item name', example: 'Extra towels' }) + @IsString() + name: string; + + @ApiPropertyOptional({ description: 'Quantity', example: 2 }) + @IsOptional() + quantity?: number; + + @ApiPropertyOptional({ description: 'Special instructions' }) + @IsOptional() + @IsString() + notes?: string; +} + +export class RoomServiceRequestDto { + @ApiProperty({ description: 'Room ID' }) + @IsString() + roomId: string; + + @ApiProperty({ description: 'Guest ID' }) + @IsString() + guestId: string; + + @ApiProperty({ description: 'Service type', enum: ServiceType }) + @IsEnum(ServiceType) + serviceType: ServiceType; + + @ApiProperty({ description: 'Service items', type: [ServiceItemDto] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ServiceItemDto) + items: ServiceItemDto[]; + + @ApiPropertyOptional({ description: 'Priority level', example: 'normal' }) + @IsOptional() + @IsString() + priority?: string; // low, normal, high, urgent + + @ApiPropertyOptional({ description: 'Preferred time' }) + @IsOptional() + @IsString() + preferredTime?: string; + + @ApiPropertyOptional({ description: 'Special instructions' }) + @IsOptional() + @IsString() + specialInstructions?: string; +} diff --git a/src/modules/hotel/hotel.controller.ts b/src/modules/hotel/hotel.controller.ts new file mode 100644 index 0000000..88030ee --- /dev/null +++ b/src/modules/hotel/hotel.controller.ts @@ -0,0 +1,220 @@ +import { + Controller, Get, Post, Body, Patch, Param, Query, UseGuards, Request +} from '@nestjs/common'; +import { + ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam +} from '@nestjs/swagger'; +import { HotelService } from './hotel.service'; +import { CreateHotelCheckinDto } from './dto/create-hotel-checkin.dto'; +import { RoomServiceRequestDto } from './dto/room-service-request.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; +import { HotelRoom } from '../../entities/hotel-room.entity'; +import { HotelCheckin } from '../../entities/hotel-checkin.entity'; +import { HotelService as HotelServiceEntity } from '../../entities/hotel-service.entity'; + +@ApiTags('Hotel') +@Controller('hotel') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class HotelController { + constructor(private readonly hotelService: HotelService) {} + + // ROOM MANAGEMENT + @Get('establishments/:establishmentId/rooms') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Get hotel rooms' }) + @ApiParam({ name: 'establishmentId', type: 'string' }) + @ApiQuery({ name: 'isAvailable', required: false, type: Boolean }) + @ApiQuery({ name: 'roomType', required: false, type: String }) + @ApiResponse({ status: 200, type: [HotelRoom] }) + getRoomsByEstablishment( + @Param('establishmentId') establishmentId: string, + @Query('isAvailable') isAvailable?: boolean, + @Query('roomType') roomType?: string, + ) { + return this.hotelService.getRoomsByEstablishment(establishmentId, isAvailable, roomType); + } + + @Patch('rooms/:roomId/availability') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Update room availability' }) + @ApiParam({ name: 'roomId', type: 'string' }) + updateRoomAvailability( + @Param('roomId') roomId: string, + @Body() body: { isAvailable: boolean }, + ) { + return this.hotelService.updateRoomAvailability(roomId, body.isAvailable); + } + + // CHECK-IN/CHECK-OUT MANAGEMENT + @Post('checkin') + @ApiOperation({ summary: 'Digital check-in' }) + @ApiResponse({ status: 201, description: 'Check-in completed successfully', type: HotelCheckin }) + digitalCheckin(@Body() createCheckinDto: CreateHotelCheckinDto) { + return this.hotelService.digitalCheckin(createCheckinDto); + } + + @Patch('checkin/:checkinId/checkout') + @ApiOperation({ summary: 'Digital check-out' }) + @ApiParam({ name: 'checkinId', type: 'string' }) + @ApiResponse({ status: 200, description: 'Check-out completed successfully', type: HotelCheckin }) + digitalCheckout(@Param('checkinId') checkinId: string) { + return this.hotelService.digitalCheckout(checkinId); + } + + @Get('checkin/:checkinId') + @ApiOperation({ summary: 'Get check-in details' }) + @ApiParam({ name: 'checkinId', type: 'string' }) + @ApiResponse({ status: 200, type: HotelCheckin }) + getCheckinDetails(@Param('checkinId') checkinId: string) { + return this.hotelService.findCheckinById(checkinId); + } + + @Get('guest/:guestId/history') + @ApiOperation({ summary: 'Get guest stay history' }) + @ApiParam({ name: 'guestId', type: 'string' }) + getGuestHistory(@Param('guestId') guestId: string) { + return this.hotelService.getGuestHistory(guestId); + } + + @Patch('checkin/:checkinId/preferences') + @ApiOperation({ summary: 'Update guest preferences' }) + @ApiParam({ name: 'checkinId', type: 'string' }) + updateGuestPreferences( + @Param('checkinId') checkinId: string, + @Body() body: { preferences: Record }, + ) { + return this.hotelService.updateGuestPreferences(checkinId, body.preferences); + } + + // ROOM SERVICE MANAGEMENT + @Post('room-service') + @ApiOperation({ summary: 'Create room service request' }) + @ApiResponse({ status: 201, description: 'Room service request created', type: HotelServiceEntity }) + createRoomServiceRequest(@Body() requestDto: RoomServiceRequestDto) { + return this.hotelService.createRoomServiceRequest(requestDto); + } + + @Get('room-service/:serviceId') + @ApiOperation({ summary: 'Get room service details' }) + @ApiParam({ name: 'serviceId', type: 'string' }) + @ApiResponse({ status: 200, type: HotelServiceEntity }) + getRoomServiceDetails(@Param('serviceId') serviceId: string) { + return this.hotelService.findServiceById(serviceId); + } + + @Get('rooms/:roomId/services') + @ApiOperation({ summary: 'Get room service requests' }) + @ApiParam({ name: 'roomId', type: 'string' }) + @ApiQuery({ name: 'status', required: false, type: String }) + getRoomServices( + @Param('roomId') roomId: string, + @Query('status') status?: string, + ) { + return this.hotelService.getRoomServices(roomId, status); + } + + @Patch('room-service/:serviceId/status') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Update room service status' }) + @ApiParam({ name: 'serviceId', type: 'string' }) + updateRoomServiceStatus( + @Param('serviceId') serviceId: string, + @Body() body: { + status: string; + staffNotes?: string; + estimatedCompletion?: string; + }, + ) { + const estimatedCompletion = body.estimatedCompletion + ? new Date(body.estimatedCompletion) + : undefined; + + return this.hotelService.updateServiceStatus( + serviceId, + body.status, + body.staffNotes, + estimatedCompletion + ); + } + + // DIGITAL KEY ACCESS + @Get('digital-key/:checkinId') + @ApiOperation({ summary: 'Get digital key information' }) + @ApiParam({ name: 'checkinId', type: 'string' }) + async getDigitalKey(@Param('checkinId') checkinId: string, @Request() req) { + const checkin = await this.hotelService.findCheckinById(checkinId); + + // Verify user owns this checkin + if (checkin.guestId !== req.user.id) { + throw new Error('Unauthorized access to digital key'); + } + + return { + digitalKey: checkin.digitalKey, + roomNumber: checkin.room?.roomNumber, + validFrom: checkin.checkinDate, + validUntil: checkin.checkoutDate, + lastAccess: new Date(), + }; + } + + // HOTEL ANALYTICS + @Get('establishments/:establishmentId/stats') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Get hotel statistics' }) + @ApiParam({ name: 'establishmentId', type: 'string' }) + getHotelStats(@Param('establishmentId') establishmentId: string) { + return this.hotelService.getHotelStats(establishmentId); + } + + // HOUSEKEEPING DASHBOARD + @Get('establishments/:establishmentId/housekeeping') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Get housekeeping dashboard data' }) + @ApiParam({ name: 'establishmentId', type: 'string' }) + async getHousekeepingDashboard(@Param('establishmentId') establishmentId: string) { + const rooms = await this.hotelService.getRoomsByEstablishment(establishmentId); + + return { + rooms: rooms.map(room => ({ + ...room, + status: room.isAvailable ? 'clean' : 'occupied', + })), + pendingServices: [], // TODO: Implement pending services query + todayCheckouts: [], // TODO: Get today's checkouts + todayCheckins: [], // TODO: Get today's checkins + }; + } + + // CONCIERGE FEATURES + @Get('establishments/:establishmentId/amenities') + @ApiOperation({ summary: 'Get hotel amenities and services' }) + @ApiParam({ name: 'establishmentId', type: 'string' }) + getHotelAmenities(@Param('establishmentId') establishmentId: string) { + // This could be expanded to include amenity booking, spa services, etc. + return { + amenities: [ + { name: 'Swimming Pool', available: true, hours: '6:00 AM - 10:00 PM' }, + { name: 'Fitness Center', available: true, hours: '24/7' }, + { name: 'Spa Services', available: true, hours: '8:00 AM - 8:00 PM' }, + { name: 'Restaurant', available: true, hours: '6:00 AM - 11:00 PM' }, + { name: 'Room Service', available: true, hours: '24/7' }, + { name: 'Laundry Service', available: true, hours: '7:00 AM - 7:00 PM' }, + ], + services: [ + { name: 'Airport Shuttle', price: 25, currency: 'USD' }, + { name: 'Car Rental', price: 45, currency: 'USD' }, + { name: 'Tour Booking', price: 0, currency: 'USD' }, + { name: 'Currency Exchange', price: 0, currency: 'USD' }, + ], + }; + } +} diff --git a/src/modules/hotel/hotel.module.ts b/src/modules/hotel/hotel.module.ts new file mode 100644 index 0000000..3e8c129 --- /dev/null +++ b/src/modules/hotel/hotel.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { HotelService } from './hotel.service'; +import { HotelController } from './hotel.controller'; +import { HotelRoom } from '../../entities/hotel-room.entity'; +import { HotelCheckin } from '../../entities/hotel-checkin.entity'; +import { HotelService as HotelServiceEntity } from '../../entities/hotel-service.entity'; +import { Reservation } from '../../entities/reservation.entity'; +import { NotificationsModule } from '../notifications/notifications.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + HotelRoom, + HotelCheckin, + HotelServiceEntity, + Reservation, + ]), + NotificationsModule, + ], + controllers: [HotelController], + providers: [HotelService], + exports: [HotelService], +}) +export class HotelModule {} diff --git a/src/modules/hotel/hotel.service.ts b/src/modules/hotel/hotel.service.ts new file mode 100755 index 0000000..7e3fe30 --- /dev/null +++ b/src/modules/hotel/hotel.service.ts @@ -0,0 +1,389 @@ +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { HotelRoom } from '../../entities/hotel-room.entity'; +import { HotelCheckin } from '../../entities/hotel-checkin.entity'; +import { HotelService as HotelServiceEntity } from '../../entities/hotel-service.entity'; +import { Reservation } from '../../entities/reservation.entity'; +import { CreateHotelCheckinDto } from './dto/create-hotel-checkin.dto'; +import { RoomServiceRequestDto } from './dto/room-service-request.dto'; +import { v4 as uuidv4 } from 'uuid'; +import { NotificationsService } from '../notifications/notifications.service'; +import { NotificationType, NotificationCategory } from '../notifications/dto/create-notification.dto'; + +@Injectable() +export class HotelService { + constructor( + @InjectRepository(HotelRoom) + private readonly hotelRoomRepository: Repository, + @InjectRepository(HotelCheckin) + private readonly hotelCheckinRepository: Repository, + @InjectRepository(HotelServiceEntity) + private readonly hotelServiceRepository: Repository, + @InjectRepository(Reservation) + private readonly reservationRepository: Repository, + private readonly notificationsService: NotificationsService, + ) {} + + // ROOM MANAGEMENT + async updateRoomAvailability(roomId: string, isAvailable: boolean): Promise { + await this.hotelRoomRepository.update(roomId, { isAvailable }); + const room = await this.hotelRoomRepository.findOne({ where: { id: roomId } }); + if (!room) { + throw new NotFoundException(`Room with ID ${roomId} not found`); + } + return room; + } + + async getRoomsByEstablishment( + establishmentId: string, + isAvailable?: boolean, + roomType?: string, + ): Promise { + const query = this.hotelRoomRepository.createQueryBuilder('room') + .where('room.establishmentId = :establishmentId', { establishmentId }); + + if (isAvailable !== undefined) { + query.andWhere('room.isAvailable = :isAvailable', { isAvailable }); + } + + if (roomType) { + query.andWhere('room.roomType = :roomType', { roomType }); + } + + return query.orderBy('room.roomNumber', 'ASC').getMany(); + } + + // CHECK-IN/CHECK-OUT MANAGEMENT + async digitalCheckin(createCheckinDto: CreateHotelCheckinDto): Promise { + // Verify reservation exists + const reservation = await this.reservationRepository.findOne({ + where: { id: createCheckinDto.reservationId }, + relations: ['user'], + }); + + if (!reservation) { + throw new NotFoundException('Reservation not found'); + } + + // Generate digital key + const digitalKey = this.generateDigitalKey(); + + // Create check-in record + const checkin = this.hotelCheckinRepository.create({ + ...createCheckinDto, + digitalKey, + status: 'checked-in', + actualCheckinTime: new Date(), + accessLog: [{ + action: 'check-in', + timestamp: new Date(), + method: 'digital', + }], + }); + + const savedCheckin = await this.hotelCheckinRepository.save(checkin); + + // Update room availability + await this.updateRoomAvailability(createCheckinDto.roomId, false); + + // Send welcome notification + await this.notificationsService.createNotification({ + userId: createCheckinDto.guestId, + type: NotificationType.PUSH, + category: NotificationCategory.BOOKING, + title: '🏨 Welcome to Your Hotel!', + message: `Your digital key is ready. Room ${savedCheckin.room?.roomNumber || 'assigned'} is now accessible.`, + data: { digitalKey, checkinId: savedCheckin.id }, + }); + + return this.findCheckinById(savedCheckin.id); + } + + async digitalCheckout(checkinId: string): Promise { + const checkin = await this.findCheckinById(checkinId); + + if (checkin.status !== 'checked-in') { + throw new BadRequestException('Invalid check-in status for checkout'); + } + + // Update check-in record + await this.hotelCheckinRepository.update(checkinId, { + status: 'checked-out', + actualCheckoutTime: new Date(), + accessLog: [ + ...checkin.accessLog, + { + action: 'check-out', + timestamp: new Date(), + method: 'digital', + }, + ], + }); + + // Update room availability + await this.updateRoomAvailability(checkin.roomId, true); + + // Send checkout confirmation + await this.notificationsService.createNotification({ + userId: checkin.guestId, + type: NotificationType.PUSH, + category: NotificationCategory.BOOKING, + title: '👋 Thank You for Your Stay!', + message: 'Checkout completed successfully. We hope you enjoyed your stay with us!', + data: { checkinId, checkoutTime: new Date() }, + }); + + return this.findCheckinById(checkinId); + } + + async findCheckinById(id: string): Promise { + const checkin = await this.hotelCheckinRepository.findOne({ + where: { id }, + relations: ['room', 'guest'], + }); + + if (!checkin) { + throw new NotFoundException(`Check-in with ID ${id} not found`); + } + + return checkin; + } + + // ROOM SERVICE MANAGEMENT + async createRoomServiceRequest(requestDto: RoomServiceRequestDto): Promise { + const service = this.hotelServiceRepository.create({ + ...requestDto, + status: 'pending', + }); + + const savedService = await this.hotelServiceRepository.save(service); + + // Notify hotel staff + // TODO: Implement staff notification system + + // Notify guest + await this.notificationsService.createNotification({ + userId: requestDto.guestId, + type: NotificationType.PUSH, + category: NotificationCategory.BOOKING, + title: '🛎️ Room Service Request Received', + message: `Your ${requestDto.serviceType} request has been received and will be processed shortly.`, + data: { serviceId: savedService.id, serviceType: requestDto.serviceType }, + }); + + return this.findServiceById(savedService.id); + } + + async updateServiceStatus( + serviceId: string, + status: string, + staffNotes?: string, + estimatedCompletion?: Date, + ): Promise { + const updateData: any = { status }; + + if (staffNotes) { + updateData.serviceNotes = staffNotes; + } + + if (estimatedCompletion) { + updateData.estimatedCompletion = estimatedCompletion; + } + + if (status === 'completed') { + updateData.completedAt = new Date(); + } + + await this.hotelServiceRepository.update(serviceId, updateData); + + const service = await this.findServiceById(serviceId); + + // Notify guest of status update + const statusMessages = { + assigned: 'Your request has been assigned to our staff', + 'in-progress': 'Our team is working on your request', + completed: 'Your service request has been completed', + cancelled: 'Your service request has been cancelled', + }; + + if (statusMessages[status]) { + await this.notificationsService.createNotification({ + userId: service.guestId, + type: NotificationType.PUSH, + category: NotificationCategory.BOOKING, + title: '🛎️ Service Update', + message: statusMessages[status], + data: { serviceId, status }, + }); + } + + return service; + } + + async findServiceById(id: string): Promise { + const service = await this.hotelServiceRepository.findOne({ + where: { id }, + relations: ['room', 'guest'], + }); + + if (!service) { + throw new NotFoundException(`Service with ID ${id} not found`); + } + + return service; + } + + async getRoomServices( + roomId: string, + status?: string, + ): Promise { + const query = this.hotelServiceRepository.createQueryBuilder('service') + .leftJoinAndSelect('service.guest', 'guest') + .where('service.roomId = :roomId', { roomId }); + + if (status) { + query.andWhere('service.status = :status', { status }); + } + + return query + .orderBy('service.createdAt', 'DESC') + .getMany(); + } + + // GUEST PREFERENCES & CRM + async updateGuestPreferences( + checkinId: string, + preferences: Record, + ): Promise { + await this.hotelCheckinRepository.update(checkinId, { + guestPreferences: preferences, + }); + + return this.findCheckinById(checkinId); + } + + async getGuestHistory(guestId: string): Promise<{ + stays: HotelCheckin[]; + preferences: Record; + totalStays: number; + favoriteRoomTypes: string[]; + }> { + const stays = await this.hotelCheckinRepository.find({ + where: { guestId }, + relations: ['room'], + order: { createdAt: 'DESC' }, + }); + + // Aggregate preferences from all stays + const allPreferences: Record = {}; + const roomTypes: string[] = []; + + stays.forEach(stay => { + if (stay.guestPreferences) { + Object.assign(allPreferences, stay.guestPreferences); + } + if (stay.room?.roomType) { + roomTypes.push(stay.room.roomType as string); + } + }); + + // Find most frequent room types + const roomTypeCount: Record = {}; + roomTypes.forEach(type => { + roomTypeCount[type] = (roomTypeCount[type] || 0) + 1; + }); + + const favoriteRoomTypes = Object.entries(roomTypeCount) + .sort(([, a], [, b]) => (b as number) - (a as number)) + .slice(0, 3) + .map(([type]) => type); + + return { + stays, + preferences: allPreferences, + totalStays: stays.length, + favoriteRoomTypes, + }; + } + + // HOTEL ANALYTICS + async getHotelStats(establishmentId: string): Promise<{ + occupancyRate: number; + averageStayDuration: number; + totalRevenue: number; + activeServices: number; + checkinsTodayCheck: number; + checkoutsTodayCheck: number; + roomTypes: Array<{ type: string; total: number; occupied: number }>; + }> { + const today = new Date().toISOString().split('T')[0]; + + const [ + totalRooms, + occupiedRooms, + checkinsTodayCheck, + checkoutsTodayCheck, + activeServices, + ] = await Promise.all([ + this.hotelRoomRepository.count({ where: { establishmentId } }), + this.hotelRoomRepository.count({ where: { establishmentId, isAvailable: false } }), + this.hotelCheckinRepository.count({ + where: { + room: { establishmentId }, + actualCheckinTime: new Date(today), + }, + }), + this.hotelCheckinRepository.count({ + where: { + room: { establishmentId }, + actualCheckoutTime: new Date(today), + }, + }), + this.hotelServiceRepository.count({ + where: { + room: { establishmentId }, + status: 'pending', + }, + }), + ]); + + // Room types analysis + const roomTypes = await this.hotelRoomRepository + .createQueryBuilder('room') + .select('room.roomType', 'type') + .addSelect('COUNT(*)', 'total') + .addSelect('SUM(CASE WHEN room.isAvailable = false THEN 1 ELSE 0 END)', 'occupied') + .where('room.establishmentId = :establishmentId', { establishmentId }) + .groupBy('room.roomType') + .getRawMany(); + + // Calculate average stay duration and revenue + const stayStats = await this.hotelCheckinRepository + .createQueryBuilder('checkin') + .leftJoin('checkin.room', 'room') + .select('AVG(EXTRACT(EPOCH FROM (checkin.checkoutDate - checkin.checkinDate))/86400)', 'avgDuration') + .where('room.establishmentId = :establishmentId', { establishmentId }) + .andWhere('checkin.status = :status', { status: 'checked-out' }) + .getRawOne(); + + return { + occupancyRate: totalRooms > 0 ? (occupiedRooms / totalRooms) * 100 : 0, + averageStayDuration: parseFloat(stayStats?.avgDuration || '0'), + totalRevenue: 0, // TODO: Calculate from reservations/transactions + activeServices, + checkinsTodayCheck, + checkoutsTodayCheck, + roomTypes: roomTypes.map(rt => ({ + type: rt.type, + total: parseInt(rt.total), + occupied: parseInt(rt.occupied), + })), + }; + } + + private generateDigitalKey(): string { + // Generate a secure digital key + return `DK-${Date.now()}-${uuidv4().substr(0, 8).toUpperCase()}`; + } +} diff --git a/src/modules/iot-tourism/dto/device-reading.dto.ts b/src/modules/iot-tourism/dto/device-reading.dto.ts new file mode 100644 index 0000000..8331394 --- /dev/null +++ b/src/modules/iot-tourism/dto/device-reading.dto.ts @@ -0,0 +1,52 @@ +import { IsString, IsNumber, IsOptional, IsBoolean, IsDateString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class DeviceReadingDto { + @ApiProperty({ description: 'Device ID' }) + @IsString() + deviceId: string; + + @ApiProperty({ description: 'Reading timestamp' }) + @IsDateString() + timestamp: string; + + @ApiPropertyOptional({ description: 'Crowd density (people per m²)' }) + @IsOptional() + @IsNumber() + crowdDensity?: number; + + @ApiPropertyOptional({ description: 'Air quality index (0-500)' }) + @IsOptional() + @IsNumber() + airQualityIndex?: number; + + @ApiPropertyOptional({ description: 'Noise level (decibels)' }) + @IsOptional() + @IsNumber() + noiseLevel?: number; + + @ApiPropertyOptional({ description: 'Temperature (celsius)' }) + @IsOptional() + @IsNumber() + temperature?: number; + + @ApiPropertyOptional({ description: 'Humidity (percentage)' }) + @IsOptional() + @IsNumber() + humidity?: number; + + @ApiPropertyOptional({ description: 'Parking occupancy (percentage)' }) + @IsOptional() + @IsNumber() + parkingOccupancy?: number; + + @ApiPropertyOptional({ description: 'WiFi connections count' }) + @IsOptional() + @IsNumber() + wifiConnections?: number; + + @ApiPropertyOptional({ description: 'Energy consumption (kWh)' }) + @IsOptional() + @IsNumber() + energyConsumption?: number; +} diff --git a/src/modules/iot-tourism/dto/wearable-sync.dto.ts b/src/modules/iot-tourism/dto/wearable-sync.dto.ts new file mode 100644 index 0000000..369b12e --- /dev/null +++ b/src/modules/iot-tourism/dto/wearable-sync.dto.ts @@ -0,0 +1,48 @@ +import { IsString, IsNumber, IsOptional, IsBoolean } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class WearableSyncDto { + @ApiProperty({ description: 'Device identifier' }) + @IsString() + deviceIdentifier: string; + + @ApiPropertyOptional({ description: 'Heart rate (BPM)' }) + @IsOptional() + @IsNumber() + heartRate?: number; + + @ApiPropertyOptional({ description: 'Step count' }) + @IsOptional() + @IsNumber() + stepCount?: number; + + @ApiPropertyOptional({ description: 'Calories burned' }) + @IsOptional() + @IsNumber() + caloriesBurned?: number; + + @ApiPropertyOptional({ description: 'Distance walked (meters)' }) + @IsOptional() + @IsNumber() + distanceWalked?: number; + + @ApiPropertyOptional({ description: 'Current latitude' }) + @IsOptional() + @IsNumber() + latitude?: number; + + @ApiPropertyOptional({ description: 'Current longitude' }) + @IsOptional() + @IsNumber() + longitude?: number; + + @ApiPropertyOptional({ description: 'Battery level (percentage)' }) + @IsOptional() + @IsNumber() + batteryLevel?: number; + + @ApiPropertyOptional({ description: 'Is device connected' }) + @IsOptional() + @IsBoolean() + isConnected?: boolean; +} diff --git a/src/modules/iot-tourism/iot-tourism.controller.ts b/src/modules/iot-tourism/iot-tourism.controller.ts new file mode 100644 index 0000000..3cda1b3 --- /dev/null +++ b/src/modules/iot-tourism/iot-tourism.controller.ts @@ -0,0 +1,111 @@ +import { + Controller, Get, Post, Body, Patch, Param, Query, UseGuards, Request +} from '@nestjs/common'; +import { + ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam +} from '@nestjs/swagger'; +import { IoTTourismService } from './iot-tourism.service'; +import { DeviceReadingDto } from './dto/device-reading.dto'; +import { WearableSyncDto } from './dto/wearable-sync.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; + +@ApiTags('IoT Tourism & 5G Connectivity') +@Controller('iot-tourism') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class IoTTourismController { + constructor(private readonly iotTourismService: IoTTourismService) {} + + @Post('devices/register') + @UseGuards(RolesGuard) + @Roles('admin') + @ApiOperation({ summary: 'Register new IoT device (Admin only)' }) + @ApiResponse({ status: 201, description: 'Device registered successfully' }) + registerDevice(@Body() deviceData: { + deviceId: string; + deviceName: string; + deviceType: string; + location: any; + specifications: any; + placeId?: string; + }) { + return this.iotTourismService.registerIoTDevice(deviceData); + } + + @Post('devices/readings') + @ApiOperation({ summary: 'Submit sensor reading from IoT device' }) + @ApiResponse({ status: 201, description: 'Reading processed successfully' }) + submitReading(@Body() readingDto: DeviceReadingDto) { + return this.iotTourismService.submitDeviceReading(readingDto); + } + + @Get('dashboard') + @UseGuards(RolesGuard) + @Roles('admin') + @ApiOperation({ summary: 'Get smart tourism dashboard (Admin only)' }) + @ApiResponse({ status: 200, description: 'Dashboard data retrieved' }) + getDashboard() { + return this.iotTourismService.getSmartTourismDashboard(); + } + + @Post('wearables/sync') + @ApiOperation({ summary: 'Sync data from wearable devices' }) + @ApiResponse({ status: 200, description: 'Wearable data synced successfully' }) + syncWearable(@Body() syncDto: WearableSyncDto, @Request() req) { + return this.iotTourismService.syncWearableDevice(req.user.id, syncDto); + } + + @Get('5g-connectivity') + @ApiOperation({ summary: 'Get 5G connectivity map and coverage' }) + @ApiResponse({ status: 200, description: '5G connectivity info retrieved' }) + get5GConnectivity() { + return { + coverageAreas: [ + { + location: { lat: 18.4861, lng: -69.9312 }, + signalStrength: 95, + networkType: '5G', + speed: { download: 850, upload: 180, latency: 8 }, + } + ], + speedTestResults: [ + { location: 'Colonial Zone', avgDownload: 850, avgUpload: 180 } + ], + networkQuality: { + overall: 'excellent', + coverage: 96.2, + reliability: 98.5, + }, + }; + } + + @Get('smart-parking') + @ApiOperation({ summary: 'Get smart parking information' }) + @ApiQuery({ name: 'latitude', required: true, type: Number }) + @ApiQuery({ name: 'longitude', required: true, type: Number }) + getSmartParking( + @Query('latitude') latitude: number, + @Query('longitude') longitude: number, + ) { + return { + nearbyParking: [ + { + id: '1', + name: 'Plaza Central Parking', + occupancy: 65, + distance: 200, + pricePerHour: 5, + features: ['covered', 'ev-charging'] + } + ], + realTimeAvailability: { + totalSpaces: 500, + availableSpaces: 175, + averageOccupancy: 65, + }, + recommendations: ['Best availability nearby at Plaza Central'] + }; + } +} diff --git a/src/modules/iot-tourism/iot-tourism.module.ts b/src/modules/iot-tourism/iot-tourism.module.ts new file mode 100644 index 0000000..c724856 --- /dev/null +++ b/src/modules/iot-tourism/iot-tourism.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { IoTTourismService } from './iot-tourism.service'; +import { IoTTourismController } from './iot-tourism.controller'; +import { IoTDevice } from '../../entities/iot-device.entity'; +import { SmartTourismData } from '../../entities/smart-tourism-data.entity'; +import { WearableDevice } from '../../entities/wearable-device.entity'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + IoTDevice, + SmartTourismData, + WearableDevice, + PlaceOfInterest, + ]), + ], + controllers: [IoTTourismController], + providers: [IoTTourismService], + exports: [IoTTourismService], +}) +export class IoTTourismModule {} diff --git a/src/modules/iot-tourism/iot-tourism.service.ts b/src/modules/iot-tourism/iot-tourism.service.ts new file mode 100644 index 0000000..1befbd7 --- /dev/null +++ b/src/modules/iot-tourism/iot-tourism.service.ts @@ -0,0 +1,340 @@ +import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, MoreThan } from 'typeorm'; +import { IoTDevice } from '../../entities/iot-device.entity'; +import { SmartTourismData } from '../../entities/smart-tourism-data.entity'; +import { WearableDevice } from '../../entities/wearable-device.entity'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { DeviceReadingDto } from './dto/device-reading.dto'; +import { WearableSyncDto } from './dto/wearable-sync.dto'; + +@Injectable() +export class IoTTourismService { + constructor( + @InjectRepository(IoTDevice) + private readonly deviceRepository: Repository, + @InjectRepository(SmartTourismData) + private readonly dataRepository: Repository, + @InjectRepository(WearableDevice) + private readonly wearableRepository: Repository, + @InjectRepository(PlaceOfInterest) + private readonly placeRepository: Repository, + ) {} + + async registerIoTDevice(deviceData: { + deviceId: string; + deviceName: string; + deviceType: string; + location: any; + specifications: any; + placeId?: string; + }): Promise { + const existingDevice = await this.deviceRepository.findOne({ + where: { deviceId: deviceData.deviceId }, + }); + + if (existingDevice) { + throw new BadRequestException('Device already registered'); + } + + const device = this.deviceRepository.create({ + deviceId: deviceData.deviceId, + deviceName: deviceData.deviceName, + deviceType: deviceData.deviceType, + status: 'active', + location: deviceData.location, + specifications: deviceData.specifications, + currentReadings: { + timestamp: new Date(), + }, + configuration: { + sampleRate: 60, + alertThresholds: { + crowdDensity: { min: 0, max: 5 }, + airQualityIndex: { min: 0, max: 150 }, + noiseLevel: { min: 0, max: 70 }, + temperature: { min: 15, max: 35 }, + }, + dataRetentionPeriod: 30, + transmissionFrequency: 5, + lowPowerMode: false, + geofenceRadius: 100, + }, + placeId: deviceData.placeId, + healthScore: 100, + }); + + return this.deviceRepository.save(device); + } + + async submitDeviceReading(readingDto: DeviceReadingDto): Promise<{ + success: boolean; + insights: any; + alerts: any[]; + }> { + const device = await this.deviceRepository.findOne({ + where: { deviceId: readingDto.deviceId }, + }); + + if (!device) { + throw new NotFoundException('Device not found'); + } + + const insights = this.processSensorData(readingDto); + const anomalyDetection = this.detectAnomalies(readingDto, device); + + device.currentReadings = { + timestamp: new Date(readingDto.timestamp), + crowdDensity: readingDto.crowdDensity, + airQualityIndex: readingDto.airQualityIndex, + noiseLevel: readingDto.noiseLevel, + temperature: readingDto.temperature, + humidity: readingDto.humidity, + parkingOccupancy: readingDto.parkingOccupancy, + wifiConnections: readingDto.wifiConnections, + energyConsumption: readingDto.energyConsumption, + }; + + await this.deviceRepository.save(device); + + const dataRecord = this.dataRepository.create({ + deviceId: readingDto.deviceId, + timestamp: new Date(readingDto.timestamp), + sensorData: { + crowdDensity: readingDto.crowdDensity, + airQualityIndex: readingDto.airQualityIndex, + noiseLevel: readingDto.noiseLevel, + temperature: readingDto.temperature, + humidity: readingDto.humidity, + parkingOccupancy: readingDto.parkingOccupancy, + wifiConnections: readingDto.wifiConnections, + energyConsumption: readingDto.energyConsumption, + }, + insights, + dataQuality: this.calculateDataQuality(readingDto), + isAnomaly: anomalyDetection.isAnomaly, + anomalyDetails: anomalyDetection.details, + }); + + await this.dataRepository.save(dataRecord); + + return { + success: true, + insights, + alerts: anomalyDetection.alerts, + }; + } + + async getSmartTourismDashboard(): Promise<{ + overview: any; + realTimeData: any; + alerts: any[]; + recommendations: string[]; + deviceStatus: any; + }> { + const now = new Date(); + const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); + + const recentData = await this.dataRepository.find({ + where: { timestamp: MoreThan(oneHourAgo) }, + relations: ['device'], + order: { timestamp: 'DESC' }, + take: 100, + }); + + const activeDevices = await this.deviceRepository.count({ + where: { status: 'active' }, + }); + + const overview = { + totalDevices: await this.deviceRepository.count(), + activeDevices, + dataPointsLastHour: recentData.length, + networkCoverage: 95.8, + systemHealth: this.calculateSystemHealth(recentData), + }; + + const realTimeData = this.aggregateRealTimeData(recentData); + const alerts = this.generateCurrentAlerts(recentData); + const recommendations = this.generateSmartRecommendations(recentData); + const deviceStatus = await this.getDeviceHealthSummary(); + + return { + overview, + realTimeData, + alerts, + recommendations, + deviceStatus, + }; + } + + async syncWearableDevice( + userId: string, + syncDto: WearableSyncDto, + ): Promise<{ + success: boolean; + tourGuidance: any; + healthInsights: any; + emergencyStatus: any; + }> { + let wearable = await this.wearableRepository.findOne({ + where: { userId, deviceIdentifier: syncDto.deviceIdentifier }, + }); + + if (!wearable) { + wearable = this.wearableRepository.create({ + userId, + deviceIdentifier: syncDto.deviceIdentifier, + deviceType: 'smartwatch', + deviceInfo: { + brand: 'Unknown', + model: 'Unknown', + osVersion: '1.0', + appVersion: '1.0', + batteryLevel: syncDto.batteryLevel || 100, + isConnected: syncDto.isConnected || true, + }, + healthData: { + heartRate: syncDto.heartRate || 70, + stepCount: syncDto.stepCount || 0, + caloriesBurned: syncDto.caloriesBurned || 0, + distanceWalked: syncDto.distanceWalked || 0, + activityLevel: 'light', + stressLevel: 20, + hydrationReminders: true, + }, + preferences: { + notificationsEnabled: true, + vibrationEnabled: true, + audioGuidance: true, + languagePreference: 'en', + hapticFeedback: true, + emergencyContacts: [], + privacySettings: { + shareLocation: true, + shareHealthData: false, + shareWithTourGroup: false, + }, + }, + smartFeatures: { + gpsTracking: true, + heartRateMonitoring: true, + fallDetection: true, + sosButton: true, + nfcPayment: false, + cameraControl: true, + voiceCommands: true, + augmentedReality: false, + }, + connectivityStatus: { + lastSync: new Date(), + connectionType: 'bluetooth', + signalStrength: 85, + dataUsage: 0, + isOnline: true, + networkQuality: 'good', + }, + }); + } + + await this.wearableRepository.save(wearable); + + const tourGuidance = await this.generateTourGuidance(wearable, syncDto); + const healthInsights = this.generateHealthInsights(wearable); + const emergencyStatus = this.checkEmergencyStatus(wearable); + + return { + success: true, + tourGuidance, + healthInsights, + emergencyStatus, + }; + } + + // PRIVATE HELPER METHODS + private processSensorData(reading: DeviceReadingDto): any { + return { + crowdLevel: 'moderate', + comfortIndex: 75, + recommendations: ['Visit during off-peak hours'], + alerts: [], + predictedTrends: [], + }; + } + + private detectAnomalies(reading: DeviceReadingDto, device: IoTDevice): any { + return { + isAnomaly: false, + alerts: [], + details: null, + }; + } + + private calculateDataQuality(reading: DeviceReadingDto): number { + return 95.5; + } + + private calculateSystemHealth(recentData: SmartTourismData[]): number { + return 98.2; + } + + private aggregateRealTimeData(data: SmartTourismData[]): any { + return { + crowdLevel: 'moderate', + averageTemperature: 26, + airQuality: 85, + parkingAvailability: 75, + lastUpdated: new Date(), + }; + } + + private generateCurrentAlerts(data: SmartTourismData[]): any[] { + return []; + } + + private generateSmartRecommendations(data: SmartTourismData[]): string[] { + return ['Perfect conditions for outdoor activities!']; + } + + private async getDeviceHealthSummary(): Promise { + return { + total: 50, + active: 48, + maintenance: 2, + error: 0, + averageHealth: 96.5, + }; + } + + private async generateTourGuidance(wearable: WearableDevice, syncDto: WearableSyncDto): Promise { + return { + nextWaypoint: { + name: 'Catedral Primada', + distance: 150, + description: 'First cathedral built in the Americas', + }, + estimatedArrival: '5 minutes walking', + suggestions: ['Take a photo at the cathedral steps'], + healthAlerts: [], + }; + } + + private generateHealthInsights(wearable: WearableDevice): any { + return { + fitnessLevel: 'active', + hydrationReminder: false, + restSuggestion: false, + achievements: ['10K Steps'], + insights: ['You are doing great!'], + }; + } + + private checkEmergencyStatus(wearable: WearableDevice): any { + return { + status: 'normal', + alerts: [], + emergencyContacts: [], + sosEnabled: true, + }; + } +} diff --git a/src/modules/listings/dto/create-listing.dto.ts b/src/modules/listings/dto/create-listing.dto.ts new file mode 100644 index 0000000..723cd46 --- /dev/null +++ b/src/modules/listings/dto/create-listing.dto.ts @@ -0,0 +1,98 @@ +import { +IsString, IsNotEmpty, IsEnum, IsOptional, IsNumber, Min, IsArray, ValidateNested, +IsDateString, Matches, IsObject +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ChannelType } from '../../channel-management/dto/create-channel.dto'; +class ImageDto { +@ApiProperty({ example: 'https://example.com/image.jpg' }) +@IsString() +url: string; +@ApiPropertyOptional({ example: 'Main view of the resort' }) +@IsOptional() +@IsString() +altText?: string; +} +export class CreateListingDto { +@ApiProperty({ description: 'Listing type', enum: ChannelType }) +@IsEnum(ChannelType) +@IsNotEmpty() +listingType: ChannelType; +@ApiProperty({ description: 'Listing title', example: 'Luxury Beach Resort in Punta Cana' }) +@IsString() +@IsNotEmpty() +title: string; +@ApiProperty({ description: 'Detailed description' }) +@IsString() +@IsNotEmpty() +description: string; +@ApiPropertyOptional({ description: 'Associated establishment ID' }) +@IsString() +@IsOptional() +establishmentId?: string; +@ApiPropertyOptional({ description: 'Latitude of the property location' }) +@IsOptional() +@IsNumber() +latitude?: number; +@ApiPropertyOptional({ description: 'Longitude of the property location' }) +@IsOptional() +@IsNumber() +longitude?: number; +@ApiPropertyOptional({ description: 'Address' }) +@IsString() +@IsOptional() +address?: string; +@ApiProperty({ description: 'Base price per night/hour/day', example: 250.00 }) +@IsNumber() +@Min(0) +basePrice: number; +@ApiPropertyOptional({ description: 'Currency', example: 'USD' }) +@IsString() +@IsOptional() +currency?: string; +@ApiPropertyOptional({ description: 'Maximum capacity', example: 4 }) +@IsNumber() +@Min(1) +@IsOptional() +capacity?: number; +@ApiPropertyOptional({ description: 'List of amenities' }) +@IsArray() +@IsString({ each: true }) +@IsOptional() +amenities?: string[]; +@ApiPropertyOptional({ description: 'Property images' }) +@IsArray() +@ValidateNested({ each: true }) +@Type(() => ImageDto) +@IsOptional() +images?: ImageDto[]; +@ApiPropertyOptional({ description: 'Property rules and policies' }) +@IsObject() +@IsOptional() +policies?: Record; +@ApiPropertyOptional({ description: 'Check-in time (HH:MM format)', example: '15:00' }) +@IsString() +@Matches(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, { message: 'Check-in time must be in HH:MM format' }) +@IsOptional() +checkinTime?: string; +@ApiPropertyOptional({ description: 'Check-out time (HH:MM format)', example: '11:00' }) +@IsString() +@Matches(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, { message: 'Check-out time must be in HH:MM format' }) +@IsOptional() +checkoutTime?: string; +@ApiPropertyOptional({ description: 'Minimum stay nights', example: 2 }) +@IsNumber() +@Min(1) +@IsOptional() +minStay?: number; +@ApiPropertyOptional({ description: 'Maximum stay nights', example: 30 }) +@IsNumber() +@Min(1) +@IsOptional() +maxStay?: number; +@ApiPropertyOptional({ description: 'Channel distribution settings' }) +@IsObject() +@IsOptional() +channelSettings?: Record; +} diff --git a/src/modules/listings/dto/get-listings-filter.dto.ts b/src/modules/listings/dto/get-listings-filter.dto.ts new file mode 100644 index 0000000..6ef39cb --- /dev/null +++ b/src/modules/listings/dto/get-listings-filter.dto.ts @@ -0,0 +1,36 @@ +import { IsString, IsOptional, IsEnum, IsNumber, Min } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { ChannelType } from '../../channel-management/dto/create-channel.dto'; +export class GetListingsFilterDto { +@ApiPropertyOptional({ description: 'Filter by listing type', enum: ChannelType }) +@IsOptional() +@IsEnum(ChannelType) +type?: ChannelType; +@ApiPropertyOptional({ description: 'Search by title or description' }) +@IsOptional() +@IsString() +search?: string; +@ApiPropertyOptional({ description: 'Filter by owner ID' }) +@IsOptional() +@IsString() +ownerId?: string; +@ApiPropertyOptional({ description: 'Filter by establishment ID' }) +@IsOptional() +@IsString() +establishmentId?: string; +@ApiPropertyOptional({ description: 'Minimum rating' }) +@IsOptional() +@IsNumber() +@Min(1) +minRating?: number; +@ApiPropertyOptional({ description: 'Page number', example: 1 }) +@IsOptional() +@IsNumber() +@Min(1) +page?: number; +@ApiPropertyOptional({ description: 'Items per page', example: 10 }) +@IsOptional() +@IsNumber() +@Min(1) +limit?: number; +} diff --git a/src/modules/listings/dto/update-listing.dto.ts b/src/modules/listings/dto/update-listing.dto.ts new file mode 100644 index 0000000..a844f5c --- /dev/null +++ b/src/modules/listings/dto/update-listing.dto.ts @@ -0,0 +1,3 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateListingDto } from './create-listing.dto'; +export class UpdateListingDto extends PartialType(CreateListingDto) {} diff --git a/src/modules/listings/listings.controller.ts b/src/modules/listings/listings.controller.ts new file mode 100644 index 0000000..661804f --- /dev/null +++ b/src/modules/listings/listings.controller.ts @@ -0,0 +1,67 @@ +import { +Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, Request +} from '@nestjs/common'; +import { +ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiParam, ApiQuery +} from '@nestjs/swagger'; +import { ListingsService } from './listings.service'; +import { CreateListingDto } from './dto/create-listing.dto'; +import { UpdateListingDto } from './dto/update-listing.dto'; +import { GetListingsFilterDto } from './dto/get-listings-filter.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; +import { Listing } from '../../entities/listing.entity'; +@ApiTags('Listings Management') +@ApiBearerAuth('JWT-auth') +@UseGuards(JwtAuthGuard) +@Controller('api/v1/listings') +export class ListingsController { +constructor(private readonly listingsService: ListingsService) {} +@Post() +@UseGuards(RolesGuard) +@Roles('admin', 'establishment', 'owner') // Owners can create listings +@ApiOperation({ summary: 'Create a new property listing' }) +@ApiResponse({ status: 201, description: 'Listing created successfully', type: Listing }) +create(@Body() createListingDto: CreateListingDto, @Request() req) { +return this.listingsService.createListing(createListingDto, req.user.id); +} +@Get() +@ApiOperation({ summary: 'Get a list of all properties (hotels, restaurants, vehicles, etc.)' }) +@ApiQuery({ name: 'type', required: false, type: String, description: 'Filter by listing type (hotel, restaurant, vehicle, flight, tour)' }) +@ApiQuery({ name: 'search', required: false, type: String, description: 'Search by title or description' }) +@ApiQuery({ name: 'ownerId', required: false, type: String, description: 'Filter by owner ID' }) +@ApiQuery({ name: 'establishmentId', required: false, type: String, description: 'Filter by establishment ID' }) +@ApiQuery({ name: 'minRating', required: false, type: Number, description: 'Minimum average rating' }) +@ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number' }) +@ApiQuery({ name: 'limit', required: false, type: Number, description: 'Items per page' }) +@ApiResponse({ status: 200, type: [Listing] }) +findAll(@Query() filterDto: GetListingsFilterDto) { +return this.listingsService.findAllListings(filterDto); +} +@Get(':id') +@ApiOperation({ summary: 'Get details of a specific property by its ID' }) +@ApiParam({ name: 'id', type: 'string', description: 'Listing ID' }) +@ApiResponse({ status: 200, type: Listing }) +findOne(@Param('id') id: string) { +return this.listingsService.findListingById(id); +} +@Patch(':id') +@UseGuards(RolesGuard) +@Roles('admin', 'establishment', 'owner') +@ApiOperation({ summary: 'Update an existing property by its ID' }) +@ApiParam({ name: 'id', type: 'string', description: 'Listing ID' }) +@ApiResponse({ status: 200, description: 'Listing updated successfully', type: Listing }) +update(@Param('id') id: string, @Body() updateListingDto: UpdateListingDto) { +return this.listingsService.updateListing(id, updateListingDto); +} +@Delete(':id') +@UseGuards(RolesGuard) +@Roles('admin', 'establishment', 'owner') +@ApiOperation({ summary: 'Delete or deactivate a property by its ID' }) +@ApiParam({ name: 'id', type: 'string', description: 'Listing ID' }) +@ApiResponse({ status: 204, description: 'Listing deleted successfully' }) +remove(@Param('id') id: string) { +return this.listingsService.deleteListing(id); +} +} diff --git a/src/modules/listings/listings.module.ts b/src/modules/listings/listings.module.ts new file mode 100644 index 0000000..c46df5d --- /dev/null +++ b/src/modules/listings/listings.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ListingsService } from './listings.service'; +import { ListingsController } from './listings.controller'; +import { Listing } from '../../entities/listing.entity'; +import { User } from '../../entities/user.entity'; +import { Establishment } from '../../entities/establishment.entity'; +@Module({ +imports: [ +TypeOrmModule.forFeature([Listing, User, Establishment]), +], +controllers: [ListingsController], +providers: [ListingsService], +exports: [ListingsService], +}) +export class ListingsModule {} diff --git a/src/modules/listings/listings.service.ts b/src/modules/listings/listings.service.ts new file mode 100644 index 0000000..63ab549 --- /dev/null +++ b/src/modules/listings/listings.service.ts @@ -0,0 +1,140 @@ +import { Injectable, NotFoundException, BadRequestException, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Listing } from '../../entities/listing.entity'; +import { CreateListingDto } from './dto/create-listing.dto'; +import { UpdateListingDto } from './dto/update-listing.dto'; +import { GetListingsFilterDto } from './dto/get-listings-filter.dto'; +import { Establishment } from '../../entities/establishment.entity'; +import { User } from '../../entities/user.entity'; +import { ChannelType } from '../channel-management/dto/create-channel.dto'; + +@Injectable() +export class ListingsService { + private readonly logger = new Logger(ListingsService.name); + + constructor( + @InjectRepository(Listing) + private readonly listingRepository: Repository, + @InjectRepository(Establishment) + private readonly establishmentRepository: Repository, + @InjectRepository(User) + private readonly userRepository: Repository, + ) {} + + async createListing(createListingDto: CreateListingDto, ownerId: string): Promise { + const { latitude, longitude, images, ...rest } = createListingDto; + + const plainImages = images ? images.map(img => ({ url: img.url, altText: img.altText })) : null; + + const owner = await this.userRepository.findOne({ where: { id: ownerId } }); + if (!owner) { + throw new NotFoundException(`User with ID "${ownerId}" not found.`); + } + + if (rest.establishmentId) { + const establishment = await this.establishmentRepository.findOne({ where: { id: rest.establishmentId } }); + if (!establishment) { + throw new NotFoundException(`Establishment with ID "${rest.establishmentId}" not found.`); + } + } + + const listing = this.listingRepository.create({ + ...rest, + ownerId, + coordinates: latitude && longitude ? `POINT(${longitude} ${latitude})` : null, + images: plainImages, + status: 'draft', + reviewsCount: 0, + bookingsCount: 0, + rating: null, + } as Partial); + return this.listingRepository.save(listing); + } + + async findAllListings(filterDto: GetListingsFilterDto): Promise<{ data: Listing[]; total: number }> { + const { type, search, ownerId, establishmentId, minRating, page = 1, limit = 10 } = filterDto; + const query = this.listingRepository.createQueryBuilder('listing'); + + if (type) { + query.andWhere('listing.listingType = :type', { type }); + } + if (search) { + query.andWhere( + '(LOWER(listing.title) LIKE :search OR LOWER(listing.description) LIKE :search)', + { search: `%${search.toLowerCase()}%` }, + ); + } + if (ownerId) { + query.andWhere('listing.ownerId = :ownerId', { ownerId }); + } + if (establishmentId) { + query.andWhere('listing.establishmentId = :establishmentId', { establishmentId }); + } + if (minRating) { + query.andWhere('listing.rating >= :minRating', { minRating }); + } + + const [data, total] = await query + .skip((page - 1) * limit) + .take(limit) + .orderBy('listing.createdAt', 'DESC') + .getManyAndCount(); + + return { data, total }; + } + + async findListingById(id: string): Promise { + const listing = await this.listingRepository.findOne({ where: { id } }); + if (!listing) { + throw new NotFoundException(`Listing with ID "${id}" not found.`); + } + return listing; + } + + async updateListing(id: string, updateListingDto: UpdateListingDto): Promise { + const listing = await this.findListingById(id); + const { latitude, longitude, images, ...rest } = updateListingDto; + + const updateData: Partial = { ...rest, lastUpdated: new Date() }; + + if (images !== undefined) { + updateData.images = images.map(img => ({ url: img.url, altText: img.altText })); + } + + if (latitude !== undefined && longitude !== undefined) { + updateData.coordinates = `POINT(${longitude} ${latitude})`; + } else if (latitude === null && longitude === null) { + updateData.coordinates = null; + } + + await this.listingRepository.update(id, updateData); + return this.findListingById(id); + } + + async deleteListing(id: string): Promise { + const result = await this.listingRepository.delete(id); + if (result.affected === 0) { + throw new NotFoundException(`Listing with ID "${id}" not found.`); + } + this.logger.log(`Listing with ID "${id}" deleted.`); + } + + async updateListingStatus(id: string, status: string): Promise { + const listing = await this.findListingById(id); + listing.status = status; + return this.listingRepository.save(listing); + } + + private extractCoordsFromPoint(point: string | null): { latitude: number; longitude: number } | null { + if (!point) return null; + const match = point.match(/POINT\(([^)]+)\)/); + if (match && match[1]) { + const coords = match[1].split(' '); + if (coords.length === 2) { + return { longitude: parseFloat(coords[0]), latitude: parseFloat(coords[1]) }; + } + } + return null; + } +} diff --git a/src/modules/notifications/dto/create-notification.dto.ts b/src/modules/notifications/dto/create-notification.dto.ts new file mode 100755 index 0000000..63ef1ab --- /dev/null +++ b/src/modules/notifications/dto/create-notification.dto.ts @@ -0,0 +1,58 @@ +import { IsString, IsNotEmpty, IsOptional, IsEnum } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum NotificationType { + PUSH = 'push', + EMAIL = 'email', + WHATSAPP = 'whatsapp', + SMS = 'sms', + IN_APP = 'in-app', +} + +export enum NotificationCategory { + BOOKING = 'booking', + SECURITY = 'security', + PROMOTION = 'promotion', + SYSTEM = 'system', + ADMIN = 'admin', + ERROR = 'error', + INFO = 'info', + WARNING = 'warning', + PERSONALIZATION = 'personalization', + PAYMENT = 'payment', +} + +export class CreateNotificationDto { + @ApiPropertyOptional({ description: 'User ID to send the notification to. Use "system" or "admin" for internal notifications.' }) + @IsString() + @IsOptional() + userId?: string; + + @ApiProperty({ description: 'Type of notification', enum: NotificationType }) + @IsEnum(NotificationType) + type: NotificationType; + + @ApiProperty({ description: 'Notification title', example: 'Booking Confirmed!' }) + @IsString() + @IsNotEmpty() + title: string; + + @ApiProperty({ description: 'Notification message content', example: 'Your reservation for Hotel XYZ has been confirmed.' }) + @IsString() + @IsNotEmpty() + message: string; + + @ApiPropertyOptional({ description: 'Category of the notification', enum: NotificationCategory }) + @IsEnum(NotificationCategory) + @IsOptional() + category?: NotificationCategory; + + @ApiPropertyOptional({ description: 'Additional data relevant to the notification' }) + @IsOptional() + data?: Record; + + @ApiPropertyOptional({ description: 'Email address of the recipient for email notifications' }) + @IsOptional() + @IsString() + recipientEmail?: string; +} diff --git a/src/modules/notifications/notifications.controller.ts b/src/modules/notifications/notifications.controller.ts new file mode 100755 index 0000000..52a3029 --- /dev/null +++ b/src/modules/notifications/notifications.controller.ts @@ -0,0 +1,79 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, Request } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam } from '@nestjs/swagger'; +import { NotificationsService } from './notifications.service'; +import { CreateNotificationDto } from './dto/create-notification.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; +import { Notification } from '../../entities/notification.entity'; + +@ApiTags('Notifications') +@Controller('notifications') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class NotificationsController { + constructor(private readonly notificationsService: NotificationsService) {} + + @Post() + @UseGuards(RolesGuard) + @Roles('admin') + @ApiOperation({ summary: 'Create a notification (Admin only)' }) + @ApiResponse({ status: 201, description: 'Notification created successfully', type: Notification }) + createNotification(@Body() createNotificationDto: CreateNotificationDto) { + return this.notificationsService.createNotification(createNotificationDto); + } + + @Get('my') + @ApiOperation({ summary: 'Get current user notifications' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ name: 'isRead', required: false, type: Boolean }) + @ApiQuery({ name: 'category', required: false, type: String }) + getMyNotifications( + @Request() req, + @Query('page') page?: number, + @Query('limit') limit?: number, + @Query('isRead') isRead?: boolean, + @Query('category') category?: string, + ) { + return this.notificationsService.findUserNotifications(req.user.id, page, limit, isRead, category); + } + + @Patch(':id/read') + @ApiOperation({ summary: 'Mark notification as read' }) + @ApiParam({ name: 'id', type: 'string' }) + markAsRead(@Param('id') id: string, @Request() req) { + return this.notificationsService.markAsRead(id, req.user.id); + } + + @Patch('mark-all-read') + @ApiOperation({ summary: 'Mark all notifications as read' }) + markAllAsRead(@Request() req) { + return this.notificationsService.markAllAsRead(req.user.id); + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete notification' }) + @ApiParam({ name: 'id', type: 'string' }) + deleteNotification(@Param('id') id: string, @Request() req) { + return this.notificationsService.deleteNotification(id, req.user.id); + } + + @Post('bulk') + @UseGuards(RolesGuard) + @Roles('admin') + @ApiOperation({ summary: 'Send bulk notifications (Admin only)' }) + sendBulkNotification( + @Body() body: { userIds: string[]; notification: Omit }, + ) { + return this.notificationsService.sendBulkNotification(body.userIds, body.notification); + } + + @Get('stats') + @UseGuards(RolesGuard) + @Roles('admin') + @ApiOperation({ summary: 'Get notification statistics (Admin only)' }) + getNotificationStats() { + return this.notificationsService.getNotificationStats(); + } +} diff --git a/src/modules/notifications/notifications.module.ts b/src/modules/notifications/notifications.module.ts new file mode 100755 index 0000000..4b651cc --- /dev/null +++ b/src/modules/notifications/notifications.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { NotificationsService } from './notifications.service'; +import { NotificationsController } from './notifications.controller'; +import { Notification } from '../../entities/notification.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Notification])], + controllers: [NotificationsController], + providers: [NotificationsService], + exports: [NotificationsService], +}) +export class NotificationsModule {} diff --git a/src/modules/notifications/notifications.service.ts b/src/modules/notifications/notifications.service.ts new file mode 100755 index 0000000..3ea6510 --- /dev/null +++ b/src/modules/notifications/notifications.service.ts @@ -0,0 +1,198 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Notification } from '../../entities/notification.entity'; +import { CreateNotificationDto, NotificationType, NotificationCategory } from './dto/create-notification.dto'; + +@Injectable() +export class NotificationsService { + constructor( + @InjectRepository(Notification) + private readonly notificationRepository: Repository, + ) {} + + async createNotification(createNotificationDto: CreateNotificationDto): Promise { + const notification = this.notificationRepository.create({ + ...createNotificationDto, + sentAt: new Date(), + }); + + const savedNotification = await this.notificationRepository.save(notification); + + // TODO: Implement actual notification sending logic + // await this.sendNotification(savedNotification); + + return savedNotification; + } + + async findUserNotifications( + userId: string, + page: number = 1, + limit: number = 20, + isRead?: boolean, + category?: string, + ): Promise<{ + notifications: Notification[]; + total: number; + unreadCount: number; + }> { + const query = this.notificationRepository.createQueryBuilder('notification') + .where('notification.userId = :userId', { userId }); + + if (isRead !== undefined) { + query.andWhere('notification.isRead = :isRead', { isRead }); + } + + if (category) { + query.andWhere('notification.category = :category', { category }); + } + + const [notifications, total] = await query + .skip((page - 1) * limit) + .take(limit) + .orderBy('notification.createdAt', 'DESC') + .getManyAndCount(); + + // Get unread count + const unreadCount = await this.notificationRepository.count({ + where: { userId, isRead: false }, + }); + + return { notifications, total, unreadCount }; + } + + async markAsRead(id: string, userId: string): Promise { + const notification = await this.notificationRepository.findOne({ + where: { id, userId }, + }); + + if (!notification) { + throw new NotFoundException(`Notification with ID ${id} not found`); + } + + await this.notificationRepository.update(id, { + isRead: true, + readAt: new Date(), + }); + + const updatedNotification = await this.notificationRepository.findOne({ where: { id } }); + + if (!updatedNotification) { + throw new NotFoundException(`Notification with ID ${id} not found after update`); + } + + return updatedNotification; + } + + async markAllAsRead(userId: string): Promise { + await this.notificationRepository.update( + { userId, isRead: false }, + { isRead: true, readAt: new Date() }, + ); + } + + async deleteNotification(id: string, userId: string): Promise { + const notification = await this.notificationRepository.findOne({ + where: { id, userId }, + }); + + if (!notification) { + throw new NotFoundException(`Notification with ID ${id} not found`); + } + + await this.notificationRepository.delete(id); + } + + // Bulk notification methods + async sendBulkNotification( + userIds: string[], + createNotificationDto: Omit, + ): Promise { + const notifications = userIds.map(userId => + this.notificationRepository.create({ + ...createNotificationDto, + userId, + sentAt: new Date(), + }), + ); + + return this.notificationRepository.save(notifications); + } + + async getNotificationStats(): Promise<{ + totalSent: number; + totalRead: number; + readRate: number; + byCategory: Array<{ category: string; count: number }>; + byType: Array<{ type: string; count: number }>; + }> { + const [totalSent, totalRead] = await Promise.all([ + this.notificationRepository.count(), + this.notificationRepository.count({ where: { isRead: true } }), + ]); + + const readRate = totalSent > 0 ? (totalRead / totalSent) * 100 : 0; + + const byCategory = await this.notificationRepository + .createQueryBuilder('notification') + .select('notification.category', 'category') + .addSelect('COUNT(*)', 'count') + .groupBy('notification.category') + .getRawMany(); + + const byType = await this.notificationRepository + .createQueryBuilder('notification') + .select('notification.type', 'type') + .addSelect('COUNT(*)', 'count') + .groupBy('notification.type') + .getRawMany(); + + return { + totalSent, + totalRead, + readRate: parseFloat(readRate.toFixed(2)), + byCategory: byCategory.map(item => ({ + category: item.category || 'uncategorized', + count: parseInt(item.count), + })), + byType: byType.map(item => ({ + type: item.type, + count: parseInt(item.count), + })), + }; + } + + // Helper methods for specific notification types + async sendBookingConfirmation(userId: string, bookingData: any): Promise { + return this.createNotification({ + userId, + type: NotificationType.PUSH, + category: NotificationCategory.BOOKING, + title: 'Booking Confirmed', + message: `Your booking for ${bookingData.establishmentName} has been confirmed.`, + data: { bookingId: bookingData.id, type: 'booking_confirmation' }, + }); + } + + async sendSecurityAlert(userId: string, alertData: any): Promise { + return this.createNotification({ + userId, + type: NotificationType.PUSH, + category: NotificationCategory.SECURITY, + title: 'Security Alert', + message: alertData.message, + data: { alertId: alertData.id, type: 'security_alert' }, + }); + } + + async sendPaymentNotification(userId: string, paymentData: any): Promise { + return this.createNotification({ + userId, + type: NotificationType.PUSH, + category: NotificationCategory.PAYMENT, + title: 'Payment Processed', + message: `Payment of $${paymentData.amount} has been processed successfully.`, + data: { transactionId: paymentData.id, type: 'payment_success' }, + }); + } +} diff --git a/src/modules/payments/dto/create-payment.dto.ts b/src/modules/payments/dto/create-payment.dto.ts new file mode 100755 index 0000000..db92028 --- /dev/null +++ b/src/modules/payments/dto/create-payment.dto.ts @@ -0,0 +1,29 @@ +import { IsString, IsNotEmpty, IsNumber, Min, IsOptional } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreatePaymentDto { + @ApiProperty({ description: 'Amount for the payment', example: 50.00 }) + @IsNumber() + @Min(0) + amount: number; + + @ApiProperty({ description: 'Currency code (e.g., USD, DOP)', example: 'USD' }) + @IsString() + @IsNotEmpty() + currency: string; + + @ApiPropertyOptional({ description: 'Description of the payment' }) + @IsOptional() + @IsString() + description?: string; + + @ApiPropertyOptional({ description: 'Customer ID for recurring payments' }) + @IsOptional() + @IsString() + customerId?: string; + + @ApiPropertyOptional({ description: 'Payment method ID (e.g., Stripe token, card ID)' }) + @IsOptional() + @IsString() + paymentMethodId?: string; +} diff --git a/src/modules/payments/dto/process-payment.dto.ts b/src/modules/payments/dto/process-payment.dto.ts new file mode 100644 index 0000000..c54eedc --- /dev/null +++ b/src/modules/payments/dto/process-payment.dto.ts @@ -0,0 +1,33 @@ +import { IsString, IsNotEmpty, IsNumber, Min, IsOptional } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class ProcessPaymentDto { + @ApiProperty({ description: 'User ID initiating the payment' }) + @IsString() + @IsNotEmpty() + userId: string; + + @ApiProperty({ description: 'Amount to be paid', example: 100.50 }) + @IsNumber() + @Min(0) + amount: number; + + @ApiProperty({ description: 'Currency code (e.g., USD, DOP)', example: 'USD' }) + @IsString() + @IsNotEmpty() + currency: string; + + @ApiProperty({ description: 'Payment method ID (e.g., Stripe token, card ID)', example: 'pm_xxxxxxxxxxxxx' }) + @IsString() + @IsNotEmpty() + paymentMethodId: string; + + @ApiPropertyOptional({ description: 'Description of the payment' }) + @IsOptional() + @IsString() + description?: string; + + @ApiPropertyOptional({ description: 'Additional metadata for the payment' }) + @IsOptional() + metadata?: Record; +} diff --git a/src/modules/payments/payments.controller.ts b/src/modules/payments/payments.controller.ts new file mode 100755 index 0000000..71b48fb --- /dev/null +++ b/src/modules/payments/payments.controller.ts @@ -0,0 +1,70 @@ +import { Controller, Post, Body, Get, Param, Headers, Req, UseGuards, Delete } from '@nestjs/common'; +import { PaymentsService } from './payments.service'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiParam } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; +import { CreatePaymentDto } from './dto/create-payment.dto'; + +@ApiTags('Payments') +@ApiBearerAuth('JWT-auth') +@UseGuards(JwtAuthGuard) +@Controller('api/v1/payments') +export class PaymentsController { + constructor(private readonly paymentsService: PaymentsService) {} + + @Post('create-intent') + @ApiOperation({ summary: 'Create a payment intent (e.g., for Stripe) to initiate a payment' }) + @ApiResponse({ status: 201, description: 'Payment intent created successfully' }) + createPaymentIntent(@Body() createPaymentDto: CreatePaymentDto, @Req() req) { + return this.paymentsService.createPaymentIntent(createPaymentDto, req.user.id); + } + + @Post('confirm-payment/:paymentIntentId') + @ApiOperation({ summary: 'Confirm a payment intent' }) + @ApiParam({ name: 'paymentIntentId', description: 'ID of the payment intent' }) + @ApiResponse({ status: 200, description: 'Payment confirmed successfully' }) + confirmPayment(@Param('paymentIntentId') paymentIntentId: string) { + return this.paymentsService.confirmPayment(paymentIntentId); + } + + @Post('customer') + @ApiOperation({ summary: 'Create a payment customer (e.g., for Stripe)' }) + @ApiResponse({ status: 201, description: 'Customer created successfully' }) + createCustomer(@Body() body: { email: string; name?: string; phone?: string }) { + return this.paymentsService.createCustomer(body.email, body.name, body.phone); + } + + @Post('refund/:paymentIntentId') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Initiate a refund for a payment' }) + @ApiParam({ name: 'paymentIntentId', description: 'ID of the payment intent to refund' }) + @ApiResponse({ status: 200, description: 'Refund initiated successfully' }) + createRefund(@Param('paymentIntentId') paymentIntentId: string, @Body() body: { amount?: number; reason?: string }) { + return this.paymentsService.createRefund(paymentIntentId, body.amount, body.reason); + } + + @Get('customer/:customerId/payment-methods') + @ApiOperation({ summary: 'Retrieve payment methods for a customer' }) + @ApiParam({ name: 'customerId', description: 'ID of the customer' }) + @ApiResponse({ status: 200, description: 'Payment methods retrieved' }) + getPaymentMethods(@Param('customerId') customerId: string) { + return this.paymentsService.getPaymentMethods(customerId); + } + + @Delete('payment-method/:paymentMethodId') // Ahora Delete está importado + @ApiOperation({ summary: 'Detach a payment method from a customer' }) + @ApiParam({ name: 'paymentMethodId', description: 'ID of the payment method' }) + @ApiResponse({ status: 200, description: 'Payment method detached' }) + detachPaymentMethod(@Param('paymentMethodId') paymentMethodId: string) { + return this.paymentsService.detachPaymentMethod(paymentMethodId); + } + + @Post('webhook') + @ApiOperation({ summary: 'Handle incoming webhook events from payment providers' }) + @ApiResponse({ status: 200, description: 'Webhook handled successfully' }) + async handleWebhook(@Req() req: any, @Headers('stripe-signature') signature: string) { + return this.paymentsService.handleWebhook(req.body, signature); + } +} diff --git a/src/modules/payments/payments.module.ts b/src/modules/payments/payments.module.ts new file mode 100755 index 0000000..80eb8f1 --- /dev/null +++ b/src/modules/payments/payments.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { PaymentsService } from './payments.service'; +import { PaymentsController } from './payments.controller'; // Asegúrate de que este controlador existe +import { Transaction } from '../../entities/transaction.entity'; // Asumo que se usa +import { User } from '../../entities/user.entity'; // Asumo que se usa + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + Transaction, // Si PaymentsService interactúa con Transaction + User, // Si PaymentsService interactúa con User + ]), + ], + controllers: [PaymentsController], + providers: [PaymentsService], + exports: [PaymentsService], +}) +export class PaymentsModule {} diff --git a/src/modules/payments/payments.service.ts b/src/modules/payments/payments.service.ts new file mode 100755 index 0000000..944943f --- /dev/null +++ b/src/modules/payments/payments.service.ts @@ -0,0 +1,69 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ProcessPaymentDto } from './dto/process-payment.dto'; +import { CreatePaymentDto } from './dto/create-payment.dto'; + +@Injectable() +export class PaymentsService { + private readonly logger = new Logger(PaymentsService.name); + + // Placeholder para createPaymentIntent + async createPaymentIntent(createPaymentDto: CreatePaymentDto, userId: string): Promise { + this.logger.log(`Simulating createPaymentIntent for user ${userId} with amount ${createPaymentDto.amount}`); + return { clientSecret: 'pi_simulated_client_secret', paymentIntentId: `pi_simulated_${Date.now()}` }; + } + + // Placeholder para confirmPayment + async confirmPayment(paymentIntentId: string): Promise { + this.logger.log(`Simulating confirmPayment for ${paymentIntentId}`); + return { status: 'succeeded' }; + } + + // Placeholder para createCustomer + async createCustomer(email: string, name?: string, phone?: string): Promise { + this.logger.log(`Simulating createCustomer for ${email}`); + return { customerId: `cus_simulated_${Date.now()}` }; + } + + // Placeholder para createRefund + async createRefund(paymentIntentId: string, amount?: number, reason?: string): Promise { + this.logger.log(`Simulating createRefund for ${paymentIntentId}`); + return { status: 'succeeded', refundId: `re_simulated_${Date.now()}` }; + } + + // Placeholder para getPaymentMethods + async getPaymentMethods(customerId: string): Promise { + this.logger.log(`Simulating getPaymentMethods for customer ${customerId}`); + return [{ id: 'pm_simulated_card', type: 'card', last4: '4242' }]; + } + + // Placeholder para detachPaymentMethod + async detachPaymentMethod(paymentMethodId: string): Promise { + this.logger.log(`Simulating detachPaymentMethod for ${paymentMethodId}`); + return { detached: true }; + } + + // Placeholder para handleWebhook + async handleWebhook(body: any, signature: string): Promise { + this.logger.log(`Simulating handleWebhook: ${body.type}`); + // En un entorno real, verificar la firma y procesar el evento + return { received: true }; + } + + async processPayment(processPaymentDto: ProcessPaymentDto): Promise<{ success: boolean; message: string; transactionId?: string }> { + this.logger.log(`Processing payment for user ${processPaymentDto.userId}, amount ${processPaymentDto.amount} ${processPaymentDto.currency}`); + + const isSuccess = Math.random() > 0.1; // 90% chance of success + if (isSuccess) { + return { + success: true, + message: 'Payment processed successfully.', + transactionId: `txn_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`, + }; + } else { + return { + success: false, + message: 'Payment failed due to an simulated error.', + }; + } + } +} diff --git a/src/modules/personalization/dto/recommendation-request.dto.ts b/src/modules/personalization/dto/recommendation-request.dto.ts new file mode 100644 index 0000000..36bbb61 --- /dev/null +++ b/src/modules/personalization/dto/recommendation-request.dto.ts @@ -0,0 +1,57 @@ +import { IsOptional, IsString, IsNumber, IsEnum, IsBoolean } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export enum RecommendationType { + DESTINATIONS = 'destinations', + ACTIVITIES = 'activities', + RESTAURANTS = 'restaurants', + ACCOMMODATIONS = 'accommodations', + EXPERIENCES = 'experiences', + ITINERARIES = 'itineraries' +} + +export class RecommendationRequestDto { + @ApiPropertyOptional({ description: 'Type of recommendation', enum: RecommendationType }) + @IsOptional() + @IsEnum(RecommendationType) + type?: RecommendationType; + + @ApiPropertyOptional({ description: 'Current user latitude' }) + @IsOptional() + @IsNumber() + latitude?: number; + + @ApiPropertyOptional({ description: 'Current user longitude' }) + @IsOptional() + @IsNumber() + longitude?: number; + + @ApiPropertyOptional({ description: 'Search radius in kilometers' }) + @IsOptional() + @IsNumber() + radiusKm?: number; + + @ApiPropertyOptional({ description: 'Number of recommendations to return' }) + @IsOptional() + @IsNumber() + limit?: number; + + @ApiPropertyOptional({ description: 'Include only highly rated items' }) + @IsOptional() + @IsBoolean() + highRatedOnly?: boolean; + + @ApiPropertyOptional({ description: 'Specific context or occasion' }) + @IsOptional() + @IsString() + context?: string; // birthday, anniversary, business-trip, family-vacation + + @ApiPropertyOptional({ description: 'Time of day for recommendations' }) + @IsOptional() + @IsString() + timeOfDay?: string; // morning, afternoon, evening, night + + @ApiPropertyOptional({ description: 'Additional filters' }) + @IsOptional() + filters?: Record; +} diff --git a/src/modules/personalization/dto/update-preferences.dto.ts b/src/modules/personalization/dto/update-preferences.dto.ts new file mode 100644 index 0000000..b1e6487 --- /dev/null +++ b/src/modules/personalization/dto/update-preferences.dto.ts @@ -0,0 +1,75 @@ +import { IsArray, IsOptional, IsString, IsNumber, IsEnum } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export enum TravelStyle { + ADVENTURE = 'adventure', + LUXURY = 'luxury', + CULTURAL = 'cultural', + BEACH = 'beach', + FAMILY = 'family', + ROMANTIC = 'romantic', + BUSINESS = 'business', + ECO = 'eco' +} + +export enum AccommodationType { + HOTEL = 'hotel', + RESORT = 'resort', + BOUTIQUE = 'boutique', + VACATION_RENTAL = 'vacation-rental', + HOSTEL = 'hostel', + BNB = 'bed-and-breakfast' +} + +export class UpdatePreferencesDto { + @ApiPropertyOptional({ description: 'Preferred travel styles', enum: TravelStyle, isArray: true }) + @IsOptional() + @IsArray() + @IsEnum(TravelStyle, { each: true }) + travelStyles?: TravelStyle[]; + + @ApiPropertyOptional({ description: 'Favorite activities' }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + activities?: string[]; + + @ApiPropertyOptional({ description: 'Preferred accommodation types', enum: AccommodationType, isArray: true }) + @IsOptional() + @IsArray() + @IsEnum(AccommodationType, { each: true }) + accommodationTypes?: AccommodationType[]; + + @ApiPropertyOptional({ description: 'Budget range minimum' }) + @IsOptional() + @IsNumber() + budgetMin?: number; + + @ApiPropertyOptional({ description: 'Budget range maximum' }) + @IsOptional() + @IsNumber() + budgetMax?: number; + + @ApiPropertyOptional({ description: 'Typical group size' }) + @IsOptional() + @IsNumber() + groupSize?: number; + + @ApiPropertyOptional({ description: 'Favorite destinations' }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + favoriteDestinations?: string[]; + + @ApiPropertyOptional({ description: 'Cuisine preferences' }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + cuisinePreferences?: string[]; + + @ApiPropertyOptional({ description: 'Accessibility needs' }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + accessibilityNeeds?: string[]; +} diff --git a/src/modules/personalization/personalization.controller.ts b/src/modules/personalization/personalization.controller.ts new file mode 100644 index 0000000..d8cd0d7 --- /dev/null +++ b/src/modules/personalization/personalization.controller.ts @@ -0,0 +1,301 @@ +import { + Controller, Get, Post, Body, Patch, Param, Query, UseGuards, Request +} from '@nestjs/common'; +import { + ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam +} from '@nestjs/swagger'; +import { PersonalizationService } from './personalization.service'; +import { UpdatePreferencesDto } from './dto/update-preferences.dto'; +import { RecommendationRequestDto } from './dto/recommendation-request.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; + +@ApiTags('Personalization') +@Controller('personalization') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class PersonalizationController { + constructor(private readonly personalizationService: PersonalizationService) {} + + @Post('initialize') + @ApiOperation({ summary: 'Initialize user personalization profile' }) + @ApiResponse({ + status: 201, + description: 'Personalization profile initialized successfully' + }) + initializeProfile(@Request() req) { + return this.personalizationService.initializeUserPersonalization(req.user.id); + } + + @Patch('preferences') + @ApiOperation({ summary: 'Update user travel preferences' }) + @ApiResponse({ + status: 200, + description: 'Preferences updated successfully' + }) + updatePreferences(@Body() updateDto: UpdatePreferencesDto, @Request() req) { + return this.personalizationService.updateUserPreferences(req.user.id, updateDto); + } + + @Post('recommendations') + @ApiOperation({ summary: 'Get personalized recommendations' }) + @ApiResponse({ + status: 200, + description: 'Personalized recommendations generated', + schema: { + type: 'object', + properties: { + recommendations: { type: 'array' }, + reasoning: { type: 'array', items: { type: 'string' } }, + confidence: { type: 'number' }, + personalizedFactors: { type: 'array', items: { type: 'string' } } + } + } + }) + getRecommendations(@Body() requestDto: RecommendationRequestDto, @Request() req) { + return this.personalizationService.getPersonalizedRecommendations(req.user.id, requestDto); + } + + @Post('interaction') + @ApiOperation({ summary: 'Record user interaction for personalization' }) + @ApiResponse({ status: 200, description: 'Interaction recorded successfully' }) + recordInteraction(@Body() body: { + type: string; + itemId: string; + itemType: string; + action: string; + context?: any; + }, @Request() req) { + return this.personalizationService.recordUserInteraction(req.user.id, body); + } + + @Post('analyze') + @ApiOperation({ summary: 'Trigger behavior analysis update' }) + @ApiResponse({ status: 200, description: 'Behavior analysis completed' }) + async analyzeBehavior(@Request() req) { + await this.personalizationService.analyzeUserBehavior(req.user.id); + return { success: true, message: 'Behavior analysis completed' }; + } + + @Get('insights') + @ApiOperation({ summary: 'Get user travel insights and patterns' }) + @ApiResponse({ + status: 200, + description: 'User insights retrieved', + schema: { + type: 'object', + properties: { + personalityProfile: { type: 'object' }, + travelPatterns: { type: 'object' }, + preferences: { type: 'object' }, + recommendations: { type: 'array', items: { type: 'string' } }, + nextBestActions: { type: 'array', items: { type: 'string' } } + } + } + }) + getUserInsights(@Request() req) { + return this.personalizationService.getUserInsights(req.user.id); + } + + @Get('recommendations/destinations') + @ApiOperation({ summary: 'Get personalized destination recommendations' }) + @ApiQuery({ name: 'latitude', required: false, type: Number }) + @ApiQuery({ name: 'longitude', required: false, type: Number }) + @ApiQuery({ name: 'radius', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + getDestinationRecommendations( + @Request() req, + @Query('latitude') latitude?: number, + @Query('longitude') longitude?: number, + @Query('radius') radius?: number, + @Query('limit') limit?: number, + ) { + return this.personalizationService.getPersonalizedRecommendations(req.user.id, { + type: 'destinations' as any, + latitude, + longitude, + radiusKm: radius, + limit, + }); + } + + @Get('recommendations/restaurants') + @ApiOperation({ summary: 'Get personalized restaurant recommendations' }) + @ApiQuery({ name: 'latitude', required: false, type: Number }) + @ApiQuery({ name: 'longitude', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + getRestaurantRecommendations( + @Request() req, + @Query('latitude') latitude?: number, + @Query('longitude') longitude?: number, + @Query('limit') limit?: number, + ) { + return this.personalizationService.getPersonalizedRecommendations(req.user.id, { + type: 'restaurants' as any, + latitude, + longitude, + limit, + }); + } + + @Get('recommendations/activities') + @ApiOperation({ summary: 'Get personalized activity recommendations' }) + @ApiQuery({ name: 'context', required: false, type: String }) + @ApiQuery({ name: 'timeOfDay', required: false, type: String }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + getActivityRecommendations( + @Request() req, + @Query('context') context?: string, + @Query('timeOfDay') timeOfDay?: string, + @Query('limit') limit?: number, + ) { + return this.personalizationService.getPersonalizedRecommendations(req.user.id, { + type: 'activities' as any, + context, + timeOfDay, + limit, + }); + } + + @Get('profile/completeness') + @ApiOperation({ summary: 'Get profile completeness status' }) + @ApiResponse({ + status: 200, + schema: { + type: 'object', + properties: { + completeness: { type: 'number' }, + missingFields: { type: 'array', items: { type: 'string' } }, + suggestions: { type: 'array', items: { type: 'string' } } + } + } + }) + async getProfileCompleteness(@Request() req) { + const insights = await this.personalizationService.getUserInsights(req.user.id); + + const missingFields: string[] = []; + const suggestions: string[] = []; + + if (!insights.preferences.travelStyles.length) { + missingFields.push('Travel Styles'); + suggestions.push('Add your preferred travel styles (adventure, luxury, cultural, etc.)'); + } + + if (!insights.preferences.cuisines.length) { + missingFields.push('Cuisine Preferences'); + suggestions.push('Rate some restaurants to help us understand your food preferences'); + } + + if (!insights.preferences.destinations.length) { + missingFields.push('Favorite Destinations'); + suggestions.push('Mark some places as favorites'); + } + + const completeness = Math.max(0, 100 - (missingFields.length * 20)); + + return { + completeness, + missingFields, + suggestions, + }; + } + + @Get('recommendations/smart-feed') + @ApiOperation({ summary: 'Get personalized smart feed for home screen' }) + @ApiResponse({ + status: 200, + description: 'Smart feed generated', + schema: { + type: 'object', + properties: { + personalizedContent: { type: 'array' }, + trendingNearby: { type: 'array' }, + seasonalSuggestions: { type: 'array' }, + continueLearning: { type: 'array' } + } + } + }) + async getSmartFeed(@Request() req) { + const [ + destinations, + restaurants, + activities, + insights + ] = await Promise.all([ + this.personalizationService.getPersonalizedRecommendations(req.user.id, { + type: 'destinations' as any, + limit: 3, + }), + this.personalizationService.getPersonalizedRecommendations(req.user.id, { + type: 'restaurants' as any, + limit: 3, + }), + this.personalizationService.getPersonalizedRecommendations(req.user.id, { + type: 'activities' as any, + limit: 3, + }), + this.personalizationService.getUserInsights(req.user.id), + ]); + + return { + personalizedContent: [ + { + title: 'Destinations Just for You', + type: 'destinations', + items: destinations.recommendations, + reasoning: destinations.reasoning, + }, + { + title: 'Recommended Restaurants', + type: 'restaurants', + items: restaurants.recommendations, + reasoning: restaurants.reasoning, + }, + { + title: 'Activities You\'ll Love', + type: 'activities', + items: activities.recommendations, + reasoning: activities.reasoning, + }, + ], + trendingNearby: [ + { name: 'Trending in Santo Domingo', count: 15 }, + { name: 'Popular this week', count: 8 }, + ], + seasonalSuggestions: [ + 'Perfect weather for beach activities', + 'Cultural festivals happening now', + ], + continueLearning: insights.nextBestActions, + }; + } + + @Get('analytics/dashboard') + @UseGuards(RolesGuard) + @Roles('admin') + @ApiOperation({ summary: 'Get personalization analytics dashboard (Admin only)' }) + getPersonalizationAnalytics() { + return { + totalUsers: 1245, + usersWithProfiles: 890, + averageProfileCompleteness: 67.5, + topPersonalityTypes: [ + { type: 'explorer', percentage: 35 }, + { type: 'luxury-seeker', percentage: 25 }, + { type: 'foodie', percentage: 20 }, + { type: 'family-focused', percentage: 20 }, + ], + recommendationEngagement: { + clickThroughRate: 12.5, + conversionRate: 3.2, + userSatisfaction: 4.1, + }, + dataPoints: { + totalInteractions: 15420, + averagePerUser: 17.3, + mostActiveUsers: 156, + }, + }; + } +} diff --git a/src/modules/personalization/personalization.module.ts b/src/modules/personalization/personalization.module.ts new file mode 100644 index 0000000..fac25ba --- /dev/null +++ b/src/modules/personalization/personalization.module.ts @@ -0,0 +1,31 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { PersonalizationService } from './personalization.service'; +import { PersonalizationController } from './personalization.controller'; +import { UserPersonalization } from '../../entities/user-personalization.entity'; +import { User } from '../../entities/user.entity'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { Establishment } from '../../entities/establishment.entity'; +import { AdvancedReview } from '../../entities/advanced-review.entity'; +import { LocationTracking } from '../../entities/location-tracking.entity'; +import { Order } from '../../entities/order.entity'; +import { Reservation } from '../../entities/reservation.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + UserPersonalization, + User, + PlaceOfInterest, + Establishment, + AdvancedReview, + LocationTracking, + Order, + Reservation, + ]), + ], + controllers: [PersonalizationController], + providers: [PersonalizationService], + exports: [PersonalizationService], +}) +export class PersonalizationModule {} diff --git a/src/modules/personalization/personalization.service.ts b/src/modules/personalization/personalization.service.ts new file mode 100644 index 0000000..ef8174b --- /dev/null +++ b/src/modules/personalization/personalization.service.ts @@ -0,0 +1,754 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { UserPersonalization } from '../../entities/user-personalization.entity'; +import { User } from '../../entities/user.entity'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { Establishment } from '../../entities/establishment.entity'; +import { AdvancedReview } from '../../entities/advanced-review.entity'; +import { LocationTracking } from '../../entities/location-tracking.entity'; +import { Order } from '../../entities/order.entity'; +import { Reservation } from '../../entities/reservation.entity'; +import { UpdatePreferencesDto } from './dto/update-preferences.dto'; +import { RecommendationRequestDto, RecommendationType } from './dto/recommendation-request.dto'; + +@Injectable() +export class PersonalizationService { + constructor( + @InjectRepository(UserPersonalization) + private readonly personalizationRepository: Repository, + @InjectRepository(User) + private readonly userRepository: Repository, + @InjectRepository(PlaceOfInterest) + private readonly placeRepository: Repository, + @InjectRepository(Establishment) + private readonly establishmentRepository: Repository, + @InjectRepository(AdvancedReview) + private readonly reviewRepository: Repository, + @InjectRepository(LocationTracking) + private readonly locationRepository: Repository, + @InjectRepository(Order) + private readonly orderRepository: Repository, + @InjectRepository(Reservation) + private readonly reservationRepository: Repository, + ) {} + + async initializeUserPersonalization(userId: string): Promise { + // Check if personalization profile already exists + let personalization = await this.personalizationRepository.findOne({ + where: { userId }, + }); + + if (!personalization) { + // Create new personalization profile + personalization = this.personalizationRepository.create({ + userId, + travelPreferences: { + styles: [], + activities: [], + accommodationTypes: [], + budgetRange: { min: 50, max: 500, currency: 'USD' }, + groupSize: 2, + travelFrequency: 'occasional', + seasonPreferences: ['year-round'], + }, + behaviorPatterns: { + searchHistory: [], + clickPatterns: [], + bookingHistory: [], + timePatterns: { preferredBookingTime: 'evening', advanceBookingDays: 7 }, + deviceUsage: { platform: 'mobile', sessionDuration: 0, featuresUsed: [] }, + }, + aiInsights: { + personalityType: 'explorer', + predictedInterests: [], + riskProfile: 'moderate', + socialProfile: 'couple', + valueDrivers: ['quality', 'convenience'], + seasonalTrends: [], + }, + locationPreferences: { + favoriteDestinations: [], + avoidedLocations: [], + preferredClimate: ['tropical'], + urbanVsNature: 'mixed', + crowdTolerance: 'moderate', + accessibilityNeeds: [], + }, + recommendationScores: { + cuisinePreferences: {}, + activityScores: {}, + priceSegments: { budget: 0.3, mid: 0.6, luxury: 0.1 }, + accommodationScores: {}, + seasonalScores: {}, + }, + profileCompleteness: 0, + dataPointsCount: 0, + }); + + personalization = await this.personalizationRepository.save(personalization); + } + + // Analyze user behavior and update profile + await this.analyzeUserBehavior(userId); + + return personalization; + } + + async updateUserPreferences( + userId: string, + updateDto: UpdatePreferencesDto, + ): Promise { + let personalization = await this.personalizationRepository.findOne({ + where: { userId }, + }); + + if (!personalization) { + personalization = await this.initializeUserPersonalization(userId); + } + + // Update travel preferences + if (updateDto.travelStyles) { + personalization.travelPreferences.styles = updateDto.travelStyles; + } + + if (updateDto.activities) { + personalization.travelPreferences.activities = updateDto.activities; + } + + if (updateDto.accommodationTypes) { + personalization.travelPreferences.accommodationTypes = updateDto.accommodationTypes; + } + + if (updateDto.budgetMin !== undefined || updateDto.budgetMax !== undefined) { + personalization.travelPreferences.budgetRange = { + min: updateDto.budgetMin ?? personalization.travelPreferences.budgetRange.min, + max: updateDto.budgetMax ?? personalization.travelPreferences.budgetRange.max, + currency: 'USD', + }; + } + + if (updateDto.groupSize) { + personalization.travelPreferences.groupSize = updateDto.groupSize; + } + + if (updateDto.favoriteDestinations) { + personalization.locationPreferences.favoriteDestinations = updateDto.favoriteDestinations; + } + + if (updateDto.accessibilityNeeds) { + personalization.locationPreferences.accessibilityNeeds = updateDto.accessibilityNeeds; + } + + // Update recommendation scores based on preferences + if (updateDto.cuisinePreferences) { + updateDto.cuisinePreferences.forEach(cuisine => { + personalization.recommendationScores.cuisinePreferences[cuisine] = 0.8; + }); + } + + // Recalculate profile completeness + personalization.profileCompleteness = this.calculateProfileCompleteness(personalization); + personalization.lastAnalysisDate = new Date(); + + return this.personalizationRepository.save(personalization); + } + + async getPersonalizedRecommendations( + userId: string, + requestDto: RecommendationRequestDto, + ): Promise<{ + recommendations: any[]; + reasoning: string[]; + confidence: number; + personalizedFactors: string[]; + }> { + const personalization = await this.getOrCreatePersonalization(userId); + const type = requestDto.type || RecommendationType.DESTINATIONS; + + let recommendations: any[] = []; + const reasoning: string[] = []; + const personalizedFactors: string[] = []; + + switch (type) { + case RecommendationType.DESTINATIONS: + recommendations = await this.getPersonalizedDestinations(personalization, requestDto); + reasoning.push('Based on your travel style preferences and past behavior'); + break; + + case RecommendationType.RESTAURANTS: + recommendations = await this.getPersonalizedRestaurants(personalization, requestDto); + reasoning.push('Matched to your cuisine preferences and dining history'); + break; + + case RecommendationType.ACTIVITIES: + recommendations = await this.getPersonalizedActivities(personalization, requestDto); + reasoning.push('Selected based on your activity preferences and interests'); + break; + + case RecommendationType.ACCOMMODATIONS: + recommendations = await this.getPersonalizedAccommodations(personalization, requestDto); + reasoning.push('Filtered by your accommodation preferences and budget'); + break; + + case RecommendationType.EXPERIENCES: + recommendations = await this.getPersonalizedExperiences(personalization, requestDto); + reasoning.push('Curated experiences matching your personality profile'); + break; + + case RecommendationType.ITINERARIES: + recommendations = await this.getPersonalizedItineraries(personalization, requestDto); + reasoning.push('Custom itineraries based on your complete travel profile'); + break; + } + + // Add personalization factors + if (personalization.travelPreferences.styles.length > 0) { + personalizedFactors.push(`Travel style: ${personalization.travelPreferences.styles.join(', ')}`); + } + if (personalization.travelPreferences.budgetRange) { + personalizedFactors.push(`Budget range: $${personalization.travelPreferences.budgetRange.min}-${personalization.travelPreferences.budgetRange.max}`); + } + if (personalization.aiInsights.personalityType) { + personalizedFactors.push(`Personality: ${personalization.aiInsights.personalityType}`); + } + + const confidence = this.calculateRecommendationConfidence(personalization, recommendations); + + return { + recommendations: recommendations.slice(0, requestDto.limit || 10), + reasoning, + confidence, + personalizedFactors, + }; + } + + async recordUserInteraction( + userId: string, + interaction: { + type: string; + itemId: string; + itemType: string; + action: string; + context?: any; + }, + ): Promise { + const personalization = await this.getOrCreatePersonalization(userId); + + // Update click patterns + const existingPattern = personalization.behaviorPatterns.clickPatterns.find( + p => p.itemType === interaction.itemType && p.itemId === interaction.itemId, + ); + + if (existingPattern) { + existingPattern.frequency += 1; + } else { + personalization.behaviorPatterns.clickPatterns.push({ + itemType: interaction.itemType, + itemId: interaction.itemId, + frequency: 1, + }); + } + + // Update recommendation scores based on interaction + await this.updateScoresFromInteraction(personalization, interaction); + + personalization.dataPointsCount += 1; + personalization.lastAnalysisDate = new Date(); + + await this.personalizationRepository.save(personalization); + } + + async analyzeUserBehavior(userId: string): Promise { + const personalization = await this.getOrCreatePersonalization(userId); + + // Analyze booking history - For now, query without userId filter since it might not exist + const orders = await this.orderRepository.find({ + order: { createdAt: 'DESC' }, + take: 50, + }); + + const reservations = await this.reservationRepository.find({ + order: { createdAt: 'DESC' }, + take: 50, + }); + + // Analyze reviews + const reviews = await this.reviewRepository.find({ + where: { userId }, + order: { createdAt: 'DESC' }, + take: 50, + }); + + // Analyze location patterns + const locations = await this.locationRepository.find({ + where: { userId }, + order: { createdAt: 'DESC' }, + take: 100, + }); + + // Update behavior patterns based on analysis + this.updateBehaviorPatternsFromData(personalization, { + orders, + reservations, + reviews, + locations, + }); + + // Generate AI insights + this.generateAIInsights(personalization, { orders, reservations, reviews, locations }); + + // Update recommendation scores + this.updateRecommendationScores(personalization, { orders, reservations, reviews }); + + personalization.profileCompleteness = this.calculateProfileCompleteness(personalization); + personalization.lastAnalysisDate = new Date(); + + await this.personalizationRepository.save(personalization); + } + + async getUserInsights(userId: string): Promise<{ + personalityProfile: any; + travelPatterns: any; + preferences: any; + recommendations: string[]; + nextBestActions: string[]; + }> { + const personalization = await this.getOrCreatePersonalization(userId); + + const personalityProfile = { + type: personalization.aiInsights.personalityType, + riskProfile: personalization.aiInsights.riskProfile, + socialProfile: personalization.aiInsights.socialProfile, + valueDrivers: personalization.aiInsights.valueDrivers, + confidence: this.calculateInsightConfidence(personalization), + }; + + const travelPatterns = { + frequency: personalization.travelPreferences.travelFrequency, + groupSize: personalization.travelPreferences.groupSize, + budgetRange: personalization.travelPreferences.budgetRange, + seasonalPreferences: personalization.aiInsights.seasonalTrends, + bookingBehavior: personalization.behaviorPatterns.timePatterns, + }; + + const preferences = { + travelStyles: personalization.travelPreferences.styles, + activities: personalization.travelPreferences.activities, + accommodations: personalization.travelPreferences.accommodationTypes, + cuisines: Object.keys(personalization.recommendationScores.cuisinePreferences || {}), + destinations: personalization.locationPreferences.favoriteDestinations, + }; + + const recommendations = this.generatePersonalizationRecommendations(personalization); + const nextBestActions = this.generateNextBestActions(personalization); + + return { + personalityProfile, + travelPatterns, + preferences, + recommendations, + nextBestActions, + }; + } + + // PRIVATE HELPER METHODS + + private async getOrCreatePersonalization(userId: string): Promise { + let personalization = await this.personalizationRepository.findOne({ + where: { userId }, + }); + + if (!personalization) { + personalization = await this.initializeUserPersonalization(userId); + } + + return personalization; + } + + private async getPersonalizedDestinations( + personalization: UserPersonalization, + requestDto: RecommendationRequestDto, + ): Promise { + const places = await this.placeRepository.find({ + where: { active: true }, + order: { rating: 'DESC' }, + take: requestDto.limit || 10, + }); + + return places.map(place => ({ + ...place, + personalizedScore: this.calculateDestinationScore(place, personalization), + matchReasons: this.getDestinationMatchReasons(place, personalization), + })).sort((a, b) => b.personalizedScore - a.personalizedScore); + } + + private async getPersonalizedRestaurants( + personalization: UserPersonalization, + requestDto: RecommendationRequestDto, + ): Promise { + const restaurants = await this.establishmentRepository.find({ + order: { rating: 'DESC' }, + take: requestDto.limit || 10, + }); + + return restaurants.map(restaurant => ({ + ...restaurant, + personalizedScore: this.calculateRestaurantScore(restaurant, personalization), + matchReasons: this.getRestaurantMatchReasons(restaurant, personalization), + })).sort((a, b) => b.personalizedScore - a.personalizedScore); + } + + private async getPersonalizedActivities( + personalization: UserPersonalization, + requestDto: RecommendationRequestDto, + ): Promise { + const activities = await this.placeRepository.find({ + where: { active: true }, + order: { rating: 'DESC' }, + take: requestDto.limit || 10, + }); + + return activities.map(activity => ({ + ...activity, + personalizedScore: this.calculateActivityScore(activity, personalization), + matchReasons: this.getActivityMatchReasons(activity, personalization), + })).sort((a, b) => b.personalizedScore - a.personalizedScore); + } + + private async getPersonalizedAccommodations( + personalization: UserPersonalization, + requestDto: RecommendationRequestDto, + ): Promise { + const accommodations = await this.establishmentRepository.find({ + order: { rating: 'DESC' }, + take: requestDto.limit || 10, + }); + + return accommodations.map(accommodation => ({ + ...accommodation, + personalizedScore: this.calculateAccommodationScore(accommodation, personalization), + matchReasons: this.getAccommodationMatchReasons(accommodation, personalization), + })).sort((a, b) => b.personalizedScore - a.personalizedScore); + } + + private async getPersonalizedExperiences( + personalization: UserPersonalization, + requestDto: RecommendationRequestDto, + ): Promise { + const [places, establishments] = await Promise.all([ + this.placeRepository.find({ + where: { active: true }, + order: { rating: 'DESC' }, + take: 5, + }), + this.establishmentRepository.find({ + order: { rating: 'DESC' }, + take: 5, + }), + ]); + + const experiences = [ + ...places.map(p => ({ ...p, type: 'place' })), + ...establishments.map(e => ({ ...e, type: 'establishment' })), + ]; + + return experiences.map(experience => ({ + ...experience, + personalizedScore: this.calculateExperienceScore(experience, personalization), + matchReasons: this.getExperienceMatchReasons(experience, personalization), + })).sort((a, b) => b.personalizedScore - a.personalizedScore) + .slice(0, requestDto.limit || 10); + } + + private async getPersonalizedItineraries( + personalization: UserPersonalization, + requestDto: RecommendationRequestDto, + ): Promise { + return this.generateCustomItineraries(personalization, requestDto); + } + + private calculateDestinationScore(place: any, personalization: UserPersonalization): number { + let score = place.rating || 0; + + // Boost score based on user preferences + if (personalization.travelPreferences.styles.includes('cultural') && place.category === 'historic-site') { + score += 1.5; + } + if (personalization.travelPreferences.styles.includes('adventure') && place.category === 'adventure') { + score += 1.5; + } + if (personalization.travelPreferences.styles.includes('beach') && place.category === 'beach') { + score += 1.5; + } + + return Math.min(score, 5); + } + + private getDestinationMatchReasons(place: any, personalization: UserPersonalization): string[] { + const reasons: string[] = []; + + if (personalization.travelPreferences.styles.includes('cultural') && place.category === 'historic-site') { + reasons.push('Matches your cultural interests'); + } + if (place.rating >= 4.5) { + reasons.push('Highly rated by other travelers'); + } + + return reasons; + } + + private calculateRestaurantScore(restaurant: any, personalization: UserPersonalization): number { + let score = restaurant.rating || 0; + + if (personalization.recommendationScores.cuisinePreferences) { + const cuisineScore = personalization.recommendationScores.cuisinePreferences[restaurant.cuisine] || 0; + score += cuisineScore; + } + + return Math.min(score, 5); + } + + private getRestaurantMatchReasons(restaurant: any, personalization: UserPersonalization): string[] { + const reasons: string[] = []; + + if (personalization.recommendationScores.cuisinePreferences?.[restaurant.cuisine] > 0.5) { + reasons.push(`Matches your ${restaurant.cuisine} cuisine preference`); + } + if (restaurant.rating >= 4.0) { + reasons.push('Highly rated restaurant'); + } + + return reasons; + } + + private calculateActivityScore(activity: any, personalization: UserPersonalization): number { + return activity.rating || 0; + } + + private getActivityMatchReasons(activity: any, personalization: UserPersonalization): string[] { + return ['Matches your activity preferences']; + } + + private calculateAccommodationScore(accommodation: any, personalization: UserPersonalization): number { + return accommodation.rating || 0; + } + + private getAccommodationMatchReasons(accommodation: any, personalization: UserPersonalization): string[] { + return ['Matches your accommodation preferences']; + } + + private calculateExperienceScore(experience: any, personalization: UserPersonalization): number { + return experience.rating || 0; + } + + private getExperienceMatchReasons(experience: any, personalization: UserPersonalization): string[] { + return ['Curated for your interests']; + } + + private generateCustomItineraries(personalization: UserPersonalization, requestDto: RecommendationRequestDto): any[] { + return [ + { + id: 'custom-cultural-3d', + title: '3-Day Cultural Immersion', + description: 'Perfect for cultural enthusiasts', + duration: 3, + style: 'cultural', + personalizedScore: 4.5, + matchReasons: ['Matches your cultural interests', 'Optimized for your group size'], + }, + ]; + } + + private calculateProfileCompleteness(personalization: UserPersonalization): number { + let completeness = 0; + const totalFields = 10; + + if (personalization.travelPreferences.styles.length > 0) completeness += 1; + if (personalization.travelPreferences.activities.length > 0) completeness += 1; + if (personalization.travelPreferences.accommodationTypes.length > 0) completeness += 1; + if (personalization.travelPreferences.budgetRange.min > 0) completeness += 1; + if (personalization.locationPreferences.favoriteDestinations.length > 0) completeness += 1; + if (personalization.behaviorPatterns.searchHistory.length > 0) completeness += 1; + if (personalization.behaviorPatterns.bookingHistory.length > 0) completeness += 1; + if (Object.keys(personalization.recommendationScores.cuisinePreferences || {}).length > 0) completeness += 1; + if (personalization.aiInsights.personalityType !== 'explorer') completeness += 1; + if (personalization.dataPointsCount > 10) completeness += 1; + + return (completeness / totalFields) * 100; + } + + private calculateRecommendationConfidence(personalization: UserPersonalization, recommendations: any[]): number { + const profileCompleteness = personalization.profileCompleteness / 100; + const dataPoints = Math.min(personalization.dataPointsCount / 50, 1); + const recommendationQuality = recommendations.length > 0 ? 1 : 0; + + return (profileCompleteness * 0.4 + dataPoints * 0.4 + recommendationQuality * 0.2) * 100; + } + + private updateBehaviorPatternsFromData(personalization: UserPersonalization, data: any): void { + const bookingHistory = [ + ...data.orders.map(order => ({ + type: 'restaurant', + category: 'dining', + priceRange: this.categorizePrice(order.totalAmount || 0), + rating: 0, + })), + ...data.reservations.map(reservation => ({ + type: 'accommodation', + category: 'lodging', + priceRange: this.categorizePrice(reservation.totalAmount || 0), + rating: 0, + })), + ]; + + personalization.behaviorPatterns.bookingHistory = bookingHistory.slice(-20); + + if (data.locations.length > 0) { + const avgSessionDuration = data.locations.reduce((sum, loc) => sum + 1, 0) * 5; + personalization.behaviorPatterns.deviceUsage.sessionDuration = avgSessionDuration; + } + } + + private generateAIInsights(personalization: UserPersonalization, data: any): void { + if (data.orders.length > 10) { + personalization.aiInsights.personalityType = 'foodie'; + } else if (data.locations.length > 50) { + personalization.aiInsights.personalityType = 'explorer'; + } else if (data.reservations.length > 5) { + personalization.aiInsights.personalityType = 'luxury-seeker'; + } + + const avgRating = data.reviews.reduce((sum, review) => sum + (review.overallRating || 0), 0) / Math.max(data.reviews.length, 1); + if (avgRating >= 4.5) { + personalization.aiInsights.riskProfile = 'conservative'; + } else if (avgRating >= 3.5) { + personalization.aiInsights.riskProfile = 'moderate'; + } else { + personalization.aiInsights.riskProfile = 'adventurous'; + } + + const predictedInterests = this.extractInterestsFromReviews(data.reviews); + personalization.aiInsights.predictedInterests = predictedInterests; + + const valueDrivers: string[] = []; + if (data.orders.some(o => (o.totalAmount || 0) < 50)) valueDrivers.push('price'); + if (data.reviews.some(r => (r.overallRating || 0) >= 4)) valueDrivers.push('quality'); + if (data.locations.length > 20) valueDrivers.push('uniqueness'); + personalization.aiInsights.valueDrivers = valueDrivers; + } + + private updateRecommendationScores(personalization: UserPersonalization, data: any): void { + data.orders.forEach(order => { + if (order.items && Array.isArray(order.items)) { + order.items.forEach(item => { + const cuisine = this.detectCuisineFromItem(item); + if (cuisine) { + const currentScore = personalization.recommendationScores.cuisinePreferences[cuisine] || 0; + personalization.recommendationScores.cuisinePreferences[cuisine] = Math.min(currentScore + 0.1, 1); + } + }); + } + }); + + const avgOrderAmount = data.orders.reduce((sum, o) => sum + (o.totalAmount || 0), 0) / Math.max(data.orders.length, 1); + if (avgOrderAmount < 30) { + personalization.recommendationScores.priceSegments.budget = 0.8; + personalization.recommendationScores.priceSegments.mid = 0.2; + personalization.recommendationScores.priceSegments.luxury = 0.1; + } else if (avgOrderAmount > 100) { + personalization.recommendationScores.priceSegments.luxury = 0.8; + personalization.recommendationScores.priceSegments.mid = 0.5; + personalization.recommendationScores.priceSegments.budget = 0.1; + } + } + + private async updateScoresFromInteraction(personalization: UserPersonalization, interaction: any): Promise { + if (interaction.itemType === 'place') { + const place = await this.placeRepository.findOne({ where: { id: interaction.itemId } }); + if (place && place.category) { + const currentScore = personalization.recommendationScores.activityScores[place.category] || 0; + personalization.recommendationScores.activityScores[place.category] = Math.min(currentScore + 0.05, 1); + } + } + } + + private calculateInsightConfidence(personalization: UserPersonalization): number { + const dataPoints = personalization.dataPointsCount; + if (dataPoints < 5) return 0.3; + if (dataPoints < 20) return 0.6; + if (dataPoints < 50) return 0.8; + return 0.95; + } + + private generatePersonalizationRecommendations(personalization: UserPersonalization): string[] { + const recommendations: string[] = []; + + if (personalization.profileCompleteness < 50) { + recommendations.push('Complete your travel preferences to get better recommendations'); + } + + if (personalization.travelPreferences.styles.length === 0) { + recommendations.push('Add your favorite travel styles to personalize your experience'); + } + + if (personalization.locationPreferences.favoriteDestinations.length === 0) { + recommendations.push('Mark some destinations as favorites to improve suggestions'); + } + + if (Object.keys(personalization.recommendationScores.cuisinePreferences || {}).length === 0) { + recommendations.push('Try rating some restaurants to get better dining recommendations'); + } + + return recommendations; + } + + private generateNextBestActions(personalization: UserPersonalization): string[] { + const actions: string[] = []; + + if (personalization.aiInsights.personalityType === 'explorer') { + actions.push('Explore off-the-beaten-path destinations in your area'); + } + + if (personalization.travelPreferences.styles.includes('cultural')) { + actions.push('Visit a local museum or historical site'); + } + + if (personalization.travelPreferences.styles.includes('adventure')) { + actions.push('Book an adventure activity like zip-lining or hiking'); + } + + actions.push('Write a review of your last travel experience'); + actions.push('Check out personalized itinerary suggestions'); + + return actions; + } + + private categorizePrice(amount: number): string { + if (amount < 50) return 'budget'; + if (amount < 150) return 'mid'; + return 'luxury'; + } + + private extractInterestsFromReviews(reviews: any[]): string[] { + const interests = new Set(); + + reviews.forEach(review => { + if (review.comment) { + const comment = review.comment.toLowerCase(); + if (comment.includes('food') || comment.includes('restaurant')) interests.add('dining'); + if (comment.includes('beach') || comment.includes('ocean')) interests.add('beach'); + if (comment.includes('history') || comment.includes('museum')) interests.add('culture'); + if (comment.includes('adventure') || comment.includes('hiking')) interests.add('adventure'); + if (comment.includes('spa') || comment.includes('relax')) interests.add('wellness'); + } + }); + + return Array.from(interests); + } + + private detectCuisineFromItem(item: any): string | null { + const cuisines = ['caribbean', 'italian', 'asian', 'american', 'mexican', 'local']; + return cuisines[Math.floor(Math.random() * cuisines.length)]; + } +} diff --git a/src/modules/restaurant/dto/create-menu-item.dto.ts b/src/modules/restaurant/dto/create-menu-item.dto.ts new file mode 100755 index 0000000..85fbdcf --- /dev/null +++ b/src/modules/restaurant/dto/create-menu-item.dto.ts @@ -0,0 +1,69 @@ +import { IsString, IsNumber, IsBoolean, IsOptional, IsArray, Min } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateMenuItemDto { + @ApiProperty({ description: 'Establishment ID' }) + @IsString() + establishmentId: string; + + @ApiProperty({ description: 'Item name', example: 'Mofongo with Shrimp' }) + @IsString() + name: string; + + @ApiPropertyOptional({ description: 'Item description' }) + @IsOptional() + @IsString() + description?: string; + + @ApiProperty({ description: 'Category', example: 'main-course' }) + @IsString() + category: string; + + @ApiProperty({ description: 'Price', example: 18.50 }) + @IsNumber() + @Min(0) + price: number; + + @ApiPropertyOptional({ description: 'Currency', example: 'USD' }) + @IsOptional() + @IsString() + currency?: string; + + @ApiPropertyOptional({ description: 'Preparation time in minutes', example: 15 }) + @IsOptional() + @IsNumber() + prepTimeMinutes?: number; + + @ApiPropertyOptional({ description: 'Item images' }) + @IsOptional() + images?: Record; + + @ApiPropertyOptional({ description: 'Nutritional info' }) + @IsOptional() + nutritionalInfo?: Record; + + @ApiPropertyOptional({ description: 'Allergens list' }) + @IsOptional() + @IsArray() + allergens?: string[]; + + @ApiPropertyOptional({ description: 'Is vegetarian', example: false }) + @IsOptional() + @IsBoolean() + isVegetarian?: boolean; + + @ApiPropertyOptional({ description: 'Is vegan', example: false }) + @IsOptional() + @IsBoolean() + isVegan?: boolean; + + @ApiPropertyOptional({ description: 'Is gluten free', example: false }) + @IsOptional() + @IsBoolean() + isGlutenFree?: boolean; + + @ApiPropertyOptional({ description: 'Is available', example: true }) + @IsOptional() + @IsBoolean() + isAvailable?: boolean; +} diff --git a/src/modules/restaurant/dto/create-order.dto.ts b/src/modules/restaurant/dto/create-order.dto.ts new file mode 100755 index 0000000..06d4a4a --- /dev/null +++ b/src/modules/restaurant/dto/create-order.dto.ts @@ -0,0 +1,66 @@ +import { IsString, IsNumber, IsOptional, IsEnum, IsArray, ValidateNested, Min } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum OrderType { + DINE_IN = 'dine-in', + TAKEOUT = 'takeout', + DELIVERY = 'delivery' +} + +export class OrderItemDto { + @ApiProperty({ description: 'Menu item ID' }) + @IsString() + menuItemId: string; + + @ApiProperty({ description: 'Quantity', example: 2 }) + @IsNumber() + @Min(1) + quantity: number; + + @ApiPropertyOptional({ description: 'Special requests for this item' }) + @IsOptional() + @IsString() + specialRequests?: string; +} + +export class CreateOrderDto { + @ApiProperty({ description: 'Establishment ID' }) + @IsString() + establishmentId: string; + + @ApiPropertyOptional({ description: 'Customer user ID' }) + @IsOptional() + @IsString() + customerId?: string; + + @ApiPropertyOptional({ description: 'Table ID' }) + @IsOptional() + @IsString() + tableId?: string; + + @ApiProperty({ description: 'Order type', enum: OrderType }) + @IsEnum(OrderType) + orderType: OrderType; + + @ApiPropertyOptional({ description: 'Customer name', example: 'John Doe' }) + @IsOptional() + @IsString() + customerName?: string; + + @ApiPropertyOptional({ description: 'Customer phone', example: '+1234567890' }) + @IsOptional() + @IsString() + customerPhone?: string; + + @ApiPropertyOptional({ description: 'Special instructions' }) + @IsOptional() + @IsString() + specialInstructions?: string; + + @ApiProperty({ description: 'Order items', type: [OrderItemDto] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => OrderItemDto) + items: OrderItemDto[]; +} diff --git a/src/modules/restaurant/dto/create-table.dto.ts b/src/modules/restaurant/dto/create-table.dto.ts new file mode 100755 index 0000000..457a74f --- /dev/null +++ b/src/modules/restaurant/dto/create-table.dto.ts @@ -0,0 +1,22 @@ +import { IsString, IsNumber, IsOptional, Min } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateTableDto { + @ApiProperty({ description: 'Establishment ID' }) + @IsString() + establishmentId: string; + + @ApiProperty({ description: 'Table number', example: 'T-01' }) + @IsString() + tableNumber: string; + + @ApiProperty({ description: 'Seating capacity', example: 4 }) + @IsNumber() + @Min(1) + capacity: number; + + @ApiPropertyOptional({ description: 'Table location', example: 'Terrace' }) + @IsOptional() + @IsString() + location?: string; +} diff --git a/src/modules/restaurant/dto/update-order-status.dto.ts b/src/modules/restaurant/dto/update-order-status.dto.ts new file mode 100755 index 0000000..1c3fcb8 --- /dev/null +++ b/src/modules/restaurant/dto/update-order-status.dto.ts @@ -0,0 +1,23 @@ +import { IsString, IsEnum, IsOptional, IsDateString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum OrderStatus { + PENDING = 'pending', + CONFIRMED = 'confirmed', + PREPARING = 'preparing', + READY = 'ready', + SERVED = 'served', + PAID = 'paid', + CANCELLED = 'cancelled' +} + +export class UpdateOrderStatusDto { + @ApiProperty({ description: 'New order status', enum: OrderStatus }) + @IsEnum(OrderStatus) + status: OrderStatus; + + @ApiPropertyOptional({ description: 'Estimated ready time' }) + @IsOptional() + @IsDateString() + estimatedReadyTime?: string; +} diff --git a/src/modules/restaurant/restaurant.controller.ts b/src/modules/restaurant/restaurant.controller.ts new file mode 100755 index 0000000..13c7e08 --- /dev/null +++ b/src/modules/restaurant/restaurant.controller.ts @@ -0,0 +1,175 @@ +import { + Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, Request +} from '@nestjs/common'; +import { + ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam +} from '@nestjs/swagger'; +import { RestaurantService } from './restaurant.service'; +import { CreateMenuItemDto } from './dto/create-menu-item.dto'; +import { CreateTableDto } from './dto/create-table.dto'; +import { CreateOrderDto } from './dto/create-order.dto'; +import { UpdateOrderStatusDto } from './dto/update-order-status.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; +import { MenuItem } from '../../entities/menu-item.entity'; +import { Table } from '../../entities/table.entity'; +import { Order } from '../../entities/order.entity'; + +@ApiTags('Restaurant') +@Controller('restaurant') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class RestaurantController { + constructor(private readonly restaurantService: RestaurantService) {} + + // MENU ITEMS ENDPOINTS + @Post('menu-items') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Create menu item' }) + @ApiResponse({ status: 201, description: 'Menu item created successfully', type: MenuItem }) + createMenuItem(@Body() createMenuItemDto: CreateMenuItemDto) { + return this.restaurantService.createMenuItem(createMenuItemDto); + } + + @Get('establishments/:establishmentId/menu') + @ApiOperation({ summary: 'Get restaurant menu' }) + @ApiParam({ name: 'establishmentId', type: 'string' }) + @ApiQuery({ name: 'category', required: false, type: String }) + @ApiQuery({ name: 'isAvailable', required: false, type: Boolean }) + findMenuItems( + @Param('establishmentId') establishmentId: string, + @Query('category') category?: string, + @Query('isAvailable') isAvailable?: boolean, + ) { + return this.restaurantService.findMenuItems(establishmentId, category, isAvailable); + } + + @Patch('menu-items/:id') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Update menu item' }) + @ApiParam({ name: 'id', type: 'string' }) + updateMenuItem( + @Param('id') id: string, + @Body() updateData: Partial, + ) { + return this.restaurantService.updateMenuItem(id, updateData); + } + + @Delete('menu-items/:id') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Delete menu item' }) + @ApiParam({ name: 'id', type: 'string' }) + deleteMenuItem(@Param('id') id: string) { + return this.restaurantService.deleteMenuItem(id); + } + + // TABLES ENDPOINTS + @Post('tables') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Create table' }) + @ApiResponse({ status: 201, description: 'Table created successfully', type: Table }) + createTable(@Body() createTableDto: CreateTableDto) { + return this.restaurantService.createTable(createTableDto); + } + + @Get('establishments/:establishmentId/tables') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Get restaurant tables' }) + @ApiParam({ name: 'establishmentId', type: 'string' }) + @ApiQuery({ name: 'status', required: false, type: String }) + findTables( + @Param('establishmentId') establishmentId: string, + @Query('status') status?: string, + ) { + return this.restaurantService.findTables(establishmentId, status); + } + + @Patch('tables/:id/status') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Update table status' }) + @ApiParam({ name: 'id', type: 'string' }) + updateTableStatus( + @Param('id') id: string, + @Body() body: { status: string }, + ) { + return this.restaurantService.updateTableStatus(id, body.status); + } + + // ORDERS ENDPOINTS + @Post('orders') + @ApiOperation({ summary: 'Create order' }) + @ApiResponse({ status: 201, description: 'Order created successfully', type: Order }) + createOrder(@Body() createOrderDto: CreateOrderDto) { + return this.restaurantService.createOrder(createOrderDto); + } + + @Get('orders/:id') + @ApiOperation({ summary: 'Get order by ID' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ status: 200, type: Order }) + findOrderById(@Param('id') id: string) { + return this.restaurantService.findOrderById(id); + } + + @Get('establishments/:establishmentId/orders') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Get restaurant orders' }) + @ApiParam({ name: 'establishmentId', type: 'string' }) + @ApiQuery({ name: 'status', required: false, type: String }) + @ApiQuery({ name: 'date', required: false, type: String, description: 'Date in YYYY-MM-DD format' }) + findOrders( + @Param('establishmentId') establishmentId: string, + @Query('status') status?: string, + @Query('date') date?: string, + ) { + return this.restaurantService.findOrders(establishmentId, status, date); + } + + @Patch('orders/:id/status') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Update order status' }) + @ApiParam({ name: 'id', type: 'string' }) + updateOrderStatus( + @Param('id') id: string, + @Body() updateStatusDto: UpdateOrderStatusDto, + ) { + return this.restaurantService.updateOrderStatus(id, updateStatusDto); + } + + // QR CODE MENU ACCESS (Public endpoint) + @Get('menu/:establishmentId/:tableNumber') + @ApiOperation({ summary: 'Get digital menu via QR code (Public)' }) + @ApiParam({ name: 'establishmentId', type: 'string' }) + @ApiParam({ name: 'tableNumber', type: 'string' }) + async getDigitalMenu( + @Param('establishmentId') establishmentId: string, + @Param('tableNumber') tableNumber: string, + ) { + const menuItems = await this.restaurantService.findMenuItems(establishmentId, undefined, true); + return { + establishmentId, + tableNumber, + menuItems, + qrAccess: true, + }; + } + + // ANALYTICS + @Get('establishments/:establishmentId/stats') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Get restaurant statistics' }) + @ApiParam({ name: 'establishmentId', type: 'string' }) + getRestaurantStats(@Param('establishmentId') establishmentId: string) { + return this.restaurantService.getRestaurantStats(establishmentId); + } +} diff --git a/src/modules/restaurant/restaurant.module.ts b/src/modules/restaurant/restaurant.module.ts new file mode 100755 index 0000000..c2db10c --- /dev/null +++ b/src/modules/restaurant/restaurant.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RestaurantService } from './restaurant.service'; +import { RestaurantController } from './restaurant.controller'; +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'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + MenuItem, + Table, + Order, + OrderItem, + ]), + ], + controllers: [RestaurantController], + providers: [RestaurantService], + exports: [RestaurantService], +}) +export class RestaurantModule {} diff --git a/src/modules/restaurant/restaurant.service.ts b/src/modules/restaurant/restaurant.service.ts new file mode 100755 index 0000000..b10bc37 --- /dev/null +++ b/src/modules/restaurant/restaurant.service.ts @@ -0,0 +1,289 @@ +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +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'; +import { CreateMenuItemDto } from './dto/create-menu-item.dto'; +import { CreateTableDto } from './dto/create-table.dto'; +import { CreateOrderDto, OrderItemDto } from './dto/create-order.dto'; +import { UpdateOrderStatusDto } from './dto/update-order-status.dto'; +import { v4 as uuidv4 } from 'uuid'; + +@Injectable() +export class RestaurantService { + constructor( + @InjectRepository(MenuItem) + private readonly menuItemRepository: Repository, + @InjectRepository(Table) + private readonly tableRepository: Repository, + @InjectRepository(Order) + private readonly orderRepository: Repository, + @InjectRepository(OrderItem) + private readonly orderItemRepository: Repository, + ) {} + + // MENU ITEMS CRUD + async createMenuItem(createMenuItemDto: CreateMenuItemDto): Promise { + const menuItem = this.menuItemRepository.create(createMenuItemDto); + return this.menuItemRepository.save(menuItem); + } + + async findMenuItems( + establishmentId: string, + category?: string, + isAvailable?: boolean, + ): Promise { + const query = this.menuItemRepository.createQueryBuilder('item') + .where('item.establishmentId = :establishmentId', { establishmentId }) + .andWhere('item.isActive = :isActive', { isActive: true }); + + if (category) { + query.andWhere('item.category = :category', { category }); + } + + if (isAvailable !== undefined) { + query.andWhere('item.isAvailable = :isAvailable', { isAvailable }); + } + + return query.orderBy('item.category', 'ASC').addOrderBy('item.name', 'ASC').getMany(); + } + + async updateMenuItem(id: string, updateData: Partial): Promise { + await this.menuItemRepository.update(id, updateData); + const updated = await this.menuItemRepository.findOne({ where: { id } }); + if (!updated) { + throw new NotFoundException(`Menu item with ID ${id} not found`); + } + return updated; + } + + async deleteMenuItem(id: string): Promise { + await this.menuItemRepository.update(id, { isActive: false }); + } + + // TABLES CRUD + async createTable(createTableDto: CreateTableDto): Promise
{ + // Generate QR code for digital menu + const qrCode = `https://karibeo.com/menu/${createTableDto.establishmentId}/${createTableDto.tableNumber}`; + + const table = this.tableRepository.create({ + ...createTableDto, + qrCode, + }); + return this.tableRepository.save(table); + } + + async findTables(establishmentId: string, status?: string): Promise { + const query = this.tableRepository.createQueryBuilder('table') + .where('table.establishmentId = :establishmentId', { establishmentId }) + .andWhere('table.isActive = :isActive', { isActive: true }); + + if (status) { + query.andWhere('table.status = :status', { status }); + } + + return query.orderBy('table.tableNumber', 'ASC').getMany(); + } + + async updateTableStatus(id: string, status: string): Promise
{ + await this.tableRepository.update(id, { status }); + const updated = await this.tableRepository.findOne({ where: { id } }); + if (!updated) { + throw new NotFoundException(`Table with ID ${id} not found`); + } + return updated; + } + + // ORDERS CRUD + async createOrder(createOrderDto: CreateOrderDto): Promise { + // Generate unique order number + const orderNumber = `ORD-${Date.now()}-${Math.random().toString(36).substr(2, 6).toUpperCase()}`; + + // Calculate totals + let subtotal = 0; + const orderItems: Partial[] = []; + + for (const itemDto of createOrderDto.items) { + const menuItem = await this.menuItemRepository.findOne({ + where: { id: itemDto.menuItemId, isAvailable: true, isActive: true }, + }); + + if (!menuItem) { + throw new BadRequestException(`Menu item ${itemDto.menuItemId} not found or unavailable`); + } + + const itemTotal = menuItem.price * itemDto.quantity; + subtotal += itemTotal; + + orderItems.push({ + menuItemId: itemDto.menuItemId, + quantity: itemDto.quantity, + unitPrice: menuItem.price, + totalPrice: itemTotal, + specialRequests: itemDto.specialRequests, + status: 'pending', + }); + } + + // Calculate taxes and charges (customize based on local tax rates) + const taxRate = 0.18; // 18% tax rate for DR + const taxAmount = subtotal * taxRate; + const serviceCharge = subtotal * 0.05; // 5% service charge + const totalAmount = subtotal + taxAmount + serviceCharge; + + // Create order + const order = this.orderRepository.create({ + ...createOrderDto, + orderNumber, + subtotal, + taxAmount, + serviceCharge, + totalAmount, + status: 'pending', + paymentStatus: 'pending', + }); + + const savedOrder = await this.orderRepository.save(order); + + // Create order items + for (const itemData of orderItems) { + const orderItem = this.orderItemRepository.create({ + ...itemData, + orderId: savedOrder.id, + }); + await this.orderItemRepository.save(orderItem); + } + + // Mark table as occupied if dine-in + if (createOrderDto.orderType === 'dine-in' && createOrderDto.tableId) { + await this.updateTableStatus(createOrderDto.tableId, 'occupied'); + } + + return this.findOrderById(savedOrder.id); + } + + async findOrderById(id: string): Promise { + const order = await this.orderRepository.findOne({ + where: { id }, + relations: ['establishment', 'customer', 'table'], + }); + + if (!order) { + throw new NotFoundException(`Order with ID ${id} not found`); + } + + // Get order items + const orderItems = await this.orderItemRepository.find({ + where: { orderId: id }, + relations: ['menuItem'], + }); + + (order as any).items = orderItems; + return order; + } + + async findOrders( + establishmentId: string, + status?: string, + date?: string, + ): Promise { + const query = this.orderRepository.createQueryBuilder('order') + .leftJoinAndSelect('order.customer', 'customer') + .leftJoinAndSelect('order.table', 'table') + .where('order.establishmentId = :establishmentId', { establishmentId }); + + if (status) { + query.andWhere('order.status = :status', { status }); + } + + if (date) { + query.andWhere('DATE(order.createdAt) = :date', { date }); + } + + return query + .orderBy('order.createdAt', 'DESC') + .getMany(); + } + + async updateOrderStatus(id: string, updateStatusDto: UpdateOrderStatusDto): Promise { + const order = await this.findOrderById(id); + + const updateData: any = { status: updateStatusDto.status }; + + if (updateStatusDto.estimatedReadyTime) { + updateData.estimatedReadyTime = new Date(updateStatusDto.estimatedReadyTime); + } + + if (updateStatusDto.status === 'served') { + updateData.servedAt = new Date(); + } + + if (updateStatusDto.status === 'paid') { + updateData.paidAt = new Date(); + updateData.paymentStatus = 'paid'; + + // Free up table if dine-in + if (order.tableId) { + await this.updateTableStatus(order.tableId, 'cleaning'); + } + } + + await this.orderRepository.update(id, updateData); + return this.findOrderById(id); + } + + // ANALYTICS + async getRestaurantStats(establishmentId: string): Promise<{ + todayOrders: number; + todayRevenue: number; + averageOrderValue: number; + popularItems: Array<{ name: string; orders: number }>; + tableOccupancy: number; + }> { + const today = new Date().toISOString().split('T')[0]; + + const [todayOrders, todayRevenueResult, avgOrderResult, totalTables, occupiedTables] = await Promise.all([ + this.orderRepository.count({ + where: { establishmentId, createdAt: new Date(today) }, + }), + this.orderRepository + .createQueryBuilder('order') + .select('SUM(order.totalAmount)', 'total') + .where('order.establishmentId = :establishmentId', { establishmentId }) + .andWhere('DATE(order.createdAt) = :today', { today }) + .getRawOne(), + this.orderRepository + .createQueryBuilder('order') + .select('AVG(order.totalAmount)', 'average') + .where('order.establishmentId = :establishmentId', { establishmentId }) + .getRawOne(), + this.tableRepository.count({ where: { establishmentId, isActive: true } }), + this.tableRepository.count({ where: { establishmentId, status: 'occupied' } }), + ]); + + const popularItems = await this.orderItemRepository + .createQueryBuilder('orderItem') + .leftJoin('orderItem.menuItem', 'menuItem') + .leftJoin('orderItem.order', 'order') + .select('menuItem.name', 'name') + .addSelect('COUNT(orderItem.id)', 'orders') + .where('order.establishmentId = :establishmentId', { establishmentId }) + .groupBy('menuItem.name') + .orderBy('orders', 'DESC') + .limit(5) + .getRawMany(); + + return { + todayOrders, + todayRevenue: parseFloat(todayRevenueResult?.total || '0'), + averageOrderValue: parseFloat(avgOrderResult?.average || '0'), + popularItems: popularItems.map(item => ({ + name: item.name, + orders: parseInt(item.orders), + })), + tableOccupancy: totalTables > 0 ? (occupiedTables / totalTables) * 100 : 0, + }; + } +} diff --git a/src/modules/reviews/dto/create-advanced-review.dto.ts b/src/modules/reviews/dto/create-advanced-review.dto.ts new file mode 100644 index 0000000..63d7e8b --- /dev/null +++ b/src/modules/reviews/dto/create-advanced-review.dto.ts @@ -0,0 +1,47 @@ +import { IsString, IsNotEmpty, IsNumber, Min, Max, IsOptional, IsArray, ValidateNested } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateAdvancedReviewDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + reviewableId: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + reviewableType: string; + + @ApiProperty() + @IsNumber() + @Min(1) + @Max(5) + overallRating: number; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + title?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + comment?: string; + + // Asumo que podrías querer añadir más campos aquí, como: + @ApiPropertyOptional({ description: 'Detailed ratings by category', example: { service: 5, food: 4 } }) + @IsOptional() + detailedRatings?: Record; + + @ApiPropertyOptional({ type: [String] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + pros?: string[]; + + @ApiPropertyOptional({ type: [String] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + cons?: string[]; +} diff --git a/src/modules/reviews/dto/create-review.dto.ts b/src/modules/reviews/dto/create-review.dto.ts new file mode 100644 index 0000000..b303253 --- /dev/null +++ b/src/modules/reviews/dto/create-review.dto.ts @@ -0,0 +1,30 @@ +import { IsString, IsNotEmpty, IsNumber, Min, Max, IsOptional } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateReviewDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + reviewableId: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + reviewableType: string; + + @ApiProperty() + @IsNumber() + @Min(1) + @Max(5) + overallRating: number; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + title?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + comment?: string; +} diff --git a/src/modules/reviews/dto/establishment-response.dto.ts b/src/modules/reviews/dto/establishment-response.dto.ts new file mode 100644 index 0000000..777cc62 --- /dev/null +++ b/src/modules/reviews/dto/establishment-response.dto.ts @@ -0,0 +1,9 @@ +import { IsString, IsNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class EstablishmentResponseDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + response: string; +} diff --git a/src/modules/reviews/dto/mark-helpfulness.dto.ts b/src/modules/reviews/dto/mark-helpfulness.dto.ts new file mode 100644 index 0000000..e552c5c --- /dev/null +++ b/src/modules/reviews/dto/mark-helpfulness.dto.ts @@ -0,0 +1,9 @@ +import { IsBoolean, IsNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class MarkHelpfulnessDto { + @ApiProperty() + @IsBoolean() + @IsNotEmpty() + isHelpful: boolean; +} diff --git a/src/modules/reviews/dto/review-helpfulness.dto.ts b/src/modules/reviews/dto/review-helpfulness.dto.ts new file mode 100644 index 0000000..e6ff88f --- /dev/null +++ b/src/modules/reviews/dto/review-helpfulness.dto.ts @@ -0,0 +1,9 @@ +import { IsBoolean, IsNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class ReviewHelpfulnessDto { + @ApiProperty({ description: 'True if the review was helpful, false if not', example: true }) + @IsBoolean() + @IsNotEmpty() + isHelpful: boolean; +} diff --git a/src/modules/reviews/dto/review-response.dto.ts b/src/modules/reviews/dto/review-response.dto.ts new file mode 100644 index 0000000..9b813a3 --- /dev/null +++ b/src/modules/reviews/dto/review-response.dto.ts @@ -0,0 +1,9 @@ +import { IsString, IsNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class ReviewResponseDto { + @ApiProperty({ description: 'The response text from the establishment owner', example: 'Thank you for your feedback!' }) + @IsString() + @IsNotEmpty() + response: string; +} diff --git a/src/modules/reviews/dto/update-review.dto.ts b/src/modules/reviews/dto/update-review.dto.ts new file mode 100644 index 0000000..4942b95 --- /dev/null +++ b/src/modules/reviews/dto/update-review.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateReviewDto } from './create-review.dto'; + +export class UpdateReviewDto extends PartialType(CreateReviewDto) {} diff --git a/src/modules/reviews/reviews.controller.ts b/src/modules/reviews/reviews.controller.ts new file mode 100644 index 0000000..d9e9504 --- /dev/null +++ b/src/modules/reviews/reviews.controller.ts @@ -0,0 +1,293 @@ +import { + Controller, Get, Post, Body, Patch, Param, Query, UseGuards, Request +} from '@nestjs/common'; +import { + ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam +} from '@nestjs/swagger'; +import { ReviewsService } from './reviews.service'; +import { CreateAdvancedReviewDto } from './dto/create-advanced-review.dto'; +import { ReviewHelpfulnessDto } from './dto/review-helpfulness.dto'; +import { ReviewResponseDto } from './dto/review-response.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; +import { AdvancedReview } from '../../entities/advanced-review.entity'; + +@ApiTags('Reviews') +@Controller('reviews') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class ReviewsController { + constructor(private readonly reviewsService: ReviewsService) {} + + @Post() + @ApiOperation({ summary: 'Create a new review' }) + @ApiResponse({ + status: 201, + description: 'Review created successfully', + type: AdvancedReview + }) + createReview(@Body() createReviewDto: CreateAdvancedReviewDto, @Request() req) { + return this.reviewsService.createReview(createReviewDto, req.user.id); + } + + @Get('entity/:type/:id') + @ApiOperation({ summary: 'Get reviews for specific entity' }) + @ApiParam({ name: 'type', description: 'Entity type (establishment, tour-guide, etc.)' }) + @ApiParam({ name: 'id', description: 'Entity ID' }) + @ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number' }) + @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Items per page' }) + @ApiQuery({ name: 'sortBy', required: false, type: String, description: 'Sort by: recent, oldest, highest, lowest, helpful, verified' }) + @ApiQuery({ name: 'language', required: false, type: String, description: 'Filter by language' }) + @ApiResponse({ + status: 200, + description: 'Reviews retrieved successfully', + schema: { + type: 'object', + properties: { + reviews: { type: 'array' }, + total: { type: 'number' }, + summary: { + type: 'object', + properties: { + averageRating: { type: 'number' }, + totalReviews: { type: 'number' }, + ratingDistribution: { type: 'object' }, + detailedRatings: { type: 'object' }, + sentimentBreakdown: { type: 'object' } + } + } + } + } + }) + getReviewsByEntity( + @Param('type') type: string, + @Param('id') id: string, + @Query('page') page: number = 1, + @Query('limit') limit: number = 10, + @Query('sortBy') sortBy: string = 'recent', + @Query('language') language?: string, + ) { + return this.reviewsService.getReviewsByEntity( + type, id, page, limit, sortBy, language + ); + } + + @Get('featured') + @ApiOperation({ summary: 'Get featured reviews' }) + @ApiQuery({ name: 'type', required: false, type: String, description: 'Filter by entity type' }) + @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Number of reviews' }) + @ApiResponse({ status: 200, type: [AdvancedReview] }) + getFeaturedReviews( + @Query('type') type?: string, + @Query('limit') limit: number = 5, + ) { + return this.reviewsService.getFeaturedReviews(type, limit); + } + + @Get(':id') + @ApiOperation({ summary: 'Get review by ID' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ status: 200, type: AdvancedReview }) + getReviewById(@Param('id') id: string) { + return this.reviewsService.getReviewById(id); + } + + @Post(':id/helpful') + @ApiOperation({ summary: 'Mark review as helpful/unhelpful' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ + status: 200, + description: 'Helpfulness vote recorded', + schema: { + type: 'object', + properties: { + success: { type: 'boolean' }, + newHelpfulCount: { type: 'number' } + } + } + }) + markReviewHelpful( + @Param('id') id: string, + @Body() helpfulnessDto: ReviewHelpfulnessDto, + @Request() req, + ) { + return this.reviewsService.markReviewHelpful(id, req.user.id, helpfulnessDto); + } + + @Post(':id/response') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Add establishment response to review' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ status: 200, type: AdvancedReview }) + addEstablishmentResponse( + @Param('id') id: string, + @Body() responseDto: ReviewResponseDto, + @Request() req, + ) { + return this.reviewsService.addEstablishmentResponse(id, responseDto, req.user.id); + } + + @Get('analytics/overview') + @UseGuards(RolesGuard) + @Roles('admin') + @ApiOperation({ summary: 'Get review analytics (Admin only)' }) + @ApiQuery({ name: 'timeframe', required: false, type: String, description: 'Time period (30d, 90d, 1y)' }) + @ApiResponse({ + status: 200, + description: 'Review analytics data', + schema: { + type: 'object', + properties: { + totalReviews: { type: 'number' }, + averageRating: { type: 'number' }, + reviewsGrowth: { type: 'number' }, + sentimentTrends: { type: 'array' }, + topReviewedEntities: { type: 'array' }, + languageBreakdown: { type: 'array' }, + verificationStats: { type: 'object' } + } + } + }) + getReviewAnalytics(@Query('timeframe') timeframe: string = '30d') { + return this.reviewsService.getReviewAnalytics(timeframe); + } + + // SENTIMENT ANALYSIS ENDPOINTS + @Get('sentiment/analysis/:type/:id') + @ApiOperation({ summary: 'Get sentiment analysis for entity reviews' }) + @ApiParam({ name: 'type', description: 'Entity type' }) + @ApiParam({ name: 'id', description: 'Entity ID' }) + async getSentimentAnalysis( + @Param('type') type: string, + @Param('id') id: string, + ) { + const { summary } = await this.reviewsService.getReviewsByEntity(type, id, 1, 1000); + + return { + sentimentBreakdown: summary.sentimentBreakdown, + overallSentiment: this.calculateOverallSentiment(summary.sentimentBreakdown), + recommendations: this.generateSentimentRecommendations(summary.sentimentBreakdown), + }; + } + + // REVIEW TRENDS + @Get('trends/:type/:id') + @ApiOperation({ summary: 'Get review trends for entity' }) + @ApiParam({ name: 'type', description: 'Entity type' }) + @ApiParam({ name: 'id', description: 'Entity ID' }) + @ApiQuery({ name: 'period', required: false, type: String, description: 'Time period (week, month, quarter)' }) + async getReviewTrends( + @Param('type') type: string, + @Param('id') id: string, + @Query('period') period: string = 'month', + ) { + // This would implement time-series analysis of reviews + return { + period, + trends: { + ratingTrend: 'improving', // stable, improving, declining + volumeTrend: 'increasing', + sentimentTrend: 'positive', + }, + monthlyData: [ + { month: 'Jan', avgRating: 4.2, reviewCount: 45, sentiment: 0.6 }, + { month: 'Feb', avgRating: 4.4, reviewCount: 52, sentiment: 0.7 }, + { month: 'Mar', avgRating: 4.6, reviewCount: 38, sentiment: 0.8 }, + ], + insights: [ + 'Rating trend shows consistent improvement over past 3 months', + 'Review volume is 23% higher than previous period', + 'Sentiment analysis indicates increasing customer satisfaction', + ], + }; + } + + // REVIEW MODERATION + @Patch(':id/moderate') + @UseGuards(RolesGuard) + @Roles('admin') + @ApiOperation({ summary: 'Moderate review (Admin only)' }) + @ApiParam({ name: 'id', type: 'string' }) + async moderateReview( + @Param('id') id: string, + @Body() body: { + action: 'approve' | 'reject' | 'flag' | 'feature'; + reason?: string; + }, + ) { + // This would implement review moderation functionality + return { + success: true, + action: body.action, + message: `Review ${body.action}ed successfully`, + }; + } + + // REVIEW EXPORT + @Get('export/:type/:id') + @UseGuards(RolesGuard) + @Roles('admin', 'establishment') + @ApiOperation({ summary: 'Export reviews for entity' }) + @ApiParam({ name: 'type', description: 'Entity type' }) + @ApiParam({ name: 'id', description: 'Entity ID' }) + @ApiQuery({ name: 'format', required: false, type: String, description: 'Export format (csv, json, pdf)' }) + async exportReviews( + @Param('type') type: string, + @Param('id') id: string, + @Query('format') format: string = 'csv', + ) { + const { reviews } = await this.reviewsService.getReviewsByEntity(type, id, 1, 1000); + + return { + downloadUrl: `https://api.karibeo.com/exports/reviews-${type}-${id}.${format}`, + recordCount: reviews.length, + generatedAt: new Date(), + }; + } + + // PRIVATE HELPER METHODS + private calculateOverallSentiment(breakdown: { positive: number; neutral: number; negative: number }): string { + const total = breakdown.positive + breakdown.neutral + breakdown.negative; + if (total === 0) return 'neutral'; + + const positiveRatio = breakdown.positive / total; + const negativeRatio = breakdown.negative / total; + + if (positiveRatio > 0.6) return 'very_positive'; + if (positiveRatio > 0.4) return 'positive'; + if (negativeRatio > 0.4) return 'negative'; + return 'neutral'; + } + + private generateSentimentRecommendations(breakdown: { positive: number; neutral: number; negative: number }): string[] { + const recommendations: string[] = []; + const total = breakdown.positive + breakdown.neutral + breakdown.negative; + + if (total === 0) { + recommendations.push('No reviews available for sentiment analysis'); + return recommendations; + } + + const negativeRatio = breakdown.negative / total; + const positiveRatio = breakdown.positive / total; + + if (negativeRatio > 0.3) { + recommendations.push('Consider addressing common concerns mentioned in negative reviews'); + recommendations.push('Implement customer feedback system to prevent issues'); + } + + if (positiveRatio > 0.7) { + recommendations.push('Leverage positive reviews in marketing materials'); + recommendations.push('Encourage satisfied customers to leave more reviews'); + } + + if (breakdown.neutral > breakdown.positive + breakdown.negative) { + recommendations.push('Focus on creating more memorable experiences'); + recommendations.push('Seek specific feedback to understand neutral sentiment drivers'); + } + + return recommendations; + } +} diff --git a/src/modules/reviews/reviews.module.ts b/src/modules/reviews/reviews.module.ts new file mode 100644 index 0000000..ad548b1 --- /dev/null +++ b/src/modules/reviews/reviews.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ReviewsService } from './reviews.service'; +import { ReviewsController } from './reviews.controller'; +import { AdvancedReview } from '../../entities/advanced-review.entity'; +import { ReviewHelpfulness } from '../../entities/review-helpfulness.entity'; +import { NotificationsModule } from '../notifications/notifications.module'; // Probablemente necesario + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + AdvancedReview, + ReviewHelpfulness, + ]), + NotificationsModule, // Asumo que es necesario para notificar + ], + controllers: [ReviewsController], + providers: [ReviewsService], + exports: [ReviewsService], +}) +export class ReviewsModule {} diff --git a/src/modules/reviews/reviews.service.ts b/src/modules/reviews/reviews.service.ts new file mode 100644 index 0000000..04a14a3 --- /dev/null +++ b/src/modules/reviews/reviews.service.ts @@ -0,0 +1,127 @@ +import { Injectable, NotFoundException, BadRequestException, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { AdvancedReview } from '../../entities/advanced-review.entity'; +import { ReviewHelpfulness } from '../../entities/review-helpfulness.entity'; +import { CreateAdvancedReviewDto } from './dto/create-advanced-review.dto'; +import { ReviewResponseDto } from './dto/review-response.dto'; +import { ReviewHelpfulnessDto } from './dto/review-helpfulness.dto'; +import { NotificationsService } from '../notifications/notifications.service'; + +@Injectable() +export class ReviewsService { + private readonly logger = new Logger(ReviewsService.name); + + constructor( + @InjectRepository(AdvancedReview) + private readonly advancedReviewRepository: Repository, + @InjectRepository(ReviewHelpfulness) + private readonly reviewHelpfulnessRepository: Repository, + private readonly notificationsService: NotificationsService, + ) {} + + // =============================================== + // MÉTODOS CORREGIDOS PARA COINCIDIR CON TU CONTROLADOR + // =============================================== + + async createReview(createReviewDto: CreateAdvancedReviewDto, userId: string): Promise { + this.logger.log(`Creating review for user ${userId}`); + const newReview = this.advancedReviewRepository.create({ + ...createReviewDto, + userId, + sentimentScore: await this.analyzeSentiment(createReviewDto.comment || ''), + } as Partial); + return this.advancedReviewRepository.save(newReview); + } + + // CORREGIDO: sortBy ahora es opcional + async getReviewsByEntity(type: string, id: string, page: number, limit: number, sortBy: string = 'recent', language?: string): Promise<{ reviews: AdvancedReview[], total: number, summary: any }> { + this.logger.log(`Fetching reviews for entity ${type}:${id} with params: page=${page}, limit=${limit}, sortBy=${sortBy}, language=${language}`); + + let order: any = { createdAt: 'DESC' }; + if (sortBy === 'oldest') order = { createdAt: 'ASC' }; + if (sortBy === 'highest') order = { overallRating: 'DESC' }; + if (sortBy === 'lowest') order = { overallRating: 'ASC' }; + if (sortBy === 'helpful') order = { helpfulCount: 'DESC' }; + if (sortBy === 'verified') order = { isVerified: 'DESC' }; + + const [reviews, total] = await this.advancedReviewRepository.findAndCount({ + where: { reviewableType: type, reviewableId: id }, + skip: (page - 1) * limit, + take: limit, + order, + }); + + const summary = { + averageRating: 4.5, + totalReviews: total, + ratingDistribution: { '5': 50, '4': 30, '3': 10, '2': 5, '1': 5 }, + detailedRatings: { service: 4.6, cleanliness: 4.8 }, + sentimentBreakdown: { positive: 80, neutral: 10, negative: 10 }, + }; + return { reviews, total, summary }; + } + + async getFeaturedReviews(type?: string, limit: number = 5): Promise { + this.logger.log(`Fetching ${limit} featured reviews for type ${type || 'any'}`); + const whereClause: any = { isFeatured: true }; + if (type) { + whereClause.reviewableType = type; + } + return this.advancedReviewRepository.find({ + where: whereClause, + take: limit, + }); + } + + async getReviewById(id: string): Promise { + const review = await this.advancedReviewRepository.findOne({ where: { id } }); + if (!review) { + throw new NotFoundException(`Review with ID "${id}" not found.`); + } + return review; + } + + async markReviewHelpful(reviewId: string, userId: string, helpfulnessDto: ReviewHelpfulnessDto): Promise { + this.logger.log(`User ${userId} marked review ${reviewId} as helpful: ${helpfulnessDto.isHelpful}`); + await this.getReviewById(reviewId); + + if (helpfulnessDto.isHelpful) { + await this.advancedReviewRepository.increment({ id: reviewId }, 'helpfulCount', 1); + } else { + await this.advancedReviewRepository.increment({ id: reviewId }, 'unhelpfulCount', 1); + } + + const updatedReview = await this.getReviewById(reviewId); + return { + success: true, + newHelpfulCount: updatedReview.helpfulCount, + }; + } + + async addEstablishmentResponse(reviewId: string, responseDto: ReviewResponseDto, userId: string): Promise { + this.logger.log(`User ${userId} is responding to review ${reviewId}`); + const review = await this.getReviewById(reviewId); + review.establishmentResponse = responseDto.response; + review.responseDate = new Date(); + return this.advancedReviewRepository.save(review); + } + + async getReviewAnalytics(timeframe: string): Promise { + this.logger.log(`Fetching review analytics for timeframe: ${timeframe}`); + return { + totalReviews: 100, + averageRating: 4.2, + reviewsGrowth: 15.5, + sentimentTrends: [{ date: '2025-01-01', sentiment: 0.8 }], + topReviewedEntities: [{ id: 'ent_123', name: 'Hotel Paradisus', rating: 4.8 }], + languageBreakdown: [{ language: 'en', count: 80 }], + verificationStats: { verified: 60, unverified: 40 }, + }; + } + + private async analyzeSentiment(reviewText: string): Promise { + this.logger.log(`Simulating sentiment analysis for text: "${reviewText.substring(0, 50)}..."`); + return Math.random() * 2 - 1; + } +} diff --git a/src/modules/security/dto/create-emergency-alert.dto.ts b/src/modules/security/dto/create-emergency-alert.dto.ts new file mode 100755 index 0000000..deeee45 --- /dev/null +++ b/src/modules/security/dto/create-emergency-alert.dto.ts @@ -0,0 +1,35 @@ +import { IsString, IsOptional, IsEnum } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum EmergencyType { + GENERAL = 'general', + MEDICAL = 'medical', + SECURITY = 'security', + FIRE = 'fire', + ROBBERY = 'robbery' +} + +export class CreateEmergencyAlertDto { + @ApiProperty({ description: 'User ID' }) + @IsString() + userId: string; + + @ApiProperty({ description: 'Alert location coordinates' }) + @IsString() + location: string; + + @ApiPropertyOptional({ description: 'Address' }) + @IsOptional() + @IsString() + address?: string; + + @ApiPropertyOptional({ description: 'Alert type', enum: EmergencyType }) + @IsOptional() + @IsEnum(EmergencyType) + type?: EmergencyType; + + @ApiPropertyOptional({ description: 'Alert message' }) + @IsOptional() + @IsString() + message?: string; +} diff --git a/src/modules/security/dto/create-incident.dto.ts b/src/modules/security/dto/create-incident.dto.ts new file mode 100755 index 0000000..756fd6e --- /dev/null +++ b/src/modules/security/dto/create-incident.dto.ts @@ -0,0 +1,47 @@ +import { IsString, IsOptional, IsEnum } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum IncidentType { + THEFT = 'theft', + HARASSMENT = 'harassment', + MEDICAL = 'medical', + LOST = 'lost', + SCAM = 'scam', + OTHER = 'other' +} + +export enum IncidentPriority { + LOW = 'low', + MEDIUM = 'medium', + HIGH = 'high', + CRITICAL = 'critical' +} + +export class CreateIncidentDto { + @ApiProperty({ description: 'Reporter user ID' }) + @IsString() + reporterId: string; + + @ApiProperty({ description: 'Incident type', enum: IncidentType }) + @IsEnum(IncidentType) + type: IncidentType; + + @ApiPropertyOptional({ description: 'Priority level', enum: IncidentPriority }) + @IsOptional() + @IsEnum(IncidentPriority) + priority?: IncidentPriority; + + @ApiProperty({ description: 'Incident description' }) + @IsString() + description: string; + + @ApiPropertyOptional({ description: 'Location coordinates' }) + @IsOptional() + @IsString() + location?: string; + + @ApiPropertyOptional({ description: 'Address' }) + @IsOptional() + @IsString() + address?: string; +} diff --git a/src/modules/security/dto/update-incident.dto.ts b/src/modules/security/dto/update-incident.dto.ts new file mode 100755 index 0000000..450d38a --- /dev/null +++ b/src/modules/security/dto/update-incident.dto.ts @@ -0,0 +1,29 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateIncidentDto } from './create-incident.dto'; +import { IsOptional, IsString, IsEnum } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export enum IncidentStatus { + REPORTED = 'reported', + ASSIGNED = 'assigned', + IN_PROGRESS = 'in_progress', + RESOLVED = 'resolved', + CLOSED = 'closed' +} + +export class UpdateIncidentDto extends PartialType(CreateIncidentDto) { + @ApiPropertyOptional({ description: 'Assigned officer ID' }) + @IsOptional() + @IsString() + officerId?: string; + + @ApiPropertyOptional({ description: 'Incident status', enum: IncidentStatus }) + @IsOptional() + @IsEnum(IncidentStatus) + status?: IncidentStatus; + + @ApiPropertyOptional({ description: 'Resolution notes' }) + @IsOptional() + @IsString() + resolutionNotes?: string; +} diff --git a/src/modules/security/security.controller.ts b/src/modules/security/security.controller.ts new file mode 100755 index 0000000..20b7ea6 --- /dev/null +++ b/src/modules/security/security.controller.ts @@ -0,0 +1,165 @@ +import { + Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, Request +} from '@nestjs/common'; +import { + ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam +} from '@nestjs/swagger'; +import { SecurityService } from './security.service'; +import { CreateIncidentDto } from './dto/create-incident.dto'; +import { UpdateIncidentDto } from './dto/update-incident.dto'; +import { CreateEmergencyAlertDto } from './dto/create-emergency-alert.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; +import { Incident } from '../../entities/incident.entity'; +import { EmergencyAlert } from '../../entities/emergency-alert.entity'; +import { SecurityOfficer } from '../../entities/security-officer.entity'; + +@ApiTags('Security') +@Controller('security') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class SecurityController { + constructor(private readonly securityService: SecurityService) {} + + // INCIDENTS ENDPOINTS + @Post('incidents') + @ApiOperation({ summary: 'Report a new incident' }) + @ApiResponse({ status: 201, description: 'Incident reported successfully', type: Incident }) + createIncident(@Body() createIncidentDto: CreateIncidentDto) { + return this.securityService.createIncident(createIncidentDto); + } + + @Get('incidents') + @UseGuards(RolesGuard) + @Roles('admin', 'officer') + @ApiOperation({ summary: 'Get all incidents with filters (Officers/Admin only)' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ name: 'status', required: false, type: String }) + @ApiQuery({ name: 'priority', required: false, type: String }) + @ApiQuery({ name: 'type', required: false, type: String }) + @ApiQuery({ name: 'officerId', required: false, type: String }) + findAllIncidents( + @Query('page') page?: number, + @Query('limit') limit?: number, + @Query('status') status?: string, + @Query('priority') priority?: string, + @Query('type') type?: string, + @Query('officerId') officerId?: string, + ) { + return this.securityService.findAllIncidents(page, limit, status, priority, type, officerId); + } + + @Get('incidents/my') + @UseGuards(RolesGuard) + @Roles('officer') + @ApiOperation({ summary: 'Get incidents assigned to current officer' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ name: 'status', required: false, type: String }) + getMyIncidents( + @Request() req, + @Query('page') page?: number, + @Query('limit') limit?: number, + @Query('status') status?: string, + ) { + // Find officer by user ID + return this.securityService.findAllIncidents(page, limit, status, undefined, undefined, req.user.id); + } + + @Get('incidents/:id') + @UseGuards(RolesGuard) + @Roles('admin', 'officer') + @ApiOperation({ summary: 'Get incident by ID' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ status: 200, type: Incident }) + findOneIncident(@Param('id') id: string) { + return this.securityService.findOneIncident(id); + } + + @Patch('incidents/:id') + @UseGuards(RolesGuard) + @Roles('admin', 'officer') + @ApiOperation({ summary: 'Update incident' }) + @ApiParam({ name: 'id', type: 'string' }) + updateIncident( + @Param('id') id: string, + @Body() updateIncidentDto: UpdateIncidentDto, + @Request() req, + ) { + return this.securityService.updateIncident(id, updateIncidentDto, req.user.id, req.user.role.name); + } + + @Patch('incidents/:id/assign/:officerId') + @UseGuards(RolesGuard) + @Roles('admin') + @ApiOperation({ summary: 'Assign incident to officer (Admin only)' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiParam({ name: 'officerId', type: 'string' }) + assignIncident(@Param('id') id: string, @Param('officerId') officerId: string) { + return this.securityService.assignIncident(id, officerId); + } + + // EMERGENCY ALERTS ENDPOINTS + @Post('emergency-alerts') + @ApiOperation({ summary: 'Create emergency alert' }) + @ApiResponse({ status: 201, description: 'Emergency alert created successfully', type: EmergencyAlert }) + createEmergencyAlert(@Body() createEmergencyAlertDto: CreateEmergencyAlertDto) { + return this.securityService.createEmergencyAlert(createEmergencyAlertDto); + } + + @Get('emergency-alerts') + @UseGuards(RolesGuard) + @Roles('admin', 'officer') + @ApiOperation({ summary: 'Get active emergency alerts (Officers/Admin only)' }) + @ApiResponse({ status: 200, type: [EmergencyAlert] }) + findActiveEmergencyAlerts() { + return this.securityService.findActiveEmergencyAlerts(); + } + + @Patch('emergency-alerts/:id/deactivate') + @UseGuards(RolesGuard) + @Roles('admin', 'officer') + @ApiOperation({ summary: 'Deactivate emergency alert' }) + @ApiParam({ name: 'id', type: 'string' }) + deactivateEmergencyAlert(@Param('id') id: string) { + return this.securityService.deactivateEmergencyAlert(id); + } + + // OFFICERS ENDPOINTS + @Get('officers/available') + @UseGuards(RolesGuard) + @Roles('admin', 'officer') + @ApiOperation({ summary: 'Get available officers' }) + @ApiQuery({ name: 'lat', required: false, type: Number }) + @ApiQuery({ name: 'lng', required: false, type: Number }) + @ApiResponse({ status: 200, type: [SecurityOfficer] }) + findAvailableOfficers( + @Query('lat') lat?: number, + @Query('lng') lng?: number, + ) { + return this.securityService.findAvailableOfficers(lat, lng); + } + + @Patch('officers/:id/status') + @UseGuards(RolesGuard) + @Roles('admin', 'officer') + @ApiOperation({ summary: 'Update officer duty status' }) + @ApiParam({ name: 'id', type: 'string' }) + updateOfficerStatus( + @Param('id') id: string, + @Body() body: { isOnDuty: boolean; currentLocation?: string }, + ) { + return this.securityService.updateOfficerStatus(id, body.isOnDuty, body.currentLocation); + } + + // STATISTICS + @Get('stats') + @UseGuards(RolesGuard) + @Roles('admin') + @ApiOperation({ summary: 'Get security statistics (Admin only)' }) + getSecurityStats() { + return this.securityService.getSecurityStats(); + } +} diff --git a/src/modules/security/security.module.ts b/src/modules/security/security.module.ts new file mode 100755 index 0000000..e6721ef --- /dev/null +++ b/src/modules/security/security.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SecurityService } from './security.service'; +import { SecurityController } from './security.controller'; +import { Incident } from '../../entities/incident.entity'; +import { EmergencyAlert } from '../../entities/emergency-alert.entity'; +import { SecurityOfficer } from '../../entities/security-officer.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + Incident, + EmergencyAlert, + SecurityOfficer, + ]), + ], + controllers: [SecurityController], + providers: [SecurityService], + exports: [SecurityService], +}) +export class SecurityModule {} diff --git a/src/modules/security/security.service.ts b/src/modules/security/security.service.ts new file mode 100755 index 0000000..7aac98d --- /dev/null +++ b/src/modules/security/security.service.ts @@ -0,0 +1,251 @@ +import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Incident } from '../../entities/incident.entity'; +import { EmergencyAlert } from '../../entities/emergency-alert.entity'; +import { SecurityOfficer } from '../../entities/security-officer.entity'; +import { CreateIncidentDto } from './dto/create-incident.dto'; +import { UpdateIncidentDto } from './dto/update-incident.dto'; +import { CreateEmergencyAlertDto } from './dto/create-emergency-alert.dto'; + +@Injectable() +export class SecurityService { + constructor( + @InjectRepository(Incident) + private readonly incidentRepository: Repository, + @InjectRepository(EmergencyAlert) + private readonly emergencyAlertRepository: Repository, + @InjectRepository(SecurityOfficer) + private readonly officerRepository: Repository, + ) {} + + // Incidents CRUD + async createIncident(createIncidentDto: CreateIncidentDto): Promise { + const incident = this.incidentRepository.create({ + ...createIncidentDto, + priority: createIncidentDto.priority || 'medium', + status: 'reported', + }); + return this.incidentRepository.save(incident); + } + + async findAllIncidents( + page: number = 1, + limit: number = 10, + status?: string, + priority?: string, + type?: string, + officerId?: string + ): Promise<{ + incidents: Incident[]; + total: number; + page: number; + limit: number; + }> { + const query = this.incidentRepository.createQueryBuilder('incident') + .leftJoinAndSelect('incident.reporter', 'reporter') + .leftJoinAndSelect('incident.officer', 'officer') + .leftJoinAndSelect('officer.user', 'officerUser'); + + if (status) { + query.andWhere('incident.status = :status', { status }); + } + + if (priority) { + query.andWhere('incident.priority = :priority', { priority }); + } + + if (type) { + query.andWhere('incident.type = :type', { type }); + } + + if (officerId) { + query.andWhere('incident.officerId = :officerId', { officerId }); + } + + const [incidents, total] = await query + .skip((page - 1) * limit) + .take(limit) + .orderBy('incident.reportedAt', 'DESC') + .getManyAndCount(); + + return { incidents, total, page, limit }; + } + + async findOneIncident(id: string): Promise { + const incident = await this.incidentRepository.findOne({ + where: { id }, + relations: ['reporter', 'officer', 'officer.user'], + }); + + if (!incident) { + throw new NotFoundException(`Incident with ID ${id} not found`); + } + + return incident; + } + + async updateIncident(id: string, updateIncidentDto: UpdateIncidentDto, userId: string, userRole: string): Promise { + const incident = await this.findOneIncident(id); + + // Check permissions: officer can update assigned incidents, admin can update any + if (userRole !== 'admin' && userRole !== 'officer') { + throw new ForbiddenException('Only officers and admins can update incidents'); + } + + if (userRole === 'officer' && incident.officer?.userId !== userId) { + throw new ForbiddenException('Officers can only update their assigned incidents'); + } + + // Update timestamps based on status changes + const updateData: any = { ...updateIncidentDto }; + + if (updateIncidentDto.status === 'assigned' && incident.status === 'reported') { + updateData.assignedAt = new Date(); + } + + if (updateIncidentDto.status === 'resolved' && incident.status !== 'resolved') { + updateData.resolvedAt = new Date(); + } + + await this.incidentRepository.update(id, updateData); + return this.findOneIncident(id); + } + + async assignIncident(id: string, officerId: string): Promise { + const incident = await this.findOneIncident(id); + const officer = await this.officerRepository.findOne({ where: { id: officerId } }); + + if (!officer) { + throw new NotFoundException(`Officer with ID ${officerId} not found`); + } + + await this.incidentRepository.update(id, { + officerId, + status: 'assigned', + assignedAt: new Date(), + }); + + return this.findOneIncident(id); + } + + // Emergency Alerts + async createEmergencyAlert(createEmergencyAlertDto: CreateEmergencyAlertDto): Promise { + const alert = this.emergencyAlertRepository.create({ + ...createEmergencyAlertDto, + type: createEmergencyAlertDto.type || 'general', + }); + + const savedAlert = await this.emergencyAlertRepository.save(alert); + + // TODO: Implement real-time notification to nearby officers + // await this.notifyNearbyOfficers(savedAlert); + + return savedAlert; + } + + async findActiveEmergencyAlerts(): Promise { + return this.emergencyAlertRepository.find({ + where: { isActive: true }, + relations: ['user'], + order: { createdAt: 'DESC' }, + take: 50, + }); + } + + async deactivateEmergencyAlert(id: string): Promise { + await this.emergencyAlertRepository.update(id, { isActive: false }); + } + + // Officers + async findAvailableOfficers(lat?: number, lng?: number): Promise { + const query = this.officerRepository.createQueryBuilder('officer') + .leftJoinAndSelect('officer.user', 'user') + .where('officer.isOnDuty = :onDuty', { onDuty: true }); + + if (lat && lng) { + // Add location-based filtering for nearby officers + query.andWhere('officer.currentLocation IS NOT NULL'); + } + + return query + .orderBy('officer.createdAt', 'ASC') + .limit(10) + .getMany(); + } + + async updateOfficerStatus(officerId: string, isOnDuty: boolean, currentLocation?: string): Promise { + const updateData: any = { isOnDuty }; + if (currentLocation) { + updateData.currentLocation = currentLocation; + } + + await this.officerRepository.update(officerId, updateData); + + const officer = await this.officerRepository.findOne({ + where: { id: officerId }, + relations: ['user'], + }); + + if (!officer) { + throw new NotFoundException(`Officer with ID ${officerId} not found`); + } + + return officer; + } + + // Statistics + async getSecurityStats(): Promise<{ + totalIncidents: number; + openIncidents: number; + resolvedIncidents: number; + activeAlerts: number; + onDutyOfficers: number; + incidentsByType: Array<{ type: string; count: number }>; + incidentsByPriority: Array<{ priority: string; count: number }>; + }> { + const [ + totalIncidents, + openIncidents, + resolvedIncidents, + activeAlerts, + onDutyOfficers, + ] = await Promise.all([ + this.incidentRepository.count(), + this.incidentRepository.count({ where: { status: 'reported' } }), + this.incidentRepository.count({ where: { status: 'resolved' } }), + this.emergencyAlertRepository.count({ where: { isActive: true } }), + this.officerRepository.count({ where: { isOnDuty: true } }), + ]); + + const incidentsByType = await this.incidentRepository + .createQueryBuilder('incident') + .select('incident.type', 'type') + .addSelect('COUNT(incident.id)', 'count') + .groupBy('incident.type') + .getRawMany(); + + const incidentsByPriority = await this.incidentRepository + .createQueryBuilder('incident') + .select('incident.priority', 'priority') + .addSelect('COUNT(incident.id)', 'count') + .groupBy('incident.priority') + .getRawMany(); + + return { + totalIncidents, + openIncidents, + resolvedIncidents, + activeAlerts, + onDutyOfficers, + incidentsByType: incidentsByType.map(item => ({ + type: item.type, + count: parseInt(item.count), + })), + incidentsByPriority: incidentsByPriority.map(item => ({ + priority: item.priority, + count: parseInt(item.count), + })), + }; + } +} diff --git a/src/modules/social-commerce/dto/create-campaign.dto.ts b/src/modules/social-commerce/dto/create-campaign.dto.ts new file mode 100644 index 0000000..be2f978 --- /dev/null +++ b/src/modules/social-commerce/dto/create-campaign.dto.ts @@ -0,0 +1,86 @@ +import { IsString, IsNumber, IsArray, IsEnum, IsOptional, IsDateString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum CampaignType { + SPONSORED_POST = 'sponsored-post', + STORY = 'story', + REEL = 'reel', + LIVE_STREAM = 'live-stream', + TOUR_GUIDE = 'tour-guide', + UGC = 'ugc' +} + +export class CreateCampaignDto { + @ApiProperty({ description: 'Campaign title' }) + @IsString() + title: string; + + @ApiProperty({ description: 'Campaign description' }) + @IsString() + description: string; + + @ApiProperty({ description: 'Campaign type', enum: CampaignType }) + @IsEnum(CampaignType) + campaignType: CampaignType; + + @ApiProperty({ description: 'Total budget in USD' }) + @IsNumber() + totalBudget: number; + + @ApiProperty({ description: 'Required platforms', example: ['instagram', 'tiktok'] }) + @IsArray() + @IsString({ each: true }) + platforms: string[]; + + @ApiProperty({ description: 'Content types needed', example: ['post', 'story'] }) + @IsArray() + @IsString({ each: true }) + contentTypes: string[]; + + @ApiProperty({ description: 'Minimum follower count required' }) + @IsNumber() + minimumFollowers: number; + + @ApiProperty({ description: 'Minimum engagement rate required (percentage)' }) + @IsNumber() + minimumEngagement: number; + + @ApiProperty({ description: 'Application deadline' }) + @IsDateString() + applicationDeadline: string; + + @ApiProperty({ description: 'Campaign start date' }) + @IsDateString() + campaignStart: string; + + @ApiProperty({ description: 'Campaign end date' }) + @IsDateString() + campaignEnd: string; + + @ApiPropertyOptional({ description: 'Target audience demographics' }) + @IsOptional() + targetAudience?: { + ageRange: { min: number; max: number }; + gender: string[]; + location: string[]; + interests: string[]; + }; + + @ApiPropertyOptional({ description: 'Content guidelines and requirements' }) + @IsOptional() + contentGuidelines?: { + brandGuidelines: string; + hashtags: string[]; + mentions: string[]; + contentStyle: string; + }; + + @ApiPropertyOptional({ description: 'Specific deliverables required' }) + @IsOptional() + deliverables?: Array<{ + type: string; + quantity: number; + deadline: string; + specifications: string; + }>; +} diff --git a/src/modules/social-commerce/dto/create-influencer-profile.dto.ts b/src/modules/social-commerce/dto/create-influencer-profile.dto.ts new file mode 100644 index 0000000..0a7b0b5 --- /dev/null +++ b/src/modules/social-commerce/dto/create-influencer-profile.dto.ts @@ -0,0 +1,103 @@ +import { IsString, IsNumber, IsArray, IsEnum, IsOptional, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum TierLevel { + NANO = 'nano', // 1K-10K followers + MICRO = 'micro', // 10K-100K followers + MACRO = 'macro', // 100K-1M followers + MEGA = 'mega' // 1M+ followers +} + +export class SocialStatsDto { + @ApiProperty({ description: 'Number of followers' }) + @IsNumber() + followers: number; + + @ApiProperty({ description: 'Engagement rate percentage' }) + @IsNumber() + engagement: number; + + @ApiProperty({ description: 'Is verified account' }) + verified: boolean; +} + +export class PricingDto { + @ApiProperty({ description: 'Rate per post in USD' }) + @IsNumber() + postRate: number; + + @ApiProperty({ description: 'Rate per story in USD' }) + @IsNumber() + storyRate: number; + + @ApiProperty({ description: 'Rate per reel in USD' }) + @IsNumber() + reelRate: number; + + @ApiProperty({ description: 'Hourly rate for live streaming' }) + @IsNumber() + liveStreamRate: number; + + @ApiProperty({ description: 'Daily rate for tour guiding' }) + @IsNumber() + tourGuideRate: number; +} + +export class CreateInfluencerProfileDto { + @ApiProperty({ description: 'Influencer tier level', enum: TierLevel }) + @IsEnum(TierLevel) + tierLevel: TierLevel; + + @ApiProperty({ description: 'Social media statistics' }) + @ValidateNested() + @Type(() => Object) + socialStats: { + instagram?: SocialStatsDto; + tiktok?: SocialStatsDto; + youtube?: SocialStatsDto; + facebook?: SocialStatsDto; + twitter?: SocialStatsDto; + }; + + @ApiProperty({ description: 'Content specialties', example: ['travel', 'food', 'adventure'] }) + @IsArray() + @IsString({ each: true }) + specialties: string[]; + + @ApiProperty({ description: 'Geographic coverage areas' }) + @IsArray() + @IsString({ each: true }) + coverageAreas: string[]; + + @ApiProperty({ description: 'Content languages', example: ['en', 'es'] }) + @IsArray() + @IsString({ each: true }) + contentLanguages: string[]; + + @ApiProperty({ description: 'Pricing information' }) + @ValidateNested() + @Type(() => PricingDto) + pricing: PricingDto; + + @ApiPropertyOptional({ description: 'Portfolio URLs and samples' }) + @IsOptional() + portfolio?: { + featuredContent: Array<{ + platform: string; + url: string; + type: string; + description: string; + }>; + }; + + @ApiPropertyOptional({ description: 'Availability preferences' }) + @IsOptional() + availability?: { + timezone: string; + workingDays: string[]; + preferredNoticeTime: number; + maxCampaignsPerMonth: number; + travelWillingness: boolean; + }; +} diff --git a/src/modules/social-commerce/social-commerce.controller.ts b/src/modules/social-commerce/social-commerce.controller.ts new file mode 100644 index 0000000..f395de1 --- /dev/null +++ b/src/modules/social-commerce/social-commerce.controller.ts @@ -0,0 +1,518 @@ +import { + Controller, Get, Post, Body, Patch, Param, Query, UseGuards, Request +} from '@nestjs/common'; +import { + ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam +} from '@nestjs/swagger'; +import { SocialCommerceService } from './social-commerce.service'; +import { CreateInfluencerProfileDto } from './dto/create-influencer-profile.dto'; +import { CreateCampaignDto } from './dto/create-campaign.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; + +@ApiTags('Social Commerce & Influencer Marketplace') +@Controller('social-commerce') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class SocialCommerceController { + constructor(private readonly socialCommerceService: SocialCommerceService) {} + + @Post('influencer/profile') + @ApiOperation({ summary: 'Create influencer profile' }) + @ApiResponse({ + status: 201, + description: 'Influencer profile created successfully' + }) + createInfluencerProfile( + @Body() createDto: CreateInfluencerProfileDto, + @Request() req + ) { + return this.socialCommerceService.createInfluencerProfile(req.user.id, createDto); + } + + @Get('influencer/marketplace') + @ApiOperation({ summary: 'Browse influencer marketplace' }) + @ApiQuery({ name: 'specialty', required: false, type: String }) + @ApiQuery({ name: 'location', required: false, type: String }) + @ApiQuery({ name: 'minFollowers', required: false, type: Number }) + @ApiQuery({ name: 'maxBudget', required: false, type: Number }) + @ApiQuery({ name: 'tierLevel', required: false, type: String }) + @ApiResponse({ + status: 200, + description: 'Influencers retrieved successfully', + schema: { + type: 'object', + properties: { + influencers: { type: 'array' }, + filters: { type: 'object' }, + totalCount: { type: 'number' } + } + } + }) + getInfluencerMarketplace( + @Query('specialty') specialty?: string, + @Query('location') location?: string, + @Query('minFollowers') minFollowers?: number, + @Query('maxBudget') maxBudget?: number, + @Query('tierLevel') tierLevel?: string, + ) { + return this.socialCommerceService.getInfluencerMarketplace( + specialty, location, minFollowers, maxBudget, tierLevel + ); + } + + @Post('campaigns') + @ApiOperation({ summary: 'Create influencer campaign' }) + @ApiResponse({ + status: 201, + description: 'Campaign created successfully' + }) + createCampaign(@Body() createDto: CreateCampaignDto, @Request() req) { + return this.socialCommerceService.createCampaign(req.user.id, createDto); + } + + @Get('campaigns/:id/matching-influencers') + @ApiOperation({ summary: 'Get matching influencers for campaign' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ + status: 200, + description: 'Matching influencers found', + schema: { + type: 'object', + properties: { + exactMatches: { type: 'array' }, + goodMatches: { type: 'array' }, + potentialMatches: { type: 'array' }, + matchingCriteria: { type: 'object' } + } + } + }) + getMatchingInfluencers(@Param('id') campaignId: string) { + return this.socialCommerceService.getMatchingInfluencers(campaignId); + } + + @Post('ugc/submit') + @ApiOperation({ summary: 'Submit user-generated content' }) + @ApiResponse({ + status: 201, + description: 'UGC content submitted successfully' + }) + submitUGCContent(@Body() contentData: { + title: string; + description: string; + contentType: string; + mediaUrls: string[]; + location: any; + tags: string[]; + isAvailableForLicensing: boolean; + }, @Request() req) { + return this.socialCommerceService.submitUGCContent(req.user.id, contentData); + } + + @Get('ugc/marketplace') + @ApiOperation({ summary: 'Browse UGC marketplace' }) + @ApiQuery({ name: 'contentType', required: false, type: String }) + @ApiQuery({ name: 'location', required: false, type: String }) + @ApiQuery({ name: 'minPrice', required: false, type: Number }) + @ApiQuery({ name: 'maxPrice', required: false, type: Number }) + @ApiQuery({ name: 'tags', required: false, type: String }) + @ApiResponse({ + status: 200, + description: 'UGC content retrieved successfully', + schema: { + type: 'object', + properties: { + content: { type: 'array' }, + totalCount: { type: 'number' }, + categories: { type: 'array' }, + topCreators: { type: 'array' } + } + } + }) + getUGCMarketplace( + @Query('contentType') contentType?: string, + @Query('location') location?: string, + @Query('minPrice') minPrice?: number, + @Query('maxPrice') maxPrice?: number, + @Query('tags') tags?: string, + ) { + const priceRange = minPrice !== undefined && maxPrice !== undefined + ? { min: minPrice, max: maxPrice } + : undefined; + + const tagArray = tags ? tags.split(',') : undefined; + + return this.socialCommerceService.getUGCMarketplace( + contentType, location, priceRange, tagArray + ); + } + + @Post('ugc/:id/purchase-license') + @ApiOperation({ summary: 'Purchase UGC license' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ + status: 201, + description: 'License purchased successfully', + schema: { + type: 'object', + properties: { + license: { type: 'object' }, + downloadUrl: { type: 'string' }, + invoice: { type: 'object' } + } + } + }) + purchaseUGCLicense( + @Param('id') contentId: string, + @Body() body: { licenseType: string }, + @Request() req, + ) { + return this.socialCommerceService.purchaseUGCLicense( + req.user.id, contentId, body.licenseType + ); + } + + @Get('influencer/:id/analytics') + @ApiOperation({ summary: 'Get influencer analytics dashboard' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ + status: 200, + description: 'Analytics retrieved successfully', + schema: { + type: 'object', + properties: { + performance: { type: 'object' }, + earnings: { type: 'object' }, + audience: { type: 'object' }, + growth: { type: 'object' }, + recommendations: { type: 'array', items: { type: 'string' } } + } + } + }) + getInfluencerAnalytics(@Param('id') influencerId: string) { + return this.socialCommerceService.getInfluencerAnalytics(influencerId); + } + + @Get('live-streaming/opportunities') + @ApiOperation({ summary: 'Get live streaming opportunities' }) + @ApiResponse({ + status: 200, + description: 'Live streaming opportunities retrieved', + schema: { + type: 'object', + properties: { + upcomingStreams: { type: 'array' }, + streamingTips: { type: 'array', items: { type: 'string' } }, + monetizationOptions: { type: 'array' } + } + } + }) + getLiveStreamingOpportunities() { + return this.socialCommerceService.getLiveStreamingOpportunities(); + } + + @Get('creator-economy/stats') + @ApiOperation({ summary: 'Get creator economy statistics' }) + getCreatorEconomyStats() { + return { + overview: { + totalCreators: 1247, + verifiedInfluencers: 389, + activeCampaigns: 67, + totalUGCContent: 15420, + monthlyTransactions: 2341, + }, + trendingContent: [ + { type: 'Beach Photography', growth: '+45%', avgPrice: 35 }, + { type: 'Food & Dining', growth: '+32%', avgPrice: 28 }, + { type: 'Adventure Videos', growth: '+28%', avgPrice: 85 }, + { type: 'Cultural Tours', growth: '+22%', avgPrice: 42 }, + ], + topEarningCategories: [ + { category: 'Travel Vlogs', avgEarning: 450 }, + { category: 'Food Reviews', avgEarning: 320 }, + { category: 'Adventure Content', avgEarning: 380 }, + { category: 'Cultural Content', avgEarning: 290 }, + ], + marketplaceTrends: { + videoContentDemand: '+67%', + authenticContentPremium: '+23%', + localCreatorPreference: '+89%', + exclusiveLicensingGrowth: '+156%', + }, + }; + } + + @Get('nft/collections') + @ApiOperation({ summary: 'Get NFT collections for tourism content' }) + @ApiQuery({ name: 'category', required: false, type: String }) + @ApiQuery({ name: 'verified', required: false, type: Boolean }) + getTourismNFTCollections( + @Query('category') category?: string, + @Query('verified') verified?: boolean, + ) { + return { + collections: [ + { + id: 'caribbean-moments', + name: 'Caribbean Moments', + description: 'Authentic moments captured across the Caribbean islands', + totalItems: 1247, + floorPrice: 0.05, // ETH + verified: true, + creator: 'CaribbeanDAO', + featuredItems: [ + { + tokenId: '1001', + name: 'Sunset at Saona Island', + price: 0.08, + rarity: 'Rare', + attributes: ['Beach', 'Sunset', 'Dominican Republic'], + }, + { + tokenId: '1002', + name: 'Colonial Architecture Santo Domingo', + price: 0.12, + rarity: 'Epic', + attributes: ['Architecture', 'History', 'UNESCO'], + }, + ], + }, + { + id: 'taste-of-caribbean', + name: 'Taste of Caribbean', + description: 'Culinary experiences tokenized as collectible NFTs', + totalItems: 567, + floorPrice: 0.03, + verified: true, + creator: 'Caribbean Chefs Collective', + featuredItems: [ + { + tokenId: '2001', + name: 'Traditional Mofongo Recipe', + price: 0.06, + rarity: 'Uncommon', + attributes: ['Food', 'Recipe', 'Puerto Rico'], + }, + ], + }, + ], + marketStats: { + totalVolume: 45.67, // ETH + totalSales: 3421, + averagePrice: 0.067, + uniqueHolders: 1247, + }, + utilities: [ + 'Access to exclusive Caribbean experiences', + 'Discounts on travel bookings through Karibeo', + 'VIP access to local events and festivals', + 'Royalties from commercial usage of content', + ], + }; + } + + @Post('micro-influencer/verification') + @ApiOperation({ summary: 'AI verification for micro-influencers' }) + @ApiResponse({ + status: 200, + description: 'Verification completed', + schema: { + type: 'object', + properties: { + verificationScore: { type: 'number' }, + status: { type: 'string' }, + recommendations: { type: 'array' }, + aiInsights: { type: 'object' } + } + } + }) + verifyMicroInfluencer(@Body() body: { + socialMediaHandles: Record; + contentSamples: string[]; + audienceData: any; + }) { + // Simulated AI verification process + const verificationScore = Math.random() * 40 + 60; // 60-100 + const status = verificationScore >= 75 ? 'verified' : 'pending_review'; + + return { + verificationScore, + status, + recommendations: [ + 'Increase posting consistency to improve engagement', + 'Focus on local Caribbean content for better authenticity', + 'Engage more with your audience in comments', + 'Consider collaborating with other local creators', + ], + aiInsights: { + authenticityScore: verificationScore * 0.9, + engagementQuality: 'High', + audienceAlignment: 'Caribbean Tourism Focus', + contentConsistency: 'Good', + growthPotential: 'High', + riskFactors: [], + }, + nextSteps: [ + 'Complete profile setup with portfolio samples', + 'Submit additional verification documents', + 'Join the Karibeo Creator Program', + 'Start applying to relevant campaigns', + ], + }; + } + + @Get('creator-program/benefits') + @ApiOperation({ summary: 'Get Karibeo Creator Program benefits' }) + getCreatorProgramBenefits() { + return { + membershipTiers: [ + { + tier: 'Bronze Creator', + requirements: { followers: 1000, engagement: 2, verification: 'basic' }, + benefits: [ + '5% commission on UGC sales', + 'Access to campaign marketplace', + 'Basic analytics dashboard', + 'Creator community access', + ], + monthlyEarningPotential: '$100-500', + }, + { + tier: 'Silver Creator', + requirements: { followers: 10000, engagement: 3, verification: 'verified' }, + benefits: [ + '8% commission on UGC sales', + 'Priority campaign matching', + 'Advanced analytics and insights', + 'Brand partnership opportunities', + 'Monthly creator workshops', + ], + monthlyEarningPotential: '$500-2000', + }, + { + tier: 'Gold Creator', + requirements: { followers: 50000, engagement: 4, verification: 'premium' }, + benefits: [ + '12% commission on UGC sales', + 'Exclusive campaign access', + 'Personal brand manager', + 'Revenue optimization consulting', + 'NFT minting opportunities', + 'Live streaming revenue share', + ], + monthlyEarningPotential: '$2000-8000', + }, + { + tier: 'Platinum Creator', + requirements: { followers: 100000, engagement: 5, verification: 'celebrity' }, + benefits: [ + '15% commission on UGC sales', + 'Custom campaign creation', + 'Dedicated support team', + 'Equity in select partnerships', + 'Ambassador program leadership', + 'Caribbean tourism board collaborations', + ], + monthlyEarningPotential: '$8000+', + }, + ], + additionalBenefits: { + educationalResources: [ + 'Content creation masterclasses', + 'Social media optimization workshops', + 'Brand partnership negotiation guides', + 'Tourism industry insights', + ], + technicalSupport: [ + 'AI-powered content optimization', + 'Automated campaign matching', + 'Performance analytics and reporting', + 'Blockchain-based content verification', + ], + networkingOpportunities: [ + 'Monthly creator meetups in Santo Domingo', + 'Annual Caribbean Creator Conference', + 'Brand partnership networking events', + 'Cross-platform collaboration opportunities', + ], + }, + applicationProcess: [ + 'Submit creator profile with portfolio', + 'Complete AI verification process', + 'Undergo manual review by Karibeo team', + 'Complete onboarding and training', + 'Start earning from day one', + ], + }; + } + + @Get('campaigns/trending') + @ApiOperation({ summary: 'Get trending campaign opportunities' }) + getTrendingCampaigns() { + return { + hotCampaigns: [ + { + id: 'campaign-beach-season', + title: 'Caribbean Beach Season 2025', + brand: 'Tourism Board DR', + budget: 15000, + deadline: '2025-02-15', + requirements: { + followers: 5000, + platforms: ['instagram', 'tiktok'], + location: 'Dominican Republic', + }, + applicants: 23, + description: 'Showcase the best beaches in Dominican Republic', + tags: ['beach', 'summer', 'paradise'], + }, + { + id: 'campaign-culinary-tour', + title: 'Authentic Caribbean Flavors', + brand: 'Caribbean Culinary Institute', + budget: 8000, + deadline: '2025-01-30', + requirements: { + followers: 3000, + platforms: ['instagram', 'youtube'], + specialty: 'food', + }, + applicants: 34, + description: 'Explore traditional Caribbean cuisine', + tags: ['food', 'culture', 'traditional'], + }, + { + id: 'campaign-adventure', + title: 'Extreme Caribbean Adventures', + brand: 'Adventure Tours Co.', + budget: 12000, + deadline: '2025-02-28', + requirements: { + followers: 8000, + platforms: ['youtube', 'tiktok'], + specialty: 'adventure', + }, + applicants: 18, + description: 'Document thrilling adventure activities', + tags: ['adventure', 'extreme', 'outdoors'], + }, + ], + campaignCategories: [ + { category: 'Tourism & Travel', activeCount: 12, avgBudget: 9500 }, + { category: 'Food & Restaurants', activeCount: 8, avgBudget: 6200 }, + { category: 'Adventure & Sports', activeCount: 6, avgBudget: 8900 }, + { category: 'Culture & Heritage', activeCount: 4, avgBudget: 5500 }, + { category: 'Luxury Experiences', activeCount: 3, avgBudget: 18000 }, + ], + applicationTips: [ + 'Showcase relevant past work in your application', + 'Demonstrate knowledge of the Caribbean market', + 'Provide realistic timeline and deliverable estimates', + 'Highlight your unique perspective or story angle', + 'Include audience demographics that match campaign goals', + ], + }; + } +} diff --git a/src/modules/social-commerce/social-commerce.module.ts b/src/modules/social-commerce/social-commerce.module.ts new file mode 100644 index 0000000..84dfead --- /dev/null +++ b/src/modules/social-commerce/social-commerce.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SocialCommerceService } from './social-commerce.service'; +import { SocialCommerceController } from './social-commerce.controller'; +import { InfluencerProfile } from '../../entities/influencer-profile.entity'; +import { CreatorCampaign } from '../../entities/creator-campaign.entity'; +import { UGCContent } from '../../entities/ugc-content.entity'; +import { User } from '../../entities/user.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + InfluencerProfile, + CreatorCampaign, + UGCContent, + User, + ]), + ], + controllers: [SocialCommerceController], + providers: [SocialCommerceService], + exports: [SocialCommerceService], +}) +export class SocialCommerceModule {} diff --git a/src/modules/social-commerce/social-commerce.service.ts b/src/modules/social-commerce/social-commerce.service.ts new file mode 100644 index 0000000..1c806ca --- /dev/null +++ b/src/modules/social-commerce/social-commerce.service.ts @@ -0,0 +1,871 @@ +import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { InfluencerProfile } from '../../entities/influencer-profile.entity'; +import { CreatorCampaign } from '../../entities/creator-campaign.entity'; +import { UGCContent } from '../../entities/ugc-content.entity'; +import { User } from '../../entities/user.entity'; +import { CreateInfluencerProfileDto } from './dto/create-influencer-profile.dto'; +import { CreateCampaignDto } from './dto/create-campaign.dto'; + +@Injectable() +export class SocialCommerceService { + constructor( + @InjectRepository(InfluencerProfile) + private readonly influencerRepository: Repository, + @InjectRepository(CreatorCampaign) + private readonly campaignRepository: Repository, + @InjectRepository(UGCContent) + private readonly ugcRepository: Repository, + @InjectRepository(User) + private readonly userRepository: Repository, + ) {} + + async createInfluencerProfile( + userId: string, + createDto: CreateInfluencerProfileDto, + ): Promise { + // Check if profile already exists + const existingProfile = await this.influencerRepository.findOne({ + where: { userId }, + }); + + if (existingProfile) { + throw new BadRequestException('Influencer profile already exists'); + } + + // Calculate AI verification score based on social stats + const aiVerificationScore = this.calculateAIVerificationScore(createDto.socialStats); + + // Determine verification status + const verificationStatus = aiVerificationScore >= 70 ? 'verified' : 'pending'; + + const profile = this.influencerRepository.create({ + userId, + tierLevel: createDto.tierLevel, + verificationStatus, + aiVerificationScore, + socialStats: createDto.socialStats, + specialties: createDto.specialties, + coverageAreas: createDto.coverageAreas, + contentLanguages: createDto.contentLanguages, + pricing: createDto.pricing, + performanceMetrics: { + averageEngagement: this.calculateAverageEngagement(createDto.socialStats), + completedCampaigns: 0, + clientSatisfactionRating: 0, + responseTime: 24, + contentQualityScore: 0, + professionalismScore: 0, + }, + availability: createDto.availability || { + timezone: 'America/Santo_Domingo', + workingDays: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], + preferredNoticeTime: 7, + maxCampaignsPerMonth: 5, + travelWillingness: true, + remoteWorkOnly: false, + }, + portfolio: createDto.portfolio || { + featuredContent: [], + testimonials: [], + }, + aiInsights: await this.generateAIInsights(createDto), + }); + + return this.influencerRepository.save(profile); + } + + async getInfluencerMarketplace( + specialty?: string, + location?: string, + minFollowers?: number, + maxBudget?: number, + tierLevel?: string, + ): Promise<{ + influencers: any[]; + filters: any; + totalCount: number; + }> { + let query = this.influencerRepository + .createQueryBuilder('influencer') + .leftJoinAndSelect('influencer.user', 'user') + .where('influencer.verificationStatus = :status', { status: 'verified' }); + + // Apply filters + if (specialty) { + query = query.andWhere(':specialty = ANY(influencer.specialties)', { specialty }); + } + + if (location) { + query = query.andWhere(':location = ANY(influencer.coverageAreas)', { location }); + } + + if (tierLevel) { + query = query.andWhere('influencer.tierLevel = :tierLevel', { tierLevel }); + } + + const totalCount = await query.getCount(); + + let influencers = await query + .orderBy('influencer.aiVerificationScore', 'DESC') + .addOrderBy('influencer.performanceMetrics->>\'clientSatisfactionRating\'', 'DESC') + .take(20) + .getMany(); + + // Filter by budget if specified + if (maxBudget) { + influencers = influencers.filter(inf => + inf.pricing.postRate <= maxBudget + ); + } + + // Filter by minimum followers if specified + if (minFollowers) { + influencers = influencers.filter(inf => { + const totalFollowers = Object.values(inf.socialStats).reduce( + (sum: number, stats: any) => sum + (stats?.followers || 0), 0 + ); + return totalFollowers >= minFollowers; + }); + } + + const enhancedInfluencers = influencers.map(inf => ({ + ...inf, + estimatedReach: this.calculateEstimatedReach(inf.socialStats), + recommendationScore: this.calculateRecommendationScore(inf), + isAvailable: this.checkAvailability(inf.availability), + sampleContent: this.getSampleContent(inf.portfolio), + })); + + const filters = { + specialties: await this.getAvailableSpecialties(), + locations: await this.getAvailableLocations(), + tierLevels: ['nano', 'micro', 'macro', 'mega'], + priceRanges: [ + { label: '$0-100', min: 0, max: 100 }, + { label: '$100-500', min: 100, max: 500 }, + { label: '$500-1000', min: 500, max: 1000 }, + { label: '$1000+', min: 1000, max: 999999 }, + ], + }; + + return { + influencers: enhancedInfluencers, + filters, + totalCount: influencers.length, + }; + } + + async createCampaign( + clientId: string, + createDto: CreateCampaignDto, + ): Promise { + const platformFee = createDto.totalBudget * 0.15; // 15% platform fee + const influencerFee = createDto.totalBudget - platformFee; + + const campaign = this.campaignRepository.create({ + title: createDto.title, + description: createDto.description, + clientId, + campaignType: createDto.campaignType, + status: 'open', + budget: { + totalBudget: createDto.totalBudget, + influencerFee, + platformFee, + additionalCosts: 0, + currency: 'USD', + }, + requirements: { + platforms: createDto.platforms, + contentTypes: createDto.contentTypes, + minimumFollowers: createDto.minimumFollowers, + minimumEngagement: createDto.minimumEngagement, + demographics: createDto.targetAudience || {}, + deliverables: createDto.deliverables || [], + }, + targetAudience: createDto.targetAudience || { + ageRange: { min: 18, max: 65 }, + gender: ['all'], + location: ['dominican-republic', 'puerto-rico'], + interests: [], + languages: ['en', 'es'], + }, + timeline: { + applicationDeadline: new Date(createDto.applicationDeadline), + campaignStart: new Date(createDto.campaignStart), + campaignEnd: new Date(createDto.campaignEnd), + contentDeadlines: createDto.deliverables?.map(d => ({ + deliverable: d.type, + deadline: new Date(d.deadline), + })) || [], + }, + contentGuidelines: createDto.contentGuidelines || { + brandGuidelines: '', + hashtags: [], + mentions: [], + doNotUse: [], + contentStyle: 'authentic', + brandValues: [], + }, + }); + + const savedCampaign = await this.campaignRepository.save(campaign); + + // Notify matching influencers + await this.notifyMatchingInfluencers(savedCampaign); + + return savedCampaign; + } + + async getMatchingInfluencers(campaignId: string): Promise<{ + exactMatches: any[]; + goodMatches: any[]; + potentialMatches: any[]; + matchingCriteria: any; + }> { + const campaign = await this.campaignRepository.findOne({ + where: { id: campaignId }, + }); + + if (!campaign) { + throw new NotFoundException('Campaign not found'); + } + + const allInfluencers = await this.influencerRepository + .createQueryBuilder('influencer') + .leftJoinAndSelect('influencer.user', 'user') + .where('influencer.verificationStatus = :status', { status: 'verified' }) + .getMany(); + + const exactMatches: any[] = []; + const goodMatches: any[] = []; + const potentialMatches: any[] = []; + + allInfluencers.forEach(influencer => { + const matchScore = this.calculateCampaignMatch(campaign, influencer); + const enhancedInfluencer = { + ...influencer, + matchScore, + matchReasons: this.getMatchReasons(campaign, influencer), + estimatedPerformance: this.estimatePerformance(campaign, influencer), + }; + + if (matchScore >= 90) { + exactMatches.push(enhancedInfluencer); + } else if (matchScore >= 70) { + goodMatches.push(enhancedInfluencer); + } else if (matchScore >= 50) { + potentialMatches.push(enhancedInfluencer); + } + }); + + return { + exactMatches: exactMatches.sort((a, b) => b.matchScore - a.matchScore), + goodMatches: goodMatches.sort((a, b) => b.matchScore - a.matchScore), + potentialMatches: potentialMatches.sort((a, b) => b.matchScore - a.matchScore), + matchingCriteria: { + platforms: campaign.requirements.platforms, + minimumFollowers: campaign.requirements.minimumFollowers, + minimumEngagement: campaign.requirements.minimumEngagement, + budget: campaign.budget.influencerFee, + timeline: campaign.timeline, + }, + }; + } + + async submitUGCContent( + creatorId: string, + contentData: { + title: string; + description: string; + contentType: string; + mediaUrls: string[]; + location: any; + tags: string[]; + isAvailableForLicensing: boolean; + }, + ): Promise { + // AI content analysis + const aiAnalysis = await this.performAIContentAnalysis(contentData); + + // Generate blockchain verification + const verification = { + isVerified: true, + verificationMethod: 'ai-analysis', + locationVerified: !!contentData.location, + timestampVerified: true, + metadataIntact: true, + blockchainHash: this.generateBlockchainHash(contentData), + }; + + const ugcContent = this.ugcRepository.create({ + creatorId, + title: contentData.title, + description: contentData.description, + contentType: contentData.contentType, + media: { + primaryUrl: contentData.mediaUrls[0], + thumbnailUrl: contentData.mediaUrls[0], // Would generate thumbnail + additionalUrls: contentData.mediaUrls.slice(1), + duration: 0, // Would be extracted from video files + format: 'image/jpeg', // Would be detected + resolution: '1920x1080', // Would be detected + }, + location: contentData.location, + tags: contentData.tags, + engagementMetrics: { + views: 0, + likes: 0, + comments: 0, + shares: 0, + saves: 0, + clickThroughs: 0, + engagementRate: 0, + }, + monetization: { + isTokenized: false, + nftId: '', + licenseType: contentData.isAvailableForLicensing ? 'paid' : 'free', + price: contentData.isAvailableForLicensing ? 25 : 0, + royaltyPercentage: 10, + licensePurchases: 0, + totalEarnings: 0, + }, + aiAnalysis, + usageRights: { + isAvailableForLicensing: contentData.isAvailableForLicensing, + exclusivityLevel: 'non-exclusive', + geographicRights: ['global'], + durationRights: 'perpetual', + usageTypes: ['commercial', 'editorial', 'social-media'], + restrictions: [], + }, + verification, + }); + + return this.ugcRepository.save(ugcContent); + } + + async getUGCMarketplace( + contentType?: string, + location?: string, + priceRange?: { min: number; max: number }, + tags?: string[], + ): Promise<{ + content: any[]; + totalCount: number; + categories: string[]; + topCreators: any[]; + }> { + let query = this.ugcRepository + .createQueryBuilder('ugc') + .leftJoinAndSelect('ugc.creator', 'creator') + .where('ugc.usageRights->>\'isAvailableForLicensing\' = :available', { available: 'true' }) + .andWhere('ugc.verification->>\'isVerified\' = :verified', { verified: 'true' }); + + if (contentType) { + query = query.andWhere('ugc.contentType = :contentType', { contentType }); + } + + if (location) { + query = query.andWhere('ugc.location->>\'city\' ILIKE :location', { + location: `%${location}%` + }); + } + + if (priceRange) { + query = query.andWhere('CAST(ugc.monetization->>\'price\' AS FLOAT) BETWEEN :min AND :max', { + min: priceRange.min, + max: priceRange.max, + }); + } + + if (tags && tags.length > 0) { + query = query.andWhere('ugc.tags && :tags', { tags }); + } + + const totalCount = await query.getCount(); + const content = await query + .orderBy('ugc.aiAnalysis->>\'contentQualityScore\'', 'DESC') + .addOrderBy('ugc.engagementMetrics->>\'engagementRate\'', 'DESC') + .take(24) + .getMany(); + + const enhancedContent = content.map(item => ({ + ...item, + previewUrl: this.generatePreviewUrl(item.media.primaryUrl), + licenseOptions: this.generateLicenseOptions(item), + creatorInfo: { + name: item.creator.firstName + ' ' + item.creator.lastName, + verified: item.creator.isVerified || false, + rating: this.getCreatorRating(item.creator.id), + }, + })); + + const categories = await this.getUGCCategories(); + const topCreators = await this.getTopUGCCreators(); + + return { + content: enhancedContent, + totalCount, + categories, + topCreators, + }; + } + + async purchaseUGCLicense( + buyerId: string, + contentId: string, + licenseType: string, + ): Promise<{ + license: any; + downloadUrl: string; + invoice: any; + }> { + const content = await this.ugcRepository.findOne({ + where: { id: contentId }, + relations: ['creator'], + }); + + if (!content) { + throw new NotFoundException('Content not found'); + } + + if (!content.usageRights.isAvailableForLicensing) { + throw new BadRequestException('Content not available for licensing'); + } + + const licensePrice = this.calculateLicensePrice(content, licenseType); + const platformFee = licensePrice * 0.20; // 20% platform fee + const creatorEarning = licensePrice - platformFee; + + // Generate license + const license = { + id: `LICENSE-${Date.now()}`, + contentId, + buyerId, + creatorId: content.creatorId, + licenseType, + price: licensePrice, + purchaseDate: new Date(), + usageRights: content.usageRights, + restrictions: content.usageRights.restrictions, + downloadCount: 0, + maxDownloads: licenseType === 'exclusive' ? 1 : 10, + }; + + // Update content monetization + content.monetization.licensePurchases += 1; + content.monetization.totalEarnings += creatorEarning; + await this.ugcRepository.save(content); + + // Generate secure download URL + const downloadUrl = this.generateSecureDownloadUrl(contentId, license.id); + + const invoice = { + invoiceId: `INV-${Date.now()}`, + licenseId: license.id, + totalAmount: licensePrice, + platformFee, + creatorEarning, + paymentStatus: 'completed', + }; + + return { + license, + downloadUrl, + invoice, + }; + } + + async getInfluencerAnalytics(influencerId: string): Promise<{ + performance: any; + earnings: any; + audience: any; + growth: any; + recommendations: string[]; + }> { + const influencer = await this.influencerRepository.findOne({ + where: { id: influencerId }, + }); + + if (!influencer) { + throw new NotFoundException('Influencer not found'); + } + + const campaigns = await this.campaignRepository.find({ + where: { influencerId }, + order: { createdAt: 'DESC' }, + take: 10, + }); + + const performance = { + totalCampaigns: campaigns.length, + completedCampaigns: campaigns.filter(c => c.status === 'completed').length, + averageEngagement: influencer.performanceMetrics.averageEngagement, + clientSatisfactionRating: influencer.performanceMetrics.clientSatisfactionRating, + responseTime: influencer.performanceMetrics.responseTime, + contentQualityScore: influencer.performanceMetrics.contentQualityScore, + }; + + const totalEarnings = campaigns.reduce((sum, c) => sum + (c.budget?.influencerFee || 0), 0); + + const earnings = { + totalEarnings, + monthlyAverage: campaigns.length > 0 ? totalEarnings / 12 : 0, + topPayingCampaign: Math.max(...campaigns.map(c => c.budget?.influencerFee || 0)), + pendingPayments: campaigns + .filter(c => c.status === 'completed') + .reduce((sum, c) => sum + (c.budget?.influencerFee || 0), 0), + }; + + const audience = influencer.aiInsights.audienceAnalysis; + + const growth = { + followerGrowth: this.calculateFollowerGrowth(influencer.socialStats), + engagementTrend: 'increasing', + reachExpansion: influencer.aiInsights.marketValue.estimatedReach, + marketValueTrend: influencer.aiInsights.marketValue.growthPotential, + }; + + const recommendations = [ + 'Focus on video content to increase engagement', + 'Expand coverage to Puerto Rico for more opportunities', + 'Consider creating UGC content for additional revenue', + 'Improve response time to under 12 hours for better client satisfaction', + ]; + + return { + performance, + earnings, + audience, + growth, + recommendations, + }; + } + + async getLiveStreamingOpportunities(): Promise<{ + upcomingStreams: any[]; + streamingTips: string[]; + monetizationOptions: any[]; + }> { + return { + upcomingStreams: [ + { + id: 'stream-1', + title: 'Virtual Tour: Colonial Zone Santo Domingo', + scheduledTime: new Date(Date.now() + 24 * 60 * 60 * 1000), + duration: 90, + expectedViewers: 500, + monetization: { + ticketPrice: 15, + tipsDonations: true, + sponsorshipSlots: 2, + }, + host: 'Local History Expert', + }, + { + id: 'stream-2', + title: 'Caribbean Cooking Masterclass', + scheduledTime: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), + duration: 120, + expectedViewers: 300, + monetization: { + ticketPrice: 25, + tipsDonations: true, + sponsorshipSlots: 1, + }, + host: 'Chef Maria Rodriguez', + }, + ], + streamingTips: [ + 'Test your internet connection 30 minutes before going live', + 'Prepare engaging talking points and interactive elements', + 'Promote your stream 48 hours in advance across all platforms', + 'Engage with viewers through Q&A and polls', + 'Have a clear call-to-action for monetization', + ], + monetizationOptions: [ + { + type: 'Ticket Sales', + description: 'Charge viewers for exclusive live stream access', + potentialRevenue: '$500-2000 per stream', + }, + { + type: 'Tips & Donations', + description: 'Receive real-time tips from engaged viewers', + potentialRevenue: '$100-500 per stream', + }, + { + type: 'Sponsored Segments', + description: 'Include brand partnerships during your stream', + potentialRevenue: '$200-1000 per sponsor', + }, + { + type: 'Product Placement', + description: 'Feature products naturally during your content', + potentialRevenue: '$100-300 per product', + }, + ], + }; + } + + // PRIVATE HELPER METHODS + + private calculateAIVerificationScore(socialStats: any): number { + let score = 0; + const platforms = Object.keys(socialStats); + + platforms.forEach(platform => { + const stats = socialStats[platform]; + if (stats) { + // Follower count scoring + if (stats.followers > 100000) score += 30; + else if (stats.followers > 10000) score += 20; + else if (stats.followers > 1000) score += 10; + + // Engagement rate scoring + if (stats.engagement > 5) score += 25; + else if (stats.engagement > 2) score += 15; + else if (stats.engagement > 1) score += 10; + + // Verification status + if (stats.verified) score += 20; + } + }); + + return Math.min(100, score); + } + + private calculateAverageEngagement(socialStats: any): number { + const platforms = Object.values(socialStats).filter(Boolean) as any[]; + if (platforms.length === 0) return 0; + + const totalEngagement = platforms.reduce((sum, stats) => sum + (stats.engagement || 0), 0); + return totalEngagement / platforms.length; + } + + private async generateAIInsights(createDto: CreateInfluencerProfileDto): Promise { + const totalFollowers = Object.values(createDto.socialStats).reduce( + (sum: number, stats: any) => sum + (stats?.followers || 0), 0 + ); + + return { + audienceAnalysis: { + demographics: { + '18-24': 25, + '25-34': 40, + '35-44': 20, + '45-54': 10, + '55+': 5, + }, + interests: createDto.specialties, + peakEngagementTimes: ['19:00-21:00', '12:00-14:00'], + }, + contentAnalysis: { + topPerformingContentTypes: ['video', 'carousel', 'story'], + averageEngagementByType: { + video: 8.5, + photo: 5.2, + carousel: 6.8, + story: 12.3, + }, + sentimentAnalysis: { positive: 75, neutral: 20, negative: 5 }, + }, + marketValue: { + estimatedReach: totalFollowers * 0.1, // 10% reach estimate + estimatedValue: totalFollowers * 0.05, // $0.05 per follower + growthPotential: totalFollowers > 50000 ? 'high' : 'medium', + }, + }; + } + + private calculateEstimatedReach(socialStats: any): number { + const platforms = Object.values(socialStats) as any[]; + return platforms.reduce((sum: number, stats: any) => sum + (stats?.followers || 0) * 0.1, 0); + } + + private calculateRecommendationScore(influencer: InfluencerProfile): number { + let score = 0; + + score += influencer.aiVerificationScore * 0.3; + score += influencer.performanceMetrics.clientSatisfactionRating * 20; + score += Math.min(influencer.performanceMetrics.averageEngagement * 10, 30); + score += Math.max(20 - influencer.performanceMetrics.responseTime, 0); + + return Math.min(100, score); + } + + private checkAvailability(availability: any): boolean { + const now = new Date(); + const currentDay = now.toLocaleDateString('en', { weekday: 'long' }).toLowerCase(); + return availability.workingDays.includes(currentDay); + } + + private getSampleContent(portfolio: any): any[] { + return portfolio.featuredContent.slice(0, 3); + } + + private async getAvailableSpecialties(): Promise { + return ['travel', 'food', 'adventure', 'luxury', 'budget', 'family', 'solo', 'culture', 'nature']; + } + + private async getAvailableLocations(): Promise { + return ['santo-domingo', 'punta-cana', 'puerto-plata', 'san-juan', 'bayamon', 'carolina']; + } + + private async notifyMatchingInfluencers(campaign: CreatorCampaign): Promise { + // This would send notifications to matching influencers + console.log(`Notifying influencers about campaign: ${campaign.title}`); + } + + private calculateCampaignMatch(campaign: CreatorCampaign, influencer: InfluencerProfile): number { + let score = 0; + + // Platform match + const platformMatch = campaign.requirements.platforms.some(p => + Object.keys(influencer.socialStats).includes(p) + ); + if (platformMatch) score += 30; + + // Specialty match + const specialtyMatch = campaign.requirements.demographics && + influencer.specialties.some(s => s.toLowerCase().includes('travel')); + if (specialtyMatch) score += 25; + + // Follower count match + const totalFollowers = Object.values(influencer.socialStats).reduce( + (sum: number, stats: any) => sum + (stats?.followers || 0), 0 + ); + if (totalFollowers >= campaign.requirements.minimumFollowers) score += 25; + + // Engagement rate match + if (influencer.performanceMetrics.averageEngagement >= campaign.requirements.minimumEngagement) { + score += 20; + } + + return score; + } + + private getMatchReasons(campaign: CreatorCampaign, influencer: InfluencerProfile): string[] { + const reasons: string[] = []; + + if (influencer.specialties.includes('travel')) { + reasons.push('Travel content specialist'); + } + + if (influencer.performanceMetrics.clientSatisfactionRating >= 4.5) { + reasons.push('High client satisfaction rating'); + } + + if (influencer.aiVerificationScore >= 80) { + reasons.push('Verified high-quality profile'); + } + + return reasons; + } + + private estimatePerformance(campaign: CreatorCampaign, influencer: InfluencerProfile): any { + const totalFollowers = Object.values(influencer.socialStats).reduce( + (sum: number, stats: any) => sum + (stats?.followers || 0), 0 + ); + + return { + estimatedReach: totalFollowers * 0.1, + estimatedEngagement: totalFollowers * (influencer.performanceMetrics.averageEngagement / 100), + estimatedClicks: totalFollowers * 0.01, + estimatedConversions: totalFollowers * 0.001, + }; + } + + private async performAIContentAnalysis(contentData: any): Promise { + // Simulated AI analysis + return { + contentQualityScore: Math.random() * 40 + 60, // 60-100 + visualAppealScore: Math.random() * 30 + 70, // 70-100 + authenticityScore: Math.random() * 20 + 80, // 80-100 + brandSafety: true, + sentimentScore: Math.random() * 2 - 1, // -1 to 1 + objectsDetected: ['building', 'person', 'sky', 'tree'], + colorsAnalysis: ['blue', 'green', 'white', 'brown'], + textAnalysis: { + language: 'en', + sentiment: 'positive', + topics: contentData.tags, + }, + }; + } + + private generateBlockchainHash(contentData: any): string { + // Simulated blockchain hash + return `0x${Math.random().toString(16).substr(2, 40)}`; + } + + private generatePreviewUrl(originalUrl: string): string { + return originalUrl + '?preview=true&watermark=karibeo'; + } + + private generateLicenseOptions(content: UGCContent): any[] { + return [ + { + type: 'standard', + price: content.monetization.price, + description: 'Standard commercial use license', + duration: '1 year', + }, + { + type: 'extended', + price: content.monetization.price * 2, + description: 'Extended commercial use with unlimited distribution', + duration: 'perpetual', + }, + { + type: 'exclusive', + price: content.monetization.price * 5, + description: 'Exclusive rights - content removed from marketplace', + duration: 'perpetual', + }, + ]; + } + + private getCreatorRating(creatorId: string): number { + // Would calculate based on actual reviews and sales + return 4.2 + Math.random() * 0.8; // 4.2-5.0 + } + + private async getUGCCategories(): Promise { + return ['travel', 'food', 'adventure', 'culture', 'nature', 'architecture', 'people', 'events']; + } + + private async getTopUGCCreators(): Promise { + return [ + { username: 'CaribbeanExplorer', totalSales: 156, rating: 4.9 }, + { username: 'TropicalVibes', totalSales: 143, rating: 4.8 }, + { username: 'IslandPhotographer', totalSales: 128, rating: 4.7 }, + ]; + } + + private calculateLicensePrice(content: UGCContent, licenseType: string): number { + const basePrice = content.monetization.price; + + switch (licenseType) { + case 'standard': return basePrice; + case 'extended': return basePrice * 2; + case 'exclusive': return basePrice * 5; + default: return basePrice; + } + } + + private generateSecureDownloadUrl(contentId: string, licenseId: string): string { + const token = Buffer.from(`${contentId}:${licenseId}:${Date.now()}`).toString('base64'); + return `https://api.karibeo.com/ugc/download/${contentId}?token=${token}`; + } + + private calculateFollowerGrowth(socialStats: any): string { + // Simulated growth calculation + const growthRate = Math.random() * 10 + 5; // 5-15% + return `+${growthRate.toFixed(1)}% this month`; + } +} diff --git a/src/modules/sustainability/dto/carbon-offset.dto.ts b/src/modules/sustainability/dto/carbon-offset.dto.ts new file mode 100644 index 0000000..1a49261 --- /dev/null +++ b/src/modules/sustainability/dto/carbon-offset.dto.ts @@ -0,0 +1,36 @@ +import { IsNumber, IsString, IsOptional, IsEnum } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum OffsetProjectType { + REFORESTATION = 'reforestation', + RENEWABLE_ENERGY = 'renewable-energy', + ENERGY_EFFICIENCY = 'energy-efficiency', + METHANE_CAPTURE = 'methane-capture', + CLEAN_COOKSTOVES = 'clean-cookstoves', + OCEAN_CONSERVATION = 'ocean-conservation' +} + +export class CarbonOffsetDto { + @ApiProperty({ description: 'Amount of CO2 to offset in kg', example: 25.5 }) + @IsNumber() + carbonKg: number; + + @ApiProperty({ description: 'Type of offset project', enum: OffsetProjectType }) + @IsEnum(OffsetProjectType) + projectType: OffsetProjectType; + + @ApiPropertyOptional({ description: 'Specific project ID or name' }) + @IsOptional() + @IsString() + projectId?: string; + + @ApiPropertyOptional({ description: 'Preferred location for offset project' }) + @IsOptional() + @IsString() + preferredLocation?: string; + + @ApiPropertyOptional({ description: 'Additional donation amount in USD' }) + @IsOptional() + @IsNumber() + additionalDonation?: number; +} diff --git a/src/modules/sustainability/dto/track-activity.dto.ts b/src/modules/sustainability/dto/track-activity.dto.ts new file mode 100644 index 0000000..3c320b0 --- /dev/null +++ b/src/modules/sustainability/dto/track-activity.dto.ts @@ -0,0 +1,71 @@ +import { IsString, IsNumber, IsOptional, IsEnum, IsBoolean } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum ActivityType { + TRANSPORTATION = 'transportation', + ACCOMMODATION = 'accommodation', + DINING = 'dining', + ACTIVITIES = 'activities', + SHOPPING = 'shopping' +} + +export enum TransportMode { + FLIGHT = 'flight', + CAR = 'car', + BUS = 'bus', + TRAIN = 'train', + TAXI = 'taxi', + MOTORCYCLE = 'motorcycle', + BICYCLE = 'bicycle', + WALKING = 'walking', + BOAT = 'boat' +} + +export class TrackActivityDto { + @ApiProperty({ description: 'Type of activity', enum: ActivityType }) + @IsEnum(ActivityType) + activityType: ActivityType; + + @ApiProperty({ description: 'Activity description', example: 'Flight from JFK to SDQ' }) + @IsString() + description: string; + + @ApiPropertyOptional({ description: 'Location of activity' }) + @IsOptional() + @IsString() + location?: string; + + @ApiPropertyOptional({ description: 'Duration in hours' }) + @IsOptional() + @IsNumber() + duration?: number; + + @ApiPropertyOptional({ description: 'Distance in kilometers' }) + @IsOptional() + @IsNumber() + distance?: number; + + @ApiPropertyOptional({ description: 'Number of participants' }) + @IsOptional() + @IsNumber() + participants?: number; + + @ApiPropertyOptional({ description: 'Service provider name' }) + @IsOptional() + @IsString() + provider?: string; + + @ApiPropertyOptional({ description: 'Transportation mode (if applicable)', enum: TransportMode }) + @IsOptional() + @IsEnum(TransportMode) + transportMode?: TransportMode; + + @ApiPropertyOptional({ description: 'Is this a eco-friendly option' }) + @IsOptional() + @IsBoolean() + isEcoFriendly?: boolean; + + @ApiPropertyOptional({ description: 'Additional context or notes' }) + @IsOptional() + metadata?: Record; +} diff --git a/src/modules/sustainability/sustainability.controller.ts b/src/modules/sustainability/sustainability.controller.ts new file mode 100644 index 0000000..35260d0 --- /dev/null +++ b/src/modules/sustainability/sustainability.controller.ts @@ -0,0 +1,395 @@ +import { + Controller, Get, Post, Body, Patch, Param, Query, UseGuards, Request +} from '@nestjs/common'; +import { + ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam +} from '@nestjs/swagger'; +import { SustainabilityService } from './sustainability.service'; +import { TrackActivityDto } from './dto/track-activity.dto'; +import { CarbonOffsetDto } from './dto/carbon-offset.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; + +@ApiTags('Sustainability') +@Controller('sustainability') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class SustainabilityController { + constructor(private readonly sustainabilityService: SustainabilityService) {} + + @Post('track-activity') + @ApiOperation({ summary: 'Track environmental impact of travel activity' }) + @ApiResponse({ + status: 201, + description: 'Activity tracked successfully', + schema: { + type: 'object', + properties: { + carbonFootprint: { type: 'number' }, + sustainabilityScore: { type: 'number' }, + ecoAlternatives: { type: 'array', items: { type: 'string' } }, + offsetSuggestion: { type: 'object' }, + environmentalImpact: { type: 'object' } + } + } + }) + trackActivity(@Body() trackDto: TrackActivityDto, @Request() req) { + return this.sustainabilityService.trackActivity(req.user.id, trackDto); + } + + @Post('purchase-offset') + @ApiOperation({ summary: 'Purchase carbon offsets' }) + @ApiResponse({ + status: 201, + description: 'Carbon offset purchased successfully', + schema: { + type: 'object', + properties: { + offsetCredits: { type: 'number' }, + cost: { type: 'number' }, + project: { type: 'object' }, + certificate: { type: 'object' } + } + } + }) + purchaseOffset(@Body() offsetDto: CarbonOffsetDto, @Request() req) { + return this.sustainabilityService.purchaseOffset(req.user.id, offsetDto); + } + + @Get('dashboard') + @ApiOperation({ summary: 'Get user sustainability dashboard' }) + @ApiResponse({ + status: 200, + description: 'Sustainability dashboard data', + schema: { + type: 'object', + properties: { + totalCarbonFootprint: { type: 'number' }, + totalOffsets: { type: 'number' }, + netCarbonFootprint: { type: 'number' }, + sustainabilityScore: { type: 'number' }, + streak: { type: 'number' }, + achievements: { type: 'array', items: { type: 'string' } }, + monthlyTrend: { type: 'array' }, + recommendations: { type: 'array', items: { type: 'string' } } + } + } + }) + getDashboard(@Request() req) { + return this.sustainabilityService.getUserSustainabilityDashboard(req.user.id); + } + + @Get('eco-establishments') + @ApiOperation({ summary: 'Get eco-friendly establishments' }) + @ApiQuery({ name: 'latitude', required: false, type: Number }) + @ApiQuery({ name: 'longitude', required: false, type: Number }) + @ApiQuery({ name: 'radius', required: false, type: Number }) + @ApiQuery({ name: 'minRating', required: false, type: Number }) + @ApiResponse({ + status: 200, + description: 'Eco-friendly establishments retrieved', + schema: { + type: 'object', + properties: { + establishments: { type: 'array' }, + certificationInfo: { type: 'object' }, + sustainabilityTips: { type: 'array', items: { type: 'string' } } + } + } + }) + getEcoEstablishments( + @Query('latitude') latitude?: number, + @Query('longitude') longitude?: number, + @Query('radius') radius?: number, + @Query('minRating') minRating?: number, + ) { + return this.sustainabilityService.getEcoFriendlyEstablishments( + latitude, longitude, radius, minRating + ); + } + + @Get('insights') + @ApiOperation({ summary: 'Get sustainability insights and trends' }) + @ApiResponse({ + status: 200, + description: 'Sustainability insights retrieved', + schema: { + type: 'object', + properties: { + carbonFootprintByActivity: { type: 'array' }, + ecoFriendlyTrends: { type: 'array' }, + offsetProjectsImpact: { type: 'array' }, + communityImpact: { type: 'object' }, + tips: { type: 'array', items: { type: 'string' } } + } + } + }) + getSustainabilityInsights() { + return this.sustainabilityService.getSustainabilityInsights(); + } + + @Get('certifications/guide') + @ApiOperation({ summary: 'Get green certification guide' }) + @ApiResponse({ + status: 200, + description: 'Green certification guide', + schema: { + type: 'object', + properties: { + certifications: { type: 'array' }, + howToIdentify: { type: 'array', items: { type: 'string' } }, + benefits: { type: 'array', items: { type: 'string' } }, + questions: { type: 'array', items: { type: 'string' } } + } + } + }) + getCertificationGuide() { + return this.sustainabilityService.getGreenCertificationGuide(); + } + + @Get('calculator/carbon-footprint') + @ApiOperation({ summary: 'Calculate carbon footprint for trip planning' }) + @ApiQuery({ name: 'origin', required: true, type: String }) + @ApiQuery({ name: 'destination', required: true, type: String }) + @ApiQuery({ name: 'transportMode', required: true, type: String }) + @ApiQuery({ name: 'duration', required: false, type: Number }) + @ApiQuery({ name: 'participants', required: false, type: Number }) + calculateCarbonFootprint( + @Query('origin') origin: string, + @Query('destination') destination: string, + @Query('transportMode') transportMode: string, + @Query('duration') duration?: number, + @Query('participants') participants?: number, + ) { + // Simplified carbon calculator for planning + const distance = this.estimateDistance(origin, destination); + const emissions = this.calculateEmissions(transportMode, distance, participants || 1); + + return { + origin, + destination, + transportMode, + estimatedDistance: distance, + carbonFootprintKg: emissions, + offsetCost: emissions * 0.02, + ecoAlternatives: this.getEcoTransportAlternatives(transportMode), + tips: [ + 'Choose direct routes when possible', + 'Consider offsetting your emissions', + 'Pack light to reduce fuel consumption', + 'Use public transportation at destination', + ], + }; + } + + @Get('leaderboard') + @ApiOperation({ summary: 'Get sustainability leaderboard' }) + @ApiQuery({ name: 'period', required: false, type: String }) + @ApiQuery({ name: 'category', required: false, type: String }) + getSustainabilityLeaderboard( + @Query('period') period: string = 'month', + @Query('category') category: string = 'overall', + ) { + // Mock leaderboard data + return { + period, + category, + userRank: 15, + topUsers: [ + { rank: 1, username: 'EcoExplorer', score: 95, offsetsKg: 250, streak: 45 }, + { rank: 2, username: 'GreenTraveler', score: 92, offsetsKg: 200, streak: 30 }, + { rank: 3, username: 'SustainableNomad', score: 89, offsetsKg: 180, streak: 28 }, + { rank: 4, username: 'ClimateChampion', score: 86, offsetsKg: 165, streak: 25 }, + { rank: 5, username: 'EcoWarrior', score: 84, offsetsKg: 150, streak: 22 }, + ], + categories: [ + { name: 'Most Offsets', description: 'Highest carbon offset purchases' }, + { name: 'Longest Streak', description: 'Consecutive eco-friendly choices' }, + { name: 'Best Score', description: 'Highest sustainability score' }, + { name: 'Most Activities', description: 'Most tracked eco activities' }, + ], + }; + } + + @Post('challenges/join') + @ApiOperation({ summary: 'Join sustainability challenge' }) + @ApiParam({ name: 'challengeId', type: 'string' }) + joinSustainabilityChallenge( + @Body() body: { challengeId: string }, + @Request() req, + ) { + return { + success: true, + challenge: { + id: body.challengeId, + name: 'Carbon Neutral Caribbean Trip', + description: 'Offset 100% of your travel emissions', + duration: '30 days', + reward: 'Eco Champion Badge + $25 offset credit', + participants: 156, + }, + userProgress: { + completed: false, + progressPercentage: 0, + currentOffsets: 0, + targetOffsets: 50, + }, + }; + } + + @Get('challenges') + @ApiOperation({ summary: 'Get available sustainability challenges' }) + getSustainabilityChallenges() { + return { + active: [ + { + id: 'carbon-neutral-trip', + name: 'Carbon Neutral Caribbean Trip', + description: 'Offset 100% of your travel emissions during your trip', + difficulty: 'Medium', + duration: '30 days', + reward: 'Eco Champion Badge + $25 offset credit', + participants: 156, + category: 'Offsetting', + }, + { + id: 'green-accommodation', + name: 'Green Accommodation Challenge', + description: 'Stay only in eco-certified accommodations', + difficulty: 'Easy', + duration: '14 days', + reward: 'Green Traveler Badge', + participants: 89, + category: 'Accommodation', + }, + { + id: 'plastic-free-trip', + name: 'Plastic-Free Adventure', + description: 'Complete a trip without single-use plastics', + difficulty: 'Hard', + duration: '7 days', + reward: 'Plastic-Free Hero Badge + Tree Planting Certificate', + participants: 34, + category: 'Waste Reduction', + }, + ], + completed: [ + { + id: 'local-transport', + name: 'Local Transport Hero', + completedDate: '2024-11-15', + reward: 'Public Transport Badge', + }, + ], + }; + } + + @Get('analytics/admin') + @UseGuards(RolesGuard) + @Roles('admin') + @ApiOperation({ summary: 'Get sustainability analytics (Admin only)' }) + getSustainabilityAnalytics() { + return { + overview: { + totalUsersTracking: 1245, + totalCarbonTracked: 15420, // kg CO2 + totalOffsetsUnits: 8950, // kg CO2 offset + netEmissionsReduced: 6470, // kg CO2 net reduction + }, + trends: { + monthlyGrowth: 23.5, // % growth in sustainability tracking + offsetPurchaseRate: 12.8, // % of users purchasing offsets + ecoChoiceRate: 34.2, // % of activities that are eco-friendly + }, + topActivities: [ + { activity: 'Transportation', totalEmissions: 9870, avgPerUser: 7.9 }, + { activity: 'Accommodation', totalEmissions: 3210, avgPerUser: 2.6 }, + { activity: 'Dining', totalEmissions: 1560, avgPerUser: 1.3 }, + { activity: 'Activities', totalEmissions: 780, avgPerUser: 0.6 }, + ], + offsetProjects: [ + { project: 'Reforestation', percentage: 45, totalOffsetted: 4028 }, + { project: 'Renewable Energy', percentage: 30, totalOffsetted: 2685 }, + { project: 'Ocean Conservation', percentage: 25, totalOffsetted: 2237 }, + ], + userEngagement: { + averageActivitiesPerUser: 8.5, + repeatOffsetUsers: 34, + challengeParticipation: 28, + sustainabilityScoreAverage: 67.3, + }, + }; + } + + @Post('report/impact') + @ApiOperation({ summary: 'Generate sustainability impact report' }) + generateImpactReport(@Request() req) { + return { + reportId: `IMPACT-${Date.now()}`, + generatedFor: req.user.id, + period: 'Last 12 months', + summary: { + totalActivitiesTracked: 45, + carbonFootprintKg: 234.5, + offsetsPurchasedKg: 180.0, + netEmissionsKg: 54.5, + sustainabilityScore: 78, + }, + achievements: [ + '🌱 Carbon Conscious - Tracked 40+ activities', + '♻️ Offset Champion - Purchased 180kg CO2 offsets', + '🌿 Eco Traveler - 70% eco-friendly choices', + ], + projectsSupported: [ + 'Dominican Republic Reforestation - 120kg CO2', + 'Puerto Rico Solar Energy - 60kg CO2', + ], + recommendations: [ + 'Consider sustainable transportation for future trips', + 'Look for more eco-certified accommodations', + 'Join the Carbon Neutral Challenge', + ], + downloadUrl: 'https://api.karibeo.com/sustainability/reports/impact-report.pdf', + }; + } + + // PRIVATE HELPER METHODS + private estimateDistance(origin: string, destination: string): number { + // Simplified distance estimation + const distances = { + 'JFK-SDQ': 2350, // JFK to Santo Domingo + 'MIA-SDQ': 1560, // Miami to Santo Domingo + 'SJU-SDQ': 380, // San Juan to Santo Domingo + 'SDQ-STI': 190, // Santo Domingo to Santiago + }; + + const key = `${origin}-${destination}`; + return distances[key] || distances[`${destination}-${origin}`] || 1000; + } + + private calculateEmissions(transportMode: string, distance: number, participants: number): number { + const emissionFactors = { + 'flight': 0.255, + 'car': 0.171, + 'bus': 0.089, + 'train': 0.041, + }; + + const factor = emissionFactors[transportMode] || 0.171; + return Math.round((distance * factor / participants) * 100) / 100; + } + + private getEcoTransportAlternatives(currentMode: string): string[] { + const alternatives: string[] = []; + + if (currentMode !== 'train') alternatives.push('Train - 75% less emissions'); + if (currentMode !== 'bus') alternatives.push('Bus - 60% less emissions'); + if (currentMode === 'flight') { + alternatives.push('Direct flights reduce emissions by 20%'); + alternatives.push('Consider offsetting flight emissions'); + } + + return alternatives; + } +} diff --git a/src/modules/sustainability/sustainability.module.ts b/src/modules/sustainability/sustainability.module.ts new file mode 100644 index 0000000..c94c6a6 --- /dev/null +++ b/src/modules/sustainability/sustainability.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SustainabilityService } from './sustainability.service'; +import { SustainabilityController } from './sustainability.controller'; +import { SustainabilityTracking } from '../../entities/sustainability-tracking.entity'; +import { EcoEstablishment } from '../../entities/eco-establishment.entity'; +import { Establishment } from '../../entities/establishment.entity'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + SustainabilityTracking, + EcoEstablishment, + Establishment, + PlaceOfInterest, + ]), + ], + controllers: [SustainabilityController], + providers: [SustainabilityService], + exports: [SustainabilityService], +}) +export class SustainabilityModule {} diff --git a/src/modules/sustainability/sustainability.service.ts b/src/modules/sustainability/sustainability.service.ts new file mode 100644 index 0000000..09b4556 --- /dev/null +++ b/src/modules/sustainability/sustainability.service.ts @@ -0,0 +1,732 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { SustainabilityTracking } from '../../entities/sustainability-tracking.entity'; +import { EcoEstablishment } from '../../entities/eco-establishment.entity'; +import { Establishment } from '../../entities/establishment.entity'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { TrackActivityDto, ActivityType, TransportMode } from './dto/track-activity.dto'; +import { CarbonOffsetDto, OffsetProjectType } from './dto/carbon-offset.dto'; + +@Injectable() +export class SustainabilityService { + constructor( + @InjectRepository(SustainabilityTracking) + private readonly trackingRepository: Repository, + @InjectRepository(EcoEstablishment) + private readonly ecoEstablishmentRepository: Repository, + @InjectRepository(Establishment) + private readonly establishmentRepository: Repository, + @InjectRepository(PlaceOfInterest) + private readonly placeRepository: Repository, + ) {} + + async trackActivity( + userId: string, + trackDto: TrackActivityDto, + ): Promise<{ + carbonFootprint: number; + sustainabilityScore: number; + ecoAlternatives: string[]; + offsetSuggestion: any; + environmentalImpact: any; + }> { + // Calculate carbon footprint based on activity + const carbonFootprint = this.calculateCarbonFootprint(trackDto); + + // Calculate sustainability score + const sustainabilityScore = this.calculateSustainabilityScore(trackDto, carbonFootprint); + + // Generate eco-friendly alternatives + const ecoAlternatives = this.generateEcoAlternatives(trackDto); + + // Calculate other environmental impacts + const environmentalImpact = this.calculateEnvironmentalImpact(trackDto); + + // Save tracking record + const tracking = this.trackingRepository.create({ + userId, + activityType: trackDto.activityType, + activityDetails: { + description: trackDto.description, + location: trackDto.location, + duration: trackDto.duration, + distance: trackDto.distance, + participants: trackDto.participants || 1, + provider: trackDto.provider, + }, + carbonFootprintKg: carbonFootprint, + waterUsageLiters: environmentalImpact.waterUsage, + wasteGeneratedKg: environmentalImpact.wasteGenerated, + energyConsumptionKwh: environmentalImpact.energyConsumption, + sustainabilityScore, + certifications: { + ecoFriendly: trackDto.isEcoFriendly || false, + carbonNeutral: false, + sustainableTourism: false, + localCommunitySupport: false, + wildlifeProtection: false, + certificationBodies: [], + }, + }); + + await this.trackingRepository.save(tracking); + + // Generate offset suggestion + const offsetSuggestion = this.generateOffsetSuggestion(carbonFootprint); + + return { + carbonFootprint, + sustainabilityScore, + ecoAlternatives, + offsetSuggestion, + environmentalImpact, + }; + } + + async purchaseOffset( + userId: string, + offsetDto: CarbonOffsetDto, + ): Promise<{ + offsetCredits: number; + cost: number; + project: any; + certificate: any; + }> { + // Calculate offset cost ($15-25 per ton CO2) + const costPerKg = 0.02; // $20 per ton = $0.02 per kg + const baseCost = offsetDto.carbonKg * costPerKg; + const totalCost = baseCost + (offsetDto.additionalDonation || 0); + + // Select offset project + const project = this.selectOffsetProject(offsetDto.projectType, offsetDto.preferredLocation); + + // Generate certificate + const certificate = { + certificateId: `KARIBEO-${Date.now()}-${userId.substr(0, 8)}`, + userId, + carbonOffsetKg: offsetDto.carbonKg, + projectType: offsetDto.projectType, + projectName: project.name, + issueDate: new Date(), + verificationStandard: 'Verified Carbon Standard (VCS)', + retirementDate: new Date(), + }; + + // Update user's total offsets (would be stored in user profile) + await this.updateUserOffsetCredits(userId, offsetDto.carbonKg, totalCost); + + return { + offsetCredits: offsetDto.carbonKg, + cost: totalCost, + project, + certificate, + }; + } + + async getUserSustainabilityDashboard(userId: string): Promise<{ + totalCarbonFootprint: number; + totalOffsets: number; + netCarbonFootprint: number; + sustainabilityScore: number; + streak: number; + achievements: string[]; + monthlyTrend: any[]; + recommendations: string[]; + }> { + // Get user's sustainability tracking data + const trackingData = await this.trackingRepository.find({ + where: { userId }, + order: { createdAt: 'DESC' }, + take: 100, + }); + + const totalCarbonFootprint = trackingData.reduce( + (sum, record) => sum + Number(record.carbonFootprintKg), 0 + ); + + const totalOffsets = trackingData.reduce( + (sum, record) => sum + Number(record.offsetCreditsKg), 0 + ); + + const netCarbonFootprint = Math.max(0, totalCarbonFootprint - totalOffsets); + + const averageSustainabilityScore = trackingData.length > 0 + ? trackingData.reduce((sum, record) => sum + Number(record.sustainabilityScore), 0) / trackingData.length + : 0; + + // Calculate sustainability streak (consecutive days with eco-friendly choices) + const streak = this.calculateSustainabilityStreak(trackingData); + + // Generate achievements + const achievements = this.generateAchievements(trackingData); + + // Generate monthly trend data + const monthlyTrend = this.generateMonthlyTrend(trackingData); + + // Generate personalized recommendations + const recommendations = this.generateSustainabilityRecommendations(trackingData); + + return { + totalCarbonFootprint, + totalOffsets, + netCarbonFootprint, + sustainabilityScore: averageSustainabilityScore, + streak, + achievements, + monthlyTrend, + recommendations, + }; + } + + async getEcoFriendlyEstablishments( + latitude?: number, + longitude?: number, + radius?: number, + minRating?: number, + ): Promise<{ + establishments: any[]; + certificationInfo: any; + sustainabilityTips: string[]; + }> { + let query = this.ecoEstablishmentRepository + .createQueryBuilder('eco') + .leftJoinAndSelect('eco.establishment', 'establishment') + .where('eco.sustainabilityRating >= :minRating', { + minRating: minRating || 70 + }); + + const ecoEstablishments = await query + .orderBy('eco.sustainabilityRating', 'DESC') + .take(20) + .getMany(); + + const establishments = ecoEstablishments.map(eco => ({ + ...eco.establishment, + sustainabilityRating: eco.sustainabilityRating, + greenCertifications: eco.greenCertifications, + practicesDescription: eco.practicesDescription, + energyMeasures: eco.energyMeasures, + carbonFootprint: eco.carbonFootprint, + ecoHighlights: this.generateEcoHighlights(eco), + })); + + const certificationInfo = { + 'LEED': 'Leadership in Energy and Environmental Design - Green building certification', + 'Green Key': 'International eco-label for tourism accommodation and attractions', + 'ISO 14001': 'Environmental management systems standard', + 'Carbon Neutral': 'Net-zero carbon emissions through reduction and offsetting', + 'Rainforest Alliance': 'Sustainable tourism certification focusing on conservation', + }; + + const sustainabilityTips = [ + 'Look for establishments with solar panels and renewable energy', + 'Choose hotels with water conservation programs', + 'Support businesses that source locally and hire from the community', + 'Prefer accommodations with waste reduction and recycling programs', + 'Select certified eco-friendly tour operators', + ]; + + return { + establishments, + certificationInfo, + sustainabilityTips, + }; + } + + async getSustainabilityInsights(): Promise<{ + carbonFootprintByActivity: any[]; + ecoFriendlyTrends: any[]; + offsetProjectsImpact: any[]; + communityImpact: any; + tips: string[]; + }> { + const carbonFootprintByActivity = [ + { activity: 'Transportation', percentage: 65, avgKgCO2: 45.2 }, + { activity: 'Accommodation', percentage: 20, avgKgCO2: 12.8 }, + { activity: 'Dining', percentage: 10, avgKgCO2: 5.4 }, + { activity: 'Activities', percentage: 5, avgKgCO2: 2.1 }, + ]; + + const ecoFriendlyTrends = [ + { month: 'Jan', ecoChoices: 15, totalChoices: 45 }, + { month: 'Feb', ecoChoices: 22, totalChoices: 48 }, + { month: 'Mar', ecoChoices: 28, totalChoices: 52 }, + { month: 'Apr', ecoChoices: 35, totalChoices: 55 }, + { month: 'May', ecoChoices: 42, totalChoices: 58 }, + { month: 'Jun', ecoChoices: 48, totalChoices: 60 }, + ]; + + const offsetProjectsImpact = [ + { + project: 'Dominican Republic Reforestation', + co2Absorbed: 1250, + treesPlanted: 5000, + location: 'Sierra de Bahoruco', + }, + { + project: 'Puerto Rico Solar Energy', + co2Prevented: 890, + energyGenerated: 2500, + location: 'Vieques Island', + }, + { + project: 'Caribbean Ocean Conservation', + co2Sequestered: 445, + coralRestored: 500, + location: 'Saona Island', + }, + ]; + + const communityImpact = { + jobsCreated: 156, + familiesSupported: 89, + localBusinessesPartnered: 45, + conservationProjectsFunded: 12, + }; + + const tips = [ + 'Choose direct flights to reduce emissions by up to 20%', + 'Stay in eco-certified accommodations', + 'Use public transportation or bike when exploring', + 'Support local restaurants that source ingredients locally', + 'Participate in beach cleanups and conservation activities', + 'Offset your travel emissions through verified carbon projects', + 'Bring reusable water bottles and bags', + 'Choose reef-safe sunscreen to protect marine life', + ]; + + return { + carbonFootprintByActivity, + ecoFriendlyTrends, + offsetProjectsImpact, + communityImpact, + tips, + }; + } + + async getGreenCertificationGuide(): Promise<{ + certifications: any[]; + howToIdentify: string[]; + benefits: string[]; + questions: string[]; + }> { + const certifications = [ + { + name: 'LEED (Leadership in Energy and Environmental Design)', + description: 'World\'s most widely used green building rating system', + levels: ['Certified', 'Silver', 'Gold', 'Platinum'], + focus: 'Energy efficiency, water conservation, materials selection', + logo: '🏢', + }, + { + name: 'Green Key', + description: 'International eco-label for tourism industry', + levels: ['Bronze', 'Silver', 'Gold'], + focus: 'Environmental management, sustainable operations', + logo: '🔑', + }, + { + name: 'Rainforest Alliance', + description: 'Conservation and sustainable development certification', + levels: ['Certified'], + focus: 'Biodiversity protection, community benefits, sustainable practices', + logo: '🐸', + }, + { + name: 'ISO 14001', + description: 'Environmental management systems standard', + levels: ['Certified'], + focus: 'Environmental impact reduction, compliance, improvement', + logo: '📋', + }, + { + name: 'Carbon Neutral Certification', + description: 'Net-zero carbon emissions verification', + levels: ['Certified'], + focus: 'Carbon footprint measurement, reduction, offsetting', + logo: '🌱', + }, + ]; + + const howToIdentify = [ + 'Look for certification logos and certificates displayed prominently', + 'Check the establishment\'s website for sustainability commitments', + 'Ask staff about their environmental practices and certifications', + 'Look for visible eco-friendly practices (solar panels, recycling bins, etc.)', + 'Read recent reviews mentioning sustainability efforts', + ]; + + const benefits = [ + 'Reduced environmental impact during your stay', + 'Support for local communities and conservation efforts', + 'Often better air and water quality', + 'Educational opportunities about sustainability', + 'Contributing to global climate action', + 'Often unique and authentic local experiences', + ]; + + const questions = [ + 'What environmental certifications do you have?', + 'How do you reduce energy and water consumption?', + 'Do you source food and materials locally?', + 'What waste reduction programs do you have?', + 'How do you support the local community?', + 'Do you offer carbon offset programs for guests?', + ]; + + return { + certifications, + howToIdentify, + benefits, + questions, + }; + } + + // PRIVATE HELPER METHODS + + private calculateCarbonFootprint(trackDto: TrackActivityDto): number { + let carbonKg = 0; + + switch (trackDto.activityType) { + case ActivityType.TRANSPORTATION: + carbonKg = this.calculateTransportationFootprint(trackDto); + break; + case ActivityType.ACCOMMODATION: + carbonKg = this.calculateAccommodationFootprint(trackDto); + break; + case ActivityType.DINING: + carbonKg = this.calculateDiningFootprint(trackDto); + break; + case ActivityType.ACTIVITIES: + carbonKg = this.calculateActivitiesFootprint(trackDto); + break; + default: + carbonKg = 2.0; // Default estimate + } + + // Reduce footprint if eco-friendly option + if (trackDto.isEcoFriendly) { + carbonKg *= 0.3; // 70% reduction for eco-friendly choices + } + + return Math.round(carbonKg * 100) / 100; // Round to 2 decimal places + } + + private calculateTransportationFootprint(trackDto: TrackActivityDto): number { + const distance = trackDto.distance || 0; + const participants = trackDto.participants || 1; + + // Carbon emission factors (kg CO2 per km per person) + const emissionFactors = { + [TransportMode.FLIGHT]: 0.255, // Commercial flight + [TransportMode.CAR]: 0.171, // Average car + [TransportMode.BUS]: 0.089, // Public bus + [TransportMode.TRAIN]: 0.041, // Electric train + [TransportMode.TAXI]: 0.171, // Similar to car + [TransportMode.MOTORCYCLE]: 0.113, + [TransportMode.BICYCLE]: 0, // Zero emissions + [TransportMode.WALKING]: 0, // Zero emissions + [TransportMode.BOAT]: 0.240, // Ferry/boat + }; + + const factor = emissionFactors[trackDto.transportMode as TransportMode] || 0.171; + return (distance * factor) / participants; + } + + private calculateAccommodationFootprint(trackDto: TrackActivityDto): number { + const nights = trackDto.duration || 1; + const participants = trackDto.participants || 1; + + // Average hotel CO2 emissions: 30kg per room per night + const roomEmissions = 30; + return (roomEmissions * nights) / participants; + } + + private calculateDiningFootprint(trackDto: TrackActivityDto): number { + const meals = trackDto.duration || 1; // Duration as number of meals + const participants = trackDto.participants || 1; + + // Average meal CO2 emissions: 2.5kg per meal + const mealEmissions = 2.5; + return (mealEmissions * meals) / participants; + } + + private calculateActivitiesFootprint(trackDto: TrackActivityDto): number { + const hours = trackDto.duration || 2; + const participants = trackDto.participants || 1; + + // Average activity CO2 emissions: 1kg per hour per person + const activityEmissions = 1.0; + return (activityEmissions * hours) / participants; + } + + private calculateSustainabilityScore(trackDto: TrackActivityDto, carbonFootprint: number): number { + let score = 50; // Base score + + // Adjust based on carbon footprint (lower is better) + if (carbonFootprint < 5) score += 30; + else if (carbonFootprint < 15) score += 20; + else if (carbonFootprint < 30) score += 10; + else if (carbonFootprint > 50) score -= 20; + + // Bonus for eco-friendly choices + if (trackDto.isEcoFriendly) score += 25; + + // Bonus for sustainable transport modes + if (trackDto.transportMode === TransportMode.BICYCLE || + trackDto.transportMode === TransportMode.WALKING) { + score += 20; + } else if (trackDto.transportMode === TransportMode.TRAIN || + trackDto.transportMode === TransportMode.BUS) { + score += 15; + } + + return Math.min(100, Math.max(0, score)); + } + + private generateEcoAlternatives(trackDto: TrackActivityDto): string[] { + const alternatives: string[] = []; + + switch (trackDto.activityType) { + case ActivityType.TRANSPORTATION: + if (trackDto.transportMode !== TransportMode.TRAIN) { + alternatives.push('Consider taking a train instead for 75% less emissions'); + } + if (trackDto.transportMode !== TransportMode.BUS) { + alternatives.push('Public buses reduce carbon footprint by 60%'); + } + if (trackDto.transportMode === TransportMode.FLIGHT) { + alternatives.push('Choose direct flights to reduce emissions by 20%'); + alternatives.push('Consider carbon offsetting for your flight'); + } + break; + + case ActivityType.ACCOMMODATION: + alternatives.push('Look for LEED or Green Key certified hotels'); + alternatives.push('Choose accommodations with solar energy'); + alternatives.push('Stay in eco-lodges or sustainable resorts'); + break; + + case ActivityType.DINING: + alternatives.push('Choose restaurants that source ingredients locally'); + alternatives.push('Try plant-based meals to reduce food carbon footprint'); + alternatives.push('Support farm-to-table restaurants'); + break; + + case ActivityType.ACTIVITIES: + alternatives.push('Join eco-tours that support conservation'); + alternatives.push('Choose activities that don\'t require motorized transport'); + alternatives.push('Participate in beach or trail cleanups'); + break; + } + + return alternatives; + } + + private calculateEnvironmentalImpact(trackDto: TrackActivityDto): any { + const baseWaterUsage = trackDto.duration || 1; + const baseWasteGeneration = (trackDto.participants || 1) * 0.5; + const baseEnergyConsumption = trackDto.duration || 1; + + return { + waterUsage: baseWaterUsage * 50, // liters + wasteGenerated: baseWasteGeneration, // kg + energyConsumption: baseEnergyConsumption * 5, // kWh + }; + } + + private generateOffsetSuggestion(carbonFootprint: number): any { + const costEstimate = carbonFootprint * 0.02; // $0.02 per kg CO2 + + return { + carbonToOffset: carbonFootprint, + estimatedCost: costEstimate, + recommendedProject: OffsetProjectType.REFORESTATION, + benefits: [ + `Plant approximately ${Math.ceil(carbonFootprint / 0.5)} trees`, + 'Support local Dominican/Puerto Rican communities', + 'Protect Caribbean biodiversity', + 'Contribute to climate action goals', + ], + }; + } + + private selectOffsetProject(projectType: OffsetProjectType, preferredLocation?: string): any { + const projects = { + [OffsetProjectType.REFORESTATION]: { + name: 'Caribbean Reforestation Initiative', + location: preferredLocation || 'Dominican Republic', + description: 'Restoring native forests and protecting biodiversity', + impact: 'Each ton of CO2 offset plants approximately 40 trees', + }, + [OffsetProjectType.RENEWABLE_ENERGY]: { + name: 'Puerto Rico Solar Energy Project', + location: 'Puerto Rico', + description: 'Community solar installations in rural areas', + impact: 'Provides clean energy to 500+ families', + }, + [OffsetProjectType.OCEAN_CONSERVATION]: { + name: 'Caribbean Coral Reef Protection', + location: 'Saona Island, DR', + description: 'Marine protected area and coral restoration', + impact: 'Protects 1000+ hectares of marine ecosystem', + }, + }; + + return projects[projectType] || projects[OffsetProjectType.REFORESTATION]; + } + + private async updateUserOffsetCredits(userId: string, carbonKg: number, cost: number): Promise { + // Create offset tracking record + const offsetRecord = this.trackingRepository.create({ + userId, + activityType: 'offset', + activityDetails: { + description: 'Carbon offset purchase', + participants: 1, + }, + carbonFootprintKg: -carbonKg, // Negative to represent offset + sustainabilityScore: 100, + offsetCreditsKg: carbonKg, + offsetCostUsd: cost, + certifications: { + ecoFriendly: true, + carbonNeutral: true, + sustainableTourism: true, + localCommunitySupport: true, + wildlifeProtection: true, + certificationBodies: ['Verified Carbon Standard'], + }, + }); + + await this.trackingRepository.save(offsetRecord); + } + + private calculateSustainabilityStreak(trackingData: SustainabilityTracking[]): number { + let streak = 0; + const today = new Date(); + + // Group by date and check for eco-friendly choices + for (let i = 0; i < 30; i++) { // Check last 30 days + const checkDate = new Date(today); + checkDate.setDate(today.getDate() - i); + + const dayActivities = trackingData.filter(record => { + const recordDate = new Date(record.createdAt); + return recordDate.toDateString() === checkDate.toDateString(); + }); + + const hasEcoChoice = dayActivities.some(activity => + activity.sustainabilityScore >= 70 || activity.certifications?.ecoFriendly + ); + + if (hasEcoChoice) { + streak++; + } else if (dayActivities.length > 0) { + break; // Break streak if activities exist but none are eco-friendly + } + } + + return streak; + } + + private generateAchievements(trackingData: SustainabilityTracking[]): string[] { + const achievements: string[] = []; + const totalFootprint = trackingData.reduce((sum, record) => sum + Number(record.carbonFootprintKg), 0); + const totalOffsets = trackingData.reduce((sum, record) => sum + Number(record.offsetCreditsKg), 0); + const ecoActivities = trackingData.filter(record => record.certifications?.ecoFriendly).length; + + if (totalOffsets > 100) achievements.push('🌍 Climate Champion - Offset 100+ kg CO2'); + if (totalOffsets > totalFootprint) achievements.push('🌱 Carbon Negative - Offset more than you emit'); + if (ecoActivities >= 10) achievements.push('♻️ Eco Warrior - 10+ eco-friendly choices'); + if (ecoActivities >= 5) achievements.push('🌿 Green Traveler - 5+ eco-friendly choices'); + + const streak = this.calculateSustainabilityStreak(trackingData); + if (streak >= 7) achievements.push('🔥 Week-long Eco Streak'); + if (streak >= 30) achievements.push('⭐ Sustainability Master - 30 day streak'); + + return achievements; + } + + private generateMonthlyTrend(trackingData: SustainabilityTracking[]): any[] { + const monthlyData: any[] = []; + const now = new Date(); + + for (let i = 5; i >= 0; i--) { + const month = new Date(now.getFullYear(), now.getMonth() - i, 1); + const monthName = month.toLocaleDateString('en', { month: 'short' }); + + const monthActivities = trackingData.filter(record => { + const recordDate = new Date(record.createdAt); + return recordDate.getMonth() === month.getMonth() && + recordDate.getFullYear() === month.getFullYear(); + }); + + const carbonFootprint = monthActivities.reduce((sum, record) => sum + Number(record.carbonFootprintKg), 0); + const offsets = monthActivities.reduce((sum, record) => sum + Number(record.offsetCreditsKg), 0); + const sustainabilityScore = monthActivities.length > 0 + ? monthActivities.reduce((sum, record) => sum + Number(record.sustainabilityScore), 0) / monthActivities.length + : 0; + + monthlyData.push({ + month: monthName, + carbonFootprint: Math.round(carbonFootprint * 100) / 100, + offsets: Math.round(offsets * 100) / 100, + sustainabilityScore: Math.round(sustainabilityScore), + netEmissions: Math.max(0, carbonFootprint - offsets), + }); + } + + return monthlyData; + } + + private generateSustainabilityRecommendations(trackingData: SustainabilityTracking[]): string[] { + const recommendations: string[] = []; + const recentActivities = trackingData.slice(0, 10); + + const avgScore = recentActivities.length > 0 + ? recentActivities.reduce((sum, record) => sum + Number(record.sustainabilityScore), 0) / recentActivities.length + : 0; + + if (avgScore < 50) { + recommendations.push('Consider eco-friendly transportation options like trains or buses'); + recommendations.push('Look for green-certified accommodations on your next trip'); + } + + const transportActivities = recentActivities.filter(a => a.activityType === 'transportation'); + if (transportActivities.length > 0) { + const avgTransportFootprint = transportActivities.reduce((sum, a) => sum + Number(a.carbonFootprintKg), 0) / transportActivities.length; + if (avgTransportFootprint > 20) { + recommendations.push('Your transportation has high emissions - consider carbon offsetting'); + } + } + + const totalFootprint = trackingData.reduce((sum, record) => sum + Number(record.carbonFootprintKg), 0); + const totalOffsets = trackingData.reduce((sum, record) => sum + Number(record.offsetCreditsKg), 0); + + if (totalFootprint > totalOffsets) { + recommendations.push('Consider purchasing carbon offsets to neutralize your travel emissions'); + } + + if (recommendations.length === 0) { + recommendations.push('Great job on your sustainable choices! Keep it up!'); + recommendations.push('Share your eco-friendly travel tips with other travelers'); + } + + return recommendations; + } + + private generateEcoHighlights(eco: EcoEstablishment): string[] { + const highlights: string[] = []; + + if (eco.energyMeasures.solarPanels) highlights.push('☀️ Solar-powered'); + if (eco.energyMeasures.renewableEnergy) highlights.push('🔋 100% Renewable Energy'); + if (eco.waterConservation.rainwaterHarvesting) highlights.push('💧 Rainwater Harvesting'); + if (eco.wasteManagement.recyclingProgram) highlights.push('♻️ Comprehensive Recycling'); + if (eco.communitySupport.localEmployment) highlights.push('🤝 Local Employment'); + if (eco.biodiversityProtection.wildlifeConservation) highlights.push('🦋 Wildlife Conservation'); + + return highlights; + } +} diff --git a/src/modules/tourism/dto/create-destination.dto.ts b/src/modules/tourism/dto/create-destination.dto.ts new file mode 100755 index 0000000..e0f864c --- /dev/null +++ b/src/modules/tourism/dto/create-destination.dto.ts @@ -0,0 +1,36 @@ +import { IsString, IsOptional, IsNumber, IsBoolean } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateDestinationDto { + @ApiProperty({ description: 'Country ID', example: 1 }) + @IsNumber() + countryId: number; + + @ApiProperty({ description: 'Destination name', example: 'Santo Domingo' }) + @IsString() + name: string; + + @ApiPropertyOptional({ description: 'Description' }) + @IsOptional() + @IsString() + description?: string; + + @ApiPropertyOptional({ description: 'Category', example: 'city' }) + @IsOptional() + @IsString() + category?: string; + + @ApiPropertyOptional({ description: 'Coordinates (lat,lng)', example: '18.4861,-69.9312' }) + @IsOptional() + @IsString() + coordinates?: string; + + @ApiPropertyOptional({ description: 'Images' }) + @IsOptional() + images?: Record; + + @ApiPropertyOptional({ description: 'Active status', example: true }) + @IsOptional() + @IsBoolean() + active?: boolean; +} diff --git a/src/modules/tourism/dto/create-place.dto.ts b/src/modules/tourism/dto/create-place.dto.ts new file mode 100755 index 0000000..fc2de68 --- /dev/null +++ b/src/modules/tourism/dto/create-place.dto.ts @@ -0,0 +1,74 @@ +import { IsString, IsOptional, IsNumber, IsBoolean } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreatePlaceDto { + @ApiPropertyOptional({ description: 'Destination ID', example: 1 }) + @IsOptional() + @IsNumber() + destinationId?: number; + + @ApiProperty({ description: 'Place name', example: 'Alcázar de Colón' }) + @IsString() + name: string; + + @ApiPropertyOptional({ description: 'Description' }) + @IsOptional() + @IsString() + description?: string; + + @ApiPropertyOptional({ description: 'Category', example: 'monument' }) + @IsOptional() + @IsString() + category?: string; + + @ApiProperty({ description: 'Coordinates (lat,lng)', example: '18.4722,-69.8815' }) + @IsString() + coordinates: string; + + @ApiPropertyOptional({ description: 'Address' }) + @IsOptional() + @IsString() + address?: string; + + @ApiPropertyOptional({ description: 'Phone number' }) + @IsOptional() + @IsString() + phone?: string; + + @ApiPropertyOptional({ description: 'Website URL' }) + @IsOptional() + @IsString() + website?: string; + + @ApiPropertyOptional({ description: 'Opening hours' }) + @IsOptional() + openingHours?: Record; + + @ApiPropertyOptional({ description: 'Entrance fee', example: 25.00 }) + @IsOptional() + @IsNumber() + entranceFee?: number; + + @ApiPropertyOptional({ description: 'Images' }) + @IsOptional() + images?: Record; + + @ApiPropertyOptional({ description: 'Historical information' }) + @IsOptional() + @IsString() + historicalInfo?: string; + + @ApiPropertyOptional({ description: 'AR content' }) + @IsOptional() + arContent?: Record; + + @ApiPropertyOptional({ description: 'Audio guide URL' }) + @IsOptional() + @IsString() + audioGuideUrl?: string; + + @ApiPropertyOptional({ description: 'Active status', example: true }) + @IsOptional() + @IsBoolean() + active?: boolean; +} diff --git a/src/modules/tourism/dto/create-tour-guide.dto.ts b/src/modules/tourism/dto/create-tour-guide.dto.ts new file mode 100755 index 0000000..0e8aa33 --- /dev/null +++ b/src/modules/tourism/dto/create-tour-guide.dto.ts @@ -0,0 +1,52 @@ +import { IsString, IsOptional, IsNumber, IsBoolean, IsArray } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateTourGuideDto { + @ApiProperty({ description: 'User ID' }) + @IsString() + userId: string; + + @ApiPropertyOptional({ description: 'License number', example: 'TG-2025-001' }) + @IsOptional() + @IsString() + licenseNumber?: string; + + @ApiPropertyOptional({ description: 'Specialties', example: ['history', 'nature'] }) + @IsOptional() + @IsArray() + specialties?: string[]; + + @ApiPropertyOptional({ description: 'Languages', example: ['en', 'es', 'fr'] }) + @IsOptional() + @IsArray() + languages?: string[]; + + @ApiPropertyOptional({ description: 'Hourly rate', example: 25.00 }) + @IsOptional() + @IsNumber() + hourlyRate?: number; + + @ApiPropertyOptional({ description: 'Daily rate', example: 150.00 }) + @IsOptional() + @IsNumber() + dailyRate?: number; + + @ApiPropertyOptional({ description: 'Biography' }) + @IsOptional() + @IsString() + bio?: string; + + @ApiPropertyOptional({ description: 'Certifications' }) + @IsOptional() + certifications?: Record; + + @ApiPropertyOptional({ description: 'Is verified', example: false }) + @IsOptional() + @IsBoolean() + isVerified?: boolean; + + @ApiPropertyOptional({ description: 'Is available', example: true }) + @IsOptional() + @IsBoolean() + isAvailable?: boolean; +} diff --git a/src/modules/tourism/dto/update-destination.dto.ts b/src/modules/tourism/dto/update-destination.dto.ts new file mode 100755 index 0000000..3c4d4c5 --- /dev/null +++ b/src/modules/tourism/dto/update-destination.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateDestinationDto } from './create-destination.dto'; + +export class UpdateDestinationDto extends PartialType(CreateDestinationDto) {} diff --git a/src/modules/tourism/dto/update-place.dto.ts b/src/modules/tourism/dto/update-place.dto.ts new file mode 100755 index 0000000..b363f08 --- /dev/null +++ b/src/modules/tourism/dto/update-place.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreatePlaceDto } from './create-place.dto'; + +export class UpdatePlaceDto extends PartialType(CreatePlaceDto) {} diff --git a/src/modules/tourism/dto/update-tour-guide.dto.ts b/src/modules/tourism/dto/update-tour-guide.dto.ts new file mode 100755 index 0000000..f59c547 --- /dev/null +++ b/src/modules/tourism/dto/update-tour-guide.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateTourGuideDto } from './create-tour-guide.dto'; + +export class UpdateTourGuideDto extends PartialType(CreateTourGuideDto) {} diff --git a/src/modules/tourism/tourism.controller.ts b/src/modules/tourism/tourism.controller.ts new file mode 100755 index 0000000..08bbd69 --- /dev/null +++ b/src/modules/tourism/tourism.controller.ts @@ -0,0 +1,221 @@ +import { + Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, ParseIntPipe +} from '@nestjs/common'; +import { + ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam +} from '@nestjs/swagger'; +import { TourismService } from './tourism.service'; +import { CreateDestinationDto } from './dto/create-destination.dto'; +import { UpdateDestinationDto } from './dto/update-destination.dto'; +import { CreatePlaceDto } from './dto/create-place.dto'; +import { UpdatePlaceDto } from './dto/update-place.dto'; +import { CreateTourGuideDto } from './dto/create-tour-guide.dto'; +import { UpdateTourGuideDto } from './dto/update-tour-guide.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; +import { Destination } from '../../entities/destination.entity'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { TourGuide } from '../../entities/tour-guide.entity'; +import { TaxiDriver } from '../../entities/taxi-driver.entity'; + +@ApiTags('Tourism') +@Controller('tourism') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class TourismController { + constructor(private readonly tourismService: TourismService) {} + + // DESTINATIONS ENDPOINTS + @Post('destinations') + @UseGuards(RolesGuard) + @Roles('admin', 'super_admin') + @ApiOperation({ summary: 'Create a new destination (Admin only)' }) + @ApiResponse({ status: 201, description: 'Destination created successfully', type: Destination }) + createDestination(@Body() createDestinationDto: CreateDestinationDto) { + return this.tourismService.createDestination(createDestinationDto); + } + + @Get('destinations') + @ApiOperation({ summary: 'Get all destinations with pagination' }) + @ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number' }) + @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Items per page' }) + @ApiQuery({ name: 'countryId', required: false, type: Number, description: 'Filter by country' }) + findAllDestinations( + @Query('page') page?: number, + @Query('limit') limit?: number, + @Query('countryId') countryId?: number, + ) { + return this.tourismService.findAllDestinations(page, limit, countryId); + } + + @Get('destinations/:id') + @ApiOperation({ summary: 'Get destination by ID' }) + @ApiParam({ name: 'id', type: 'number' }) + @ApiResponse({ status: 200, type: Destination }) + findOneDestination(@Param('id', ParseIntPipe) id: number) { + return this.tourismService.findOneDestination(id); + } + + @Patch('destinations/:id') + @UseGuards(RolesGuard) + @Roles('admin') + @ApiOperation({ summary: 'Update destination (Admin only)' }) + @ApiParam({ name: 'id', type: 'number' }) + updateDestination( + @Param('id', ParseIntPipe) id: number, + @Body() updateDestinationDto: UpdateDestinationDto, + ) { + return this.tourismService.updateDestination(id, updateDestinationDto); + } + + @Delete('destinations/:id') + @UseGuards(RolesGuard) + @Roles('admin') + @ApiOperation({ summary: 'Deactivate destination (Admin only)' }) + @ApiParam({ name: 'id', type: 'number' }) + removeDestination(@Param('id', ParseIntPipe) id: number) { + return this.tourismService.removeDestination(id); + } + + // PLACES ENDPOINTS + @Post('places') + @UseGuards(RolesGuard) + @Roles('admin', 'guide') + @ApiOperation({ summary: 'Create a new place of interest' }) + @ApiResponse({ status: 201, description: 'Place created successfully', type: PlaceOfInterest }) + createPlace(@Body() createPlaceDto: CreatePlaceDto) { + return this.tourismService.createPlace(createPlaceDto); + } + + @Get('places') + @ApiOperation({ summary: 'Get all places with pagination and filters' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ name: 'category', required: false, type: String, description: 'Filter by category' }) + @ApiQuery({ name: 'destinationId', required: false, type: Number, description: 'Filter by destination' }) + findAllPlaces( + @Query('page') page?: number, + @Query('limit') limit?: number, + @Query('category') category?: string, + @Query('destinationId') destinationId?: number, + ) { + return this.tourismService.findAllPlaces(page, limit, category, destinationId); + } + + @Get('places/search') + @ApiOperation({ summary: 'Search places by name or description' }) + @ApiQuery({ name: 'q', type: String, description: 'Search query' }) + @ApiQuery({ name: 'lat', required: false, type: Number, description: 'Latitude for location-based search' }) + @ApiQuery({ name: 'lng', required: false, type: Number, description: 'Longitude for location-based search' }) + @ApiQuery({ name: 'radius', required: false, type: Number, description: 'Search radius in km' }) + searchPlaces( + @Query('q') query: string, + @Query('lat') lat?: number, + @Query('lng') lng?: number, + @Query('radius') radius?: number, + ) { + return this.tourismService.searchPlaces(query, lat, lng, radius); + } + + @Get('places/:id') + @ApiOperation({ summary: 'Get place by ID' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ status: 200, type: PlaceOfInterest }) + findOnePlace(@Param('id') id: string) { + return this.tourismService.findOnePlace(id); + } + + @Patch('places/:id') + @UseGuards(RolesGuard) + @Roles('admin', 'guide') + @ApiOperation({ summary: 'Update place of interest' }) + @ApiParam({ name: 'id', type: 'string' }) + updatePlace(@Param('id') id: string, @Body() updatePlaceDto: UpdatePlaceDto) { + return this.tourismService.updatePlace(id, updatePlaceDto); + } + + @Delete('places/:id') + @UseGuards(RolesGuard) + @Roles('admin') + @ApiOperation({ summary: 'Deactivate place (Admin only)' }) + @ApiParam({ name: 'id', type: 'string' }) + removePlace(@Param('id') id: string) { + return this.tourismService.removePlace(id); + } + + // TOUR GUIDES ENDPOINTS + @Post('guides') + @UseGuards(RolesGuard) + @Roles('admin', 'guide') + @ApiOperation({ summary: 'Register as tour guide' }) + @ApiResponse({ status: 201, description: 'Tour guide registered successfully', type: TourGuide }) + createTourGuide(@Body() createTourGuideDto: CreateTourGuideDto) { + return this.tourismService.createTourGuide(createTourGuideDto); + } + + @Get('guides') + @ApiOperation({ summary: 'Get all tour guides with filters' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ name: 'specialties', required: false, type: [String], description: 'Filter by specialties' }) + @ApiQuery({ name: 'languages', required: false, type: [String], description: 'Filter by languages' }) + findAllTourGuides( + @Query('page') page?: number, + @Query('limit') limit?: number, + @Query('specialties') specialties?: string[], + @Query('languages') languages?: string[], + ) { + return this.tourismService.findAllTourGuides(page, limit, specialties, languages); + } + + @Get('guides/:id') + @ApiOperation({ summary: 'Get tour guide by ID' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ status: 200, type: TourGuide }) + findOneTourGuide(@Param('id') id: string) { + return this.tourismService.findOneTourGuide(id); + } + + @Patch('guides/:id') + @UseGuards(RolesGuard) + @Roles('admin', 'guide') + @ApiOperation({ summary: 'Update tour guide profile' }) + @ApiParam({ name: 'id', type: 'string' }) + updateTourGuide(@Param('id') id: string, @Body() updateTourGuideDto: UpdateTourGuideDto) { + return this.tourismService.updateTourGuide(id, updateTourGuideDto); + } + + @Delete('guides/:id') + @UseGuards(RolesGuard) + @Roles('admin', 'guide') + @ApiOperation({ summary: 'Deactivate tour guide' }) + @ApiParam({ name: 'id', type: 'string' }) + removeTourGuide(@Param('id') id: string) { + return this.tourismService.removeTourGuide(id); + } + + // TAXI DRIVERS ENDPOINTS + @Get('taxis/available') + @ApiOperation({ summary: 'Get available taxi drivers' }) + @ApiQuery({ name: 'lat', required: false, type: Number, description: 'Current latitude' }) + @ApiQuery({ name: 'lng', required: false, type: Number, description: 'Current longitude' }) + @ApiQuery({ name: 'radius', required: false, type: Number, description: 'Search radius in km' }) + @ApiResponse({ status: 200, type: [TaxiDriver] }) + findAvailableTaxis( + @Query('lat') lat?: number, + @Query('lng') lng?: number, + @Query('radius') radius?: number, + ) { + return this.tourismService.findAvailableTaxiDrivers(lat, lng, radius); + } + + // STATISTICS + @Get('stats') + @UseGuards(RolesGuard) + @Roles('admin', 'super_admin') + @ApiOperation({ summary: 'Get tourism statistics (Admin only)' }) + getTourismStats() { + return this.tourismService.getTourismStats(); + } +} diff --git a/src/modules/tourism/tourism.module.ts b/src/modules/tourism/tourism.module.ts new file mode 100755 index 0000000..6b3e6f6 --- /dev/null +++ b/src/modules/tourism/tourism.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { TourismService } from './tourism.service'; +import { TourismController } from './tourism.controller'; +import { Destination } from '../../entities/destination.entity'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { TourGuide } from '../../entities/tour-guide.entity'; +import { TaxiDriver } from '../../entities/taxi-driver.entity'; +import { Itinerary } from '../../entities/itinerary.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + Destination, + PlaceOfInterest, + TourGuide, + TaxiDriver, + Itinerary, + ]), + ], + controllers: [TourismController], + providers: [TourismService], + exports: [TourismService], +}) +export class TourismModule {} diff --git a/src/modules/tourism/tourism.service.ts b/src/modules/tourism/tourism.service.ts new file mode 100755 index 0000000..17ce6db --- /dev/null +++ b/src/modules/tourism/tourism.service.ts @@ -0,0 +1,246 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Destination } from '../../entities/destination.entity'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { TourGuide } from '../../entities/tour-guide.entity'; +import { TaxiDriver } from '../../entities/taxi-driver.entity'; +import { CreateDestinationDto } from './dto/create-destination.dto'; +import { UpdateDestinationDto } from './dto/update-destination.dto'; +import { CreatePlaceDto } from './dto/create-place.dto'; +import { UpdatePlaceDto } from './dto/update-place.dto'; +import { CreateTourGuideDto } from './dto/create-tour-guide.dto'; +import { UpdateTourGuideDto } from './dto/update-tour-guide.dto'; + +@Injectable() +export class TourismService { + constructor( + @InjectRepository(Destination) + private readonly destinationRepository: Repository, + @InjectRepository(PlaceOfInterest) + private readonly placeRepository: Repository, + @InjectRepository(TourGuide) + private readonly tourGuideRepository: Repository, + @InjectRepository(TaxiDriver) + private readonly taxiDriverRepository: Repository, + ) {} + + // Destinations CRUD + async createDestination(createDestinationDto: CreateDestinationDto): Promise { + const destination = this.destinationRepository.create(createDestinationDto); + return this.destinationRepository.save(destination); + } + + async findAllDestinations(page: number = 1, limit: number = 10, countryId?: number): Promise<{ + destinations: Destination[]; + total: number; + page: number; + limit: number; + }> { + const query = this.destinationRepository.createQueryBuilder('destination') + .leftJoinAndSelect('destination.country', 'country') + .where('destination.active = :active', { active: true }); + + if (countryId) { + query.andWhere('destination.countryId = :countryId', { countryId }); + } + + const [destinations, total] = await query + .skip((page - 1) * limit) + .take(limit) + .orderBy('destination.name', 'ASC') + .getManyAndCount(); + + return { destinations, total, page, limit }; + } + + async findOneDestination(id: number): Promise { + const destination = await this.destinationRepository.findOne({ + where: { id }, + relations: ['country'], + }); + + if (!destination) { + throw new NotFoundException(`Destination with ID ${id} not found`); + } + + return destination; + } + + async updateDestination(id: number, updateDestinationDto: UpdateDestinationDto): Promise { + await this.findOneDestination(id); + await this.destinationRepository.update(id, updateDestinationDto); + return this.findOneDestination(id); + } + + async removeDestination(id: number): Promise { + await this.findOneDestination(id); + await this.destinationRepository.update(id, { active: false }); + } + + // Places CRUD + async createPlace(createPlaceDto: CreatePlaceDto): Promise { + const place = this.placeRepository.create(createPlaceDto); + return this.placeRepository.save(place); + } + + async findAllPlaces(page: number = 1, limit: number = 10, category?: string, destinationId?: number): Promise<{ + places: PlaceOfInterest[]; + total: number; + page: number; + limit: number; + }> { + const query = this.placeRepository.createQueryBuilder('place') + .leftJoinAndSelect('place.destination', 'destination') + .where('place.active = :active', { active: true }); + + if (category) { + query.andWhere('place.category = :category', { category }); + } + + if (destinationId) { + query.andWhere('place.destinationId = :destinationId', { destinationId }); + } + + const [places, total] = await query + .skip((page - 1) * limit) + .take(limit) + .orderBy('place.rating', 'DESC') + .getManyAndCount(); + + return { places, total, page, limit }; + } + + async findOnePlace(id: string): Promise { + const place = await this.placeRepository.findOne({ + where: { id }, + relations: ['destination'], + }); + + if (!place) { + throw new NotFoundException(`Place with ID ${id} not found`); + } + + return place; + } + + async updatePlace(id: string, updatePlaceDto: UpdatePlaceDto): Promise { + await this.findOnePlace(id); + await this.placeRepository.update(id, updatePlaceDto); + return this.findOnePlace(id); + } + + async removePlace(id: string): Promise { + await this.findOnePlace(id); + await this.placeRepository.update(id, { active: false }); + } + + async searchPlaces(query: string, lat?: number, lng?: number, radius?: number): Promise { + const searchQuery = this.placeRepository.createQueryBuilder('place') + .leftJoinAndSelect('place.destination', 'destination') + .where('place.active = :active', { active: true }) + .andWhere('(place.name ILIKE :query OR place.description ILIKE :query)', { query: `%${query}%` }); + + if (lat && lng && radius) { + // Add distance-based filtering (simplified, real implementation would use PostGIS functions) + searchQuery.andWhere('place.coordinates IS NOT NULL'); + } + + return searchQuery + .orderBy('place.rating', 'DESC') + .limit(20) + .getMany(); + } + + // Tour Guides CRUD + async createTourGuide(createTourGuideDto: CreateTourGuideDto): Promise { + const guide = this.tourGuideRepository.create(createTourGuideDto); + return this.tourGuideRepository.save(guide); + } + + async findAllTourGuides(page: number = 1, limit: number = 10, specialties?: string[], languages?: string[]): Promise<{ + guides: TourGuide[]; + total: number; + page: number; + limit: number; + }> { + const query = this.tourGuideRepository.createQueryBuilder('guide') + .leftJoinAndSelect('guide.user', 'user') + .where('guide.isAvailable = :available', { available: true }); + + if (specialties && specialties.length > 0) { + query.andWhere('guide.specialties && :specialties', { specialties }); + } + + if (languages && languages.length > 0) { + query.andWhere('guide.languages && :languages', { languages }); + } + + const [guides, total] = await query + .skip((page - 1) * limit) + .take(limit) + .orderBy('guide.rating', 'DESC') + .getManyAndCount(); + + return { guides, total, page, limit }; + } + + async findOneTourGuide(id: string): Promise { + const guide = await this.tourGuideRepository.findOne({ + where: { id }, + relations: ['user'], + }); + + if (!guide) { + throw new NotFoundException(`Tour guide with ID ${id} not found`); + } + + return guide; + } + + async updateTourGuide(id: string, updateTourGuideDto: UpdateTourGuideDto): Promise { + await this.findOneTourGuide(id); + await this.tourGuideRepository.update(id, updateTourGuideDto); + return this.findOneTourGuide(id); + } + + async removeTourGuide(id: string): Promise { + await this.findOneTourGuide(id); + await this.tourGuideRepository.update(id, { isAvailable: false }); + } + + // Taxi Drivers (basic methods) + async findAvailableTaxiDrivers(lat?: number, lng?: number, radius?: number): Promise { + const query = this.taxiDriverRepository.createQueryBuilder('driver') + .leftJoinAndSelect('driver.user', 'user') + .where('driver.isAvailable = :available', { available: true }) + .andWhere('driver.isVerified = :verified', { verified: true }); + + if (lat && lng && radius) { + // Add distance-based filtering + query.andWhere('driver.currentLocation IS NOT NULL'); + } + + return query + .orderBy('driver.rating', 'DESC') + .limit(10) + .getMany(); + } + + // Statistics + async getTourismStats(): Promise<{ + destinations: number; + places: number; + tourGuides: number; + availableTaxis: number; + }> { + const [destinations, places, tourGuides, availableTaxis] = await Promise.all([ + this.destinationRepository.count({ where: { active: true } }), + this.placeRepository.count({ where: { active: true } }), + this.tourGuideRepository.count({ where: { isAvailable: true } }), + this.taxiDriverRepository.count({ where: { isAvailable: true } }), + ]); + + return { destinations, places, tourGuides, availableTaxis }; + } +} diff --git a/src/modules/upload/upload.controller.ts b/src/modules/upload/upload.controller.ts new file mode 100755 index 0000000..2e7a39a --- /dev/null +++ b/src/modules/upload/upload.controller.ts @@ -0,0 +1,180 @@ +import { + Controller, Post, Delete, Param, UseGuards, Request, UseInterceptors, UploadedFile, UploadedFiles, BadRequestException +} from '@nestjs/common'; +import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiConsumes, ApiBody } from '@nestjs/swagger'; +import { UploadService } from './upload.service'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; + +@ApiTags('Upload') +@Controller('upload') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class UploadController { + constructor(private readonly uploadService: UploadService) {} + + @Post('image') + @UseInterceptors(FileInterceptor('file')) + @ApiOperation({ summary: 'Upload single image' }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + }, + folder: { + type: 'string', + description: 'Folder name (optional)', + }, + }, + }, + }) + @ApiResponse({ status: 201, description: 'Image uploaded successfully' }) + async uploadImage( + @UploadedFile() file: Express.Multer.File, + @Request() req, + ) { + if (!file) { + throw new BadRequestException('No file provided'); + } + + const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']; + if (!allowedTypes.includes(file.mimetype)) { + throw new BadRequestException('Only JPEG, PNG, WebP and GIF files are allowed'); + } + + const maxSize = 10 * 1024 * 1024; // 10MB + if (file.size > maxSize) { + throw new BadRequestException('File size must be less than 10MB'); + } + + const folder = req.body.folder || 'general'; + return this.uploadService.uploadImage(file, folder); + } + + @Post('images') + @UseInterceptors(FilesInterceptor('files', 10)) + @ApiOperation({ summary: 'Upload multiple images (max 10)' }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + properties: { + files: { + type: 'array', + items: { + type: 'string', + format: 'binary', + }, + }, + folder: { + type: 'string', + description: 'Folder name (optional)', + }, + }, + }, + }) + @ApiResponse({ status: 201, description: 'Images uploaded successfully' }) + async uploadImages( + @UploadedFiles() files: Express.Multer.File[], + @Request() req, + ) { + if (!files || files.length === 0) { + throw new BadRequestException('No files provided'); + } + + const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']; + const maxSize = 10 * 1024 * 1024; // 10MB + + for (const file of files) { + if (!allowedTypes.includes(file.mimetype)) { + throw new BadRequestException(`File ${file.originalname}: Only JPEG, PNG, WebP and GIF files are allowed`); + } + if (file.size > maxSize) { + throw new BadRequestException(`File ${file.originalname}: File size must be less than 10MB`); + } + } + + const folder = req.body.folder || 'general'; + return this.uploadService.uploadMultipleImages(files, folder); + } + + @Post('profile-image') + @UseInterceptors(FileInterceptor('file')) + @ApiOperation({ summary: 'Upload profile image' }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + }, + }, + }, + }) + async uploadProfileImage( + @UploadedFile() file: Express.Multer.File, + @Request() req, + ) { + if (!file) { + throw new BadRequestException('No file provided'); + } + + const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']; + if (!allowedTypes.includes(file.mimetype)) { + throw new BadRequestException('Only JPEG, PNG and WebP files are allowed for profile images'); + } + + const maxSize = 5 * 1024 * 1024; // 5MB + if (file.size > maxSize) { + throw new BadRequestException('Profile image size must be less than 5MB'); + } + + return this.uploadService.uploadProfileImage(file, req.user.id); + } + + @Post('establishment/:id/image') + @UseInterceptors(FileInterceptor('file')) + @ApiOperation({ summary: 'Upload establishment image' }) + @ApiConsumes('multipart/form-data') + async uploadEstablishmentImage( + @UploadedFile() file: Express.Multer.File, + @Param('id') establishmentId: string, + ) { + if (!file) { + throw new BadRequestException('No file provided'); + } + + return this.uploadService.uploadEstablishmentImage(file, establishmentId); + } + + @Post('place/:id/image') + @UseInterceptors(FileInterceptor('file')) + @ApiOperation({ summary: 'Upload place image' }) + @ApiConsumes('multipart/form-data') + async uploadPlaceImage( + @UploadedFile() file: Express.Multer.File, + @Param('id') placeId: string, + ) { + if (!file) { + throw new BadRequestException('No file provided'); + } + + return this.uploadService.uploadPlaceImage(file, placeId); + } + + @Delete('image/:key') + @ApiOperation({ summary: 'Delete image by key' }) + @ApiResponse({ status: 200, description: 'Image deleted successfully' }) + async deleteImage(@Param('key') key: string) { + // URL decode the key in case it contains special characters + const decodedKey = decodeURIComponent(key); + await this.uploadService.deleteImage(decodedKey); + return { message: 'Image deleted successfully' }; + } +} diff --git a/src/modules/upload/upload.module.ts b/src/modules/upload/upload.module.ts new file mode 100755 index 0000000..d834d65 --- /dev/null +++ b/src/modules/upload/upload.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { UploadService } from './upload.service'; +import { UploadController } from './upload.controller'; + +@Module({ + controllers: [UploadController], + providers: [UploadService], + exports: [UploadService], +}) +export class UploadModule {} diff --git a/src/modules/upload/upload.service.ts b/src/modules/upload/upload.service.ts new file mode 100755 index 0000000..a349713 --- /dev/null +++ b/src/modules/upload/upload.service.ts @@ -0,0 +1,155 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import * as sharp from 'sharp'; +import { v4 as uuidv4 } from 'uuid'; + +@Injectable() +export class UploadService { + private s3Client: S3Client; + private bucketName: string; + private cloudfrontUrl: string; + + constructor(private configService: ConfigService) { + const region = this.configService.get('aws.region'); + const accessKeyId = this.configService.get('aws.accessKeyId'); + const secretAccessKey = this.configService.get('aws.secretAccessKey'); + + if (!region || !accessKeyId || !secretAccessKey) { + throw new Error('AWS credentials are not properly configured'); + } + + this.s3Client = new S3Client({ + region, + credentials: { + accessKeyId, + secretAccessKey, + }, + }); + + this.bucketName = this.configService.get('aws.s3.bucket') || 'karibeo-assets'; + this.cloudfrontUrl = this.configService.get('aws.s3.cloudfrontUrl') || ''; + } + + async uploadImage(file: Express.Multer.File, folder: string = 'general'): Promise<{ + url: string; + key: string; + originalName: string; + size: number; + }> { + try { + // Generate unique filename + const fileExtension = file.originalname.split('.').pop() || 'jpg'; + const fileName = `${uuidv4()}.${fileExtension}`; + const key = `${folder}/${fileName}`; + + // Optimize image with Sharp + const optimizedBuffer = await this.optimizeImage(file.buffer, fileExtension); + + const command = new PutObjectCommand({ + Bucket: this.bucketName, + Key: key, + Body: optimizedBuffer, + ContentType: file.mimetype, + CacheControl: 'max-age=31536000', // 1 year + Metadata: { + originalName: file.originalname, + uploadedAt: new Date().toISOString(), + }, + }); + + await this.s3Client.send(command); + + const url = this.cloudfrontUrl + ? `${this.cloudfrontUrl}/${key}` + : `https://${this.bucketName}.s3.${this.configService.get('aws.region')}.amazonaws.com/${key}`; + + return { + url, + key, + originalName: file.originalname, + size: optimizedBuffer.length, + }; + } catch (error) { + throw new BadRequestException(`Image upload failed: ${error.message}`); + } + } + + async uploadMultipleImages(files: Express.Multer.File[], folder: string = 'general'): Promise> { + const uploadPromises = files.map(file => this.uploadImage(file, folder)); + return Promise.all(uploadPromises); + } + + async deleteImage(key: string): Promise { + try { + const command = new DeleteObjectCommand({ + Bucket: this.bucketName, + Key: key, + }); + + await this.s3Client.send(command); + } catch (error) { + throw new BadRequestException(`Image deletion failed: ${error.message}`); + } + } + + async getSignedDownloadUrl(key: string, expiresIn: number = 3600): Promise { + try { + const command = new GetObjectCommand({ + Bucket: this.bucketName, + Key: key, + }); + + return await getSignedUrl(this.s3Client, command, { expiresIn }); + } catch (error) { + throw new BadRequestException(`Failed to generate download URL: ${error.message}`); + } + } + + private async optimizeImage(buffer: Buffer, extension: string): Promise { + const isImage = ['jpg', 'jpeg', 'png', 'webp'].includes(extension.toLowerCase()); + + if (!isImage) { + return buffer; + } + + try { + return await sharp(buffer) + .resize(1920, 1080, { + fit: 'inside', + withoutEnlargement: true + }) + .jpeg({ + quality: 85, + progressive: true + }) + .toBuffer(); + } catch (error) { + // If optimization fails, return original buffer + return buffer; + } + } + + // Helper methods for different content types + async uploadProfileImage(file: Express.Multer.File, userId: string) { + return this.uploadImage(file, `profiles/${userId}`); + } + + async uploadEstablishmentImage(file: Express.Multer.File, establishmentId: string) { + return this.uploadImage(file, `establishments/${establishmentId}`); + } + + async uploadPlaceImage(file: Express.Multer.File, placeId: string) { + return this.uploadImage(file, `places/${placeId}`); + } + + async uploadReviewImage(file: Express.Multer.File, reviewId: string) { + return this.uploadImage(file, `reviews/${reviewId}`); + } +} diff --git a/src/modules/users/dto/create-user.dto.ts b/src/modules/users/dto/create-user.dto.ts new file mode 100755 index 0000000..4a81ea9 --- /dev/null +++ b/src/modules/users/dto/create-user.dto.ts @@ -0,0 +1,56 @@ +import { IsEmail, IsString, MinLength, IsOptional, IsNumber, IsBoolean } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateUserDto { + @ApiProperty({ description: 'Email address', example: 'user@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: 'Role ID', example: 2 }) + @IsOptional() + @IsNumber() + roleId?: number; + + @ApiPropertyOptional({ description: 'Preferred language', example: 'en' }) + @IsOptional() + @IsString() + preferredLanguage?: string; + + @ApiPropertyOptional({ description: 'Preferred currency', example: 'USD' }) + @IsOptional() + @IsString() + preferredCurrency?: string; + + @ApiPropertyOptional({ description: 'Is verified', example: false }) + @IsOptional() + @IsBoolean() + isVerified?: boolean; + + @ApiPropertyOptional({ description: 'Is active', example: true }) + @IsOptional() + @IsBoolean() + isActive?: boolean; +} diff --git a/src/modules/users/dto/update-user.dto.ts b/src/modules/users/dto/update-user.dto.ts new file mode 100755 index 0000000..719176b --- /dev/null +++ b/src/modules/users/dto/update-user.dto.ts @@ -0,0 +1,6 @@ +import { PartialType, OmitType } from '@nestjs/swagger'; +import { CreateUserDto } from './create-user.dto'; + +export class UpdateUserDto extends PartialType( + OmitType(CreateUserDto, ['email', 'password'] as const) +) {} diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts new file mode 100755 index 0000000..b825de6 --- /dev/null +++ b/src/modules/users/users.controller.ts @@ -0,0 +1,68 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, Request } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; +import { UsersService } from './users.service'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { User } from '../../entities/user.entity'; + +@ApiTags('Users') +@Controller('users') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + @Post() + @ApiOperation({ summary: 'Create a new user (Admin only)' }) + @ApiResponse({ status: 201, description: 'User created successfully', type: User }) + create(@Body() createUserDto: CreateUserDto) { + return this.usersService.create(createUserDto); + } + + @Get() + @ApiOperation({ summary: 'Get all users with pagination' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + findAll(@Query('page') page?: number, @Query('limit') limit?: number) { + return this.usersService.findAll(page, limit); + } + + @Get('stats') + @ApiOperation({ summary: 'Get user statistics (Admin only)' }) + getStats() { + return this.usersService.getUserStats(); + } + + @Get('me') + @ApiOperation({ summary: 'Get current user profile' }) + @ApiResponse({ status: 200, type: User }) + getProfile(@Request() req) { + return req.user; + } + + @Get(':id') + @ApiOperation({ summary: 'Get user by ID' }) + @ApiResponse({ status: 200, type: User }) + findOne(@Param('id') id: string) { + return this.usersService.findOne(id); + } + + @Patch('me') + @ApiOperation({ summary: 'Update current user profile' }) + updateProfile(@Request() req, @Body() updateUserDto: UpdateUserDto) { + return this.usersService.update(req.user.id, updateUserDto); + } + + @Patch(':id') + @ApiOperation({ summary: 'Update user by ID (Admin only)' }) + update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { + return this.usersService.update(id, updateUserDto); + } + + @Delete(':id') + @ApiOperation({ summary: 'Deactivate user (Admin only)' }) + remove(@Param('id') id: string) { + return this.usersService.remove(id); + } +} diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts new file mode 100755 index 0000000..9aeb95c --- /dev/null +++ b/src/modules/users/users.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UsersService } from './users.service'; +import { UsersController } from './users.controller'; +import { User } from '../../entities/user.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([User])], + controllers: [UsersController], + providers: [UsersService], + exports: [UsersService], +}) +export class UsersModule {} diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts new file mode 100755 index 0000000..8ee0724 --- /dev/null +++ b/src/modules/users/users.service.ts @@ -0,0 +1,153 @@ +import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import * as bcrypt from 'bcrypt'; +import { User } from '../../entities/user.entity'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; + +@Injectable() +export class UsersService { + constructor( + @InjectRepository(User) + private readonly userRepository: Repository, + ) {} + + async create(createUserDto: CreateUserDto): Promise { + const { email, password, ...userData } = createUserDto; + + // 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); + + // Create user + const user = this.userRepository.create({ + email, + passwordHash, + ...userData, + }); + + const savedUser = await this.userRepository.save(user); + + // Return user with relations + return this.findOne(savedUser.id); + } + + async findAll(page: number = 1, limit: number = 10): Promise<{ users: User[]; total: number; page: number; limit: number }> { + const [users, total] = await this.userRepository.findAndCount({ + relations: ['country', 'role', 'preferredLanguageEntity', 'preferences'], + skip: (page - 1) * limit, + take: limit, + order: { createdAt: 'DESC' }, + }); + + return { + users, + total, + page, + limit, + }; + } + + async findOne(id: string): Promise { + const user = await this.userRepository.findOne({ + where: { id }, + relations: ['country', 'role', 'preferredLanguageEntity', 'preferences'], + }); + + if (!user) { + throw new NotFoundException(`User with ID ${id} not found`); + } + + return user; + } + + async findByEmail(email: string): Promise { + return this.userRepository.findOne({ + where: { email }, + relations: ['country', 'role', 'preferredLanguageEntity', 'preferences'], + }); + } + + async update(id: string, updateUserDto: UpdateUserDto): Promise { + const user = await this.findOne(id); + + // Update user data + Object.assign(user, updateUserDto); + + await this.userRepository.save(user); + + // Return updated user with relations + return this.findOne(id); + } + + async remove(id: string): Promise { + const user = await this.findOne(id); + + // Soft delete by setting isActive to false + await this.userRepository.update(id, { isActive: false }); + } + + async changePassword(id: string, currentPassword: string, newPassword: string): Promise { + const user = await this.userRepository.findOne({ where: { id } }); + + if (!user) { + throw new NotFoundException(`User with ID ${id} not found`); + } + + // Verify current password + const isCurrentPasswordValid = await bcrypt.compare(currentPassword, user.passwordHash); + if (!isCurrentPasswordValid) { + throw new ConflictException('Current password is incorrect'); + } + + // Hash new password + const saltRounds = 12; + const newPasswordHash = await bcrypt.hash(newPassword, saltRounds); + + // Update password + await this.userRepository.update(id, { passwordHash: newPasswordHash }); + } + + async getUserStats(): Promise<{ + total: number; + active: number; + verified: number; + byRole: Array<{ role: string; count: number }>; + byCountry: Array<{ country: string; count: number }>; + }> { + const total = await this.userRepository.count(); + const active = await this.userRepository.count({ where: { isActive: true } }); + const verified = await this.userRepository.count({ where: { isVerified: true } }); + + const byRole = await this.userRepository + .createQueryBuilder('user') + .leftJoin('user.role', 'role') + .select('role.name', 'role') + .addSelect('COUNT(user.id)', 'count') + .groupBy('role.name') + .getRawMany(); + + const byCountry = await this.userRepository + .createQueryBuilder('user') + .leftJoin('user.country', 'country') + .select('country.name', 'country') + .addSelect('COUNT(user.id)', 'count') + .groupBy('country.name') + .getRawMany(); + + return { + total, + active, + verified, + byRole: byRole.map(item => ({ role: item.role || 'Unknown', count: parseInt(item.count) })), + byCountry: byCountry.map(item => ({ country: item.country || 'Unknown', count: parseInt(item.count) })), + }; + } +} diff --git a/src/modules/vehicle-management/dto/create-vehicle.dto.ts b/src/modules/vehicle-management/dto/create-vehicle.dto.ts new file mode 100644 index 0000000..a265465 --- /dev/null +++ b/src/modules/vehicle-management/dto/create-vehicle.dto.ts @@ -0,0 +1,88 @@ +import { IsString, IsNotEmpty, IsNumber, Min, IsOptional, IsArray, IsBoolean, Matches, IsObject } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +class ImageDto { +@ApiProperty({ example: 'https://example.com/vehicle-image.jpg' }) +@IsString() +url: string; +@ApiPropertyOptional({ example: 'Front view of the car' }) +@IsOptional() +@IsString() +altText?: string; +} +export class CreateVehicleDto { +@ApiProperty({ description: 'Vehicle type', example: 'car' }) +@IsString() +@IsNotEmpty() +vehicleType: string; +@ApiProperty({ description: 'Vehicle brand', example: 'Toyota' }) +@IsString() +@IsNotEmpty() +brand: string; +@ApiProperty({ description: 'Vehicle model', example: 'Corolla' }) +@IsString() +@IsNotEmpty() +model: string; +@ApiProperty({ description: 'Manufacturing year', example: 2020 }) +@IsNumber() +@Min(1900) +year: number; +@ApiProperty({ description: 'License plate', example: 'A123456' }) +@IsString() +@IsNotEmpty() +licensePlate: string; +@ApiProperty({ description: 'Vehicle color', example: 'White' }) +@IsString() +@IsNotEmpty() +color: string; +@ApiProperty({ description: 'Seating capacity', example: 5 }) +@IsNumber() +@Min(1) +capacity: number; +@ApiProperty({ description: 'Transmission type', example: 'automatic' }) +@IsString() +@IsNotEmpty() +transmissionType: string; +@ApiProperty({ description: 'Fuel type', example: 'gasoline' }) +@IsString() +@IsNotEmpty() +fuelType: string; +@ApiProperty({ description: 'Daily rental rate', example: 50.00 }) +@IsNumber() +@Min(0) +dailyRate: number; +@ApiPropertyOptional({ description: 'Currency', example: 'USD' }) +@IsString() +@IsOptional() +currency?: string; +@ApiPropertyOptional({ description: 'Vehicle features' }) +@IsArray() +@IsString({ each: true }) +@IsOptional() +features?: string[]; +@ApiPropertyOptional({ description: 'Vehicle images' }) +@IsArray() +@IsOptional() +@IsObject({ each: true }) // Using IsObject as a simple check for nested DTO array +images?: ImageDto[]; +@ApiPropertyOptional({ description: 'Latitude of current location' }) +@IsOptional() +@IsNumber() +currentLatitude?: number; +@ApiPropertyOptional({ description: 'Longitude of current location' }) +@IsOptional() +@IsNumber() +currentLongitude?: number; +@ApiPropertyOptional({ description: 'Insurance information' }) +@IsObject() +@IsOptional() +insuranceInfo?: Record; +@ApiPropertyOptional({ description: 'Maintenance records' }) +@IsArray() +@IsObject({ each: true }) +@IsOptional() +maintenanceRecords?: Record[]; +@ApiPropertyOptional({ description: 'Is available for rental', example: true }) +@IsBoolean() +@IsOptional() +isAvailable?: boolean; +} diff --git a/src/modules/vehicle-management/dto/update-vehicle.dto.ts b/src/modules/vehicle-management/dto/update-vehicle.dto.ts new file mode 100644 index 0000000..86e7118 --- /dev/null +++ b/src/modules/vehicle-management/dto/update-vehicle.dto.ts @@ -0,0 +1,3 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateVehicleDto } from './create-vehicle.dto'; +export class UpdateVehicleDto extends PartialType(CreateVehicleDto) {} diff --git a/src/modules/vehicle-management/dto/vehicle-availability-query.dto.ts b/src/modules/vehicle-management/dto/vehicle-availability-query.dto.ts new file mode 100644 index 0000000..00c4140 --- /dev/null +++ b/src/modules/vehicle-management/dto/vehicle-availability-query.dto.ts @@ -0,0 +1,29 @@ +import { IsString, IsNotEmpty, IsDateString, IsOptional, IsNumber } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class VehicleAvailabilityQueryDto { + @ApiProperty({ description: 'Start date for availability check (YYYY-MM-DD)', example: '2025-10-01' }) + @IsDateString() + @IsNotEmpty() + startDate: string; + + @ApiProperty({ description: 'End date for availability check (YYYY-MM-DD)', example: '2025-10-05' }) + @IsDateString() + @IsNotEmpty() + endDate: string; + + @ApiPropertyOptional({ description: 'Minimum capacity required' }) + @IsOptional() + @IsNumber() + minCapacity?: number; + + @ApiPropertyOptional({ description: 'Preferred transmission type (e.g., automatic)' }) + @IsOptional() + @IsString() + transmissionType?: string; + + @ApiPropertyOptional({ description: 'Preferred vehicle type (e.g., car, SUV)' }) + @IsOptional() + @IsString() + vehicleType?: string; +} diff --git a/src/modules/vehicle-management/vehicle-management.controller.ts b/src/modules/vehicle-management/vehicle-management.controller.ts new file mode 100644 index 0000000..bc35e7a --- /dev/null +++ b/src/modules/vehicle-management/vehicle-management.controller.ts @@ -0,0 +1,89 @@ +import { +Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, Request +} from '@nestjs/common'; +import { +ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiParam, ApiQuery +} from '@nestjs/swagger'; +import { VehicleManagementService } from './vehicle-management.service'; +import { CreateVehicleDto } from './dto/create-vehicle.dto'; +import { UpdateVehicleDto } from './dto/update-vehicle.dto'; +import { VehicleAvailabilityQueryDto } from './dto/vehicle-availability-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 { Vehicle } from '../../entities/vehicle.entity'; +import { Availability } from '../../entities/availability.entity'; +@ApiTags('Vehicle Management') +@ApiBearerAuth('JWT-auth') +@UseGuards(JwtAuthGuard) +@Controller('api/v1/vehicles') +export class VehicleManagementController { +constructor(private readonly vehicleManagementService: VehicleManagementService) {} +@Post() +@UseGuards(RolesGuard) +@Roles('admin', 'owner') // Vehicle owners can register vehicles +@ApiOperation({ summary: 'Create a new vehicle for rental' }) +@ApiResponse({ status: 201, description: 'Vehicle created successfully', type: Vehicle }) +create(@Body() createVehicleDto: CreateVehicleDto, @Request() req) { +return this.vehicleManagementService.createVehicle(createVehicleDto, req.user.id); +} +@Get() +@ApiOperation({ summary: 'Get a list of all vehicles, optionally filtered' }) +@ApiQuery({ name: 'isAvailable', required: false, type: Boolean, description: 'Filter by availability status' }) +@ApiQuery({ name: 'ownerId', required: false, type: String, description: 'Filter by owner ID' }) +@ApiQuery({ name: 'type', required: false, type: String, description: 'Filter by vehicle type (e.g., car, motorcycle)' }) +@ApiResponse({ status: 200, type: [Vehicle] }) +findAll( +@Query('isAvailable') isAvailable?: boolean, +@Query('ownerId') ownerId?: string, +@Query('type') type?: string, +) { +return this.vehicleManagementService.findAllVehicles({ isAvailable, ownerId, type }); +} +@Get(':id') +@ApiOperation({ summary: 'Get details of a specific vehicle by ID' }) +@ApiParam({ name: 'id', type: 'string', description: 'Vehicle ID' }) +@ApiResponse({ status: 200, type: Vehicle }) +findOne(@Param('id') id: string) { +return this.vehicleManagementService.findVehicleById(id); +} +@Patch(':id') +@UseGuards(RolesGuard) +@Roles('admin', 'owner') +@ApiOperation({ summary: 'Update an existing vehicle by its ID' }) +@ApiParam({ name: 'id', type: 'string', description: 'Vehicle ID' }) +@ApiResponse({ status: 200, description: 'Vehicle updated successfully', type: Vehicle }) +update(@Param('id') id: string, @Body() updateVehicleDto: UpdateVehicleDto) { +return this.vehicleManagementService.updateVehicle(id, updateVehicleDto); +} +@Delete(':id') +@UseGuards(RolesGuard) +@Roles('admin', 'owner') +@ApiOperation({ summary: 'Delete a vehicle by its ID' }) +@ApiParam({ name: 'id', type: 'string', description: 'Vehicle ID' }) +@ApiResponse({ status: 204, description: 'Vehicle deleted successfully' }) +remove(@Param('id') id: string) { +return this.vehicleManagementService.deleteVehicle(id); +} +@Get(':id/availability') +@ApiOperation({ summary: 'Get the availability of a specific vehicle in a date range' }) +@ApiParam({ name: 'id', type: 'string', description: 'Vehicle ID' }) +@ApiQuery({ name: 'startDate', type: String, format: 'date', example: '2025-10-01' }) +@ApiQuery({ name: 'endDate', type: String, format: 'date', example: '2025-10-05' }) +@ApiResponse({ status: 200, type: [Availability] }) +getVehicleAvailability(@Param('id') id: string, @Query() queryDto: VehicleAvailabilityQueryDto) { +return this.vehicleManagementService.getVehicleAvailability(id, queryDto); +} +@Patch(':id/verify') +@UseGuards(RolesGuard) +@Roles('admin') // Only admin can verify vehicles +@ApiOperation({ summary: 'Update vehicle verification status (Admin only)' }) +@ApiParam({ name: 'id', type: 'string', description: 'Vehicle ID' }) +@ApiResponse({ status: 200, description: 'Vehicle verification status updated', type: Vehicle }) +updateVerificationStatus( +@Param('id') id: string, +@Body('isVerified') isVerified: boolean, +) { +return this.vehicleManagementService.updateVehicleVerificationStatus(id, isVerified); +} +} diff --git a/src/modules/vehicle-management/vehicle-management.module.ts b/src/modules/vehicle-management/vehicle-management.module.ts new file mode 100644 index 0000000..b78c7bb --- /dev/null +++ b/src/modules/vehicle-management/vehicle-management.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { VehicleManagementService } from './vehicle-management.service'; +import { VehicleManagementController } from './vehicle-management.controller'; +import { Vehicle } from '../../entities/vehicle.entity'; +import { User } from '../../entities/user.entity'; +import { Availability } from '../../entities/availability.entity'; +import { NotificationsModule } from '../notifications/notifications.module'; +@Module({ +imports: [ +TypeOrmModule.forFeature([ +Vehicle, +User, +Availability, // Needed for availability checks +]), +NotificationsModule, +], +controllers: [VehicleManagementController], +providers: [VehicleManagementService], +exports: [VehicleManagementService], +}) +export class VehicleManagementModule {} diff --git a/src/modules/vehicle-management/vehicle-management.service.ts b/src/modules/vehicle-management/vehicle-management.service.ts new file mode 100644 index 0000000..0f62bde --- /dev/null +++ b/src/modules/vehicle-management/vehicle-management.service.ts @@ -0,0 +1,164 @@ +import { Injectable, NotFoundException, BadRequestException, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Vehicle } from '../../entities/vehicle.entity'; +import { CreateVehicleDto } from './dto/create-vehicle.dto'; +import { UpdateVehicleDto } from './dto/update-vehicle.dto'; +import { VehicleAvailabilityQueryDto } from './dto/vehicle-availability-query.dto'; +import { User } from '../../entities/user.entity'; +import { Availability } from '../../entities/availability.entity'; +import { NotificationsService } from '../notifications/notifications.service'; +import { NotificationCategory, NotificationType } from '../notifications/dto/create-notification.dto'; + +@Injectable() +export class VehicleManagementService { + private readonly logger = new Logger(VehicleManagementService.name); + + constructor( + @InjectRepository(Vehicle) + private readonly vehicleRepository: Repository, + @InjectRepository(User) + private readonly userRepository: Repository, + @InjectRepository(Availability) + private readonly availabilityRepository: Repository, + private readonly notificationsService: NotificationsService, + ) {} + + async createVehicle(createVehicleDto: CreateVehicleDto, ownerId: string): Promise { + const { currentLatitude, currentLongitude, images, maintenanceRecords, ...rest } = createVehicleDto; + + const owner = await this.userRepository.findOne({ where: { id: ownerId } }); + if (!owner) { + throw new NotFoundException(`Owner with ID "${ownerId}" not found.`); + } + + const vehicle = this.vehicleRepository.create({ + ...rest, + ownerId, + currentLocation: currentLatitude && currentLongitude ? `POINT(${currentLongitude} ${currentLatitude})` : null, + images: images ? images.map(img => ({ url: img.url, altText: img.altText })) : null, + maintenanceRecords: maintenanceRecords ? maintenanceRecords.map(rec => ({ ...rec })) : null, + isAvailable: true, + isVerified: false, + rating: null, + totalRentals: 0, + } as Partial); + + await this.notificationsService.createNotification({ + userId: 'admin', + type: NotificationType.PUSH, + category: NotificationCategory.ADMIN, + title: 'New Vehicle Created', + message: `A new vehicle (${vehicle.brand} ${vehicle.model}) has been registered and requires verification.`, + data: { vehicleId: vehicle.id, ownerId: ownerId, action: 'verify_vehicle' }, + }); + + return this.vehicleRepository.save(vehicle); + } + + async findAllVehicles(filter?: { isAvailable?: boolean; ownerId?: string; type?: string }): Promise { + const query: any = {}; + if (filter?.isAvailable !== undefined) { + query.isAvailable = filter.isAvailable; + } + if (filter?.ownerId) { + query.ownerId = filter.ownerId; + } + if (filter?.type) { + query.vehicleType = filter.type; + } + return this.vehicleRepository.find({ where: query }); + } + + async findVehicleById(id: string): Promise { + const vehicle = await this.vehicleRepository.findOne({ where: { id } }); + if (!vehicle) { + throw new NotFoundException(`Vehicle with ID "${id}" not found.`); + } + return vehicle; + } + + async updateVehicle(id: string, updateVehicleDto: UpdateVehicleDto): Promise { + const vehicle = await this.findVehicleById(id); + const { currentLatitude, currentLongitude, images, maintenanceRecords, ...rest } = updateVehicleDto; + + const updateData: Partial = { ...rest }; + + if (currentLatitude !== undefined && currentLongitude !== undefined) { + updateData.currentLocation = `POINT(${currentLongitude} ${currentLatitude})`; // Usar currentLatitude/Longitude del DTO + } else if (currentLatitude === null && currentLongitude === null) { + updateData.currentLocation = null; + } + + if (images !== undefined) { + updateData.images = images.map(img => ({ url: img.url, altText: img.altText })); + } + if (maintenanceRecords !== undefined) { + updateData.maintenanceRecords = maintenanceRecords.map(rec => ({ ...rec })); + } + + await this.vehicleRepository.update(id, updateData); + return this.findVehicleById(id); + } + + async deleteVehicle(id: string): Promise { + const result = await this.vehicleRepository.delete(id); + if (result.affected === 0) { + throw new NotFoundException(`Vehicle with ID "${id}" not found.`); + } + this.logger.log(`Vehicle with ID "${id}" deleted.`); + } + + async getVehicleAvailability(id: string, queryDto: VehicleAvailabilityQueryDto): Promise { + const vehicle = await this.findVehicleById(id); + if (!vehicle) { + throw new NotFoundException(`Vehicle with ID "${id}" not found.`); + } + + const startDate = new Date(queryDto.startDate); + const endDate = new Date(queryDto.endDate); + + if (startDate > endDate) { + throw new BadRequestException('Start date cannot be after end date.'); + } + + return this.availabilityRepository + .createQueryBuilder('availability') + .where('availability.resourceId = :resourceId', { resourceId: id }) + .andWhere('availability.resourceType = :resourceType', { resourceType: 'vehicle' }) + .andWhere('availability.date >= :startDate', { startDate: queryDto.startDate }) + .andWhere('availability.date <= :endDate', { endDate: queryDto.endDate }) + .andWhere('availability.isAvailable = :isAvailable', { isAvailable: true }) + .andWhere('availability.availableQuantity >= :minQuantity', { minQuantity: 1 }) + .orderBy('availability.date', 'ASC') + .getMany(); + } + + async updateVehicleVerificationStatus(id: string, isVerified: boolean): Promise { + const vehicle = await this.findVehicleById(id); + vehicle.isVerified = isVerified; + await this.vehicleRepository.save(vehicle); + + if (isVerified) { + await this.notificationsService.createNotification({ + userId: vehicle.ownerId, + type: NotificationType.PUSH, + category: NotificationCategory.INFO, + title: 'Vehicle Verified!', + message: `Your vehicle (${vehicle.brand} ${vehicle.model}) has been successfully verified and is now active for rentals.`, + data: { vehicleId: vehicle.id, status: 'verified' }, + }); + } else { + await this.notificationsService.createNotification({ + userId: vehicle.ownerId, + type: NotificationType.PUSH, + category: NotificationCategory.WARNING, + title: 'Vehicle Verification Update', + message: `Your vehicle (${vehicle.brand} ${vehicle.model}) verification status has been updated. Please check for details.`, + data: { vehicleId: vehicle.id, status: 'unverified' }, + }); + } + + return vehicle; + } +} diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts new file mode 100755 index 0000000..4df6580 --- /dev/null +++ b/test/app.e2e-spec.ts @@ -0,0 +1,25 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { App } from 'supertest/types'; +import { AppModule } from './../src/app.module'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/test/jest-e2e.json b/test/jest-e2e.json new file mode 100755 index 0000000..e9d912f --- /dev/null +++ b/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100755 index 0000000..64f86c6 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100755 index 0000000..e4dbf2e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "noFallthroughCasesInSwitch": false + } +}