feat: add place gate population scripts, map style assets, automated cron sync tasks, and expand dashboard and landing applications
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiExcludeController } from '@nestjs/swagger';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@ApiExcludeController()
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiExcludeController } from '@nestjs/swagger';
|
||||
import { AuthService } from './auth.service';
|
||||
import { ProvisionDeviceKeyDto } from './dto/provision-device-key.dto';
|
||||
|
||||
@ApiExcludeController()
|
||||
@ApiTags('auth')
|
||||
@Controller('auth')
|
||||
export class DeviceAuthController {
|
||||
|
||||
@@ -3,7 +3,12 @@ import { ApiKey } from './api-key.entity';
|
||||
|
||||
export enum TenantPlan {
|
||||
FREE = 'FREE',
|
||||
PAY_AS_YOU_GO = 'PAY_AS_YOU_GO',
|
||||
STARTER = 'STARTER',
|
||||
FLEET = 'FLEET',
|
||||
SCALE = 'SCALE',
|
||||
ENTERPRISE_FLEET = 'ENTERPRISE_FLEET',
|
||||
NATIONAL_GRID = 'NATIONAL_GRID',
|
||||
PRO = 'PRO',
|
||||
ENTERPRISE = 'ENTERPRISE',
|
||||
}
|
||||
@@ -14,15 +19,25 @@ export enum TenantRole {
|
||||
}
|
||||
|
||||
export const QUOTA_LIMITS: Record<TenantPlan, number> = {
|
||||
[TenantPlan.FREE]: 8000,
|
||||
[TenantPlan.STARTER]: 25000,
|
||||
[TenantPlan.FREE]: 10000,
|
||||
[TenantPlan.PAY_AS_YOU_GO]: 100000000,
|
||||
[TenantPlan.STARTER]: 2000000,
|
||||
[TenantPlan.FLEET]: 5000000,
|
||||
[TenantPlan.SCALE]: 15000000,
|
||||
[TenantPlan.ENTERPRISE_FLEET]: 40000000,
|
||||
[TenantPlan.NATIONAL_GRID]: 200000000,
|
||||
[TenantPlan.PRO]: 100000,
|
||||
[TenantPlan.ENTERPRISE]: 500000,
|
||||
[TenantPlan.ENTERPRISE]: 50000000,
|
||||
};
|
||||
|
||||
export const RATE_LIMITS: Record<TenantPlan, number> = {
|
||||
[TenantPlan.FREE]: 5,
|
||||
[TenantPlan.STARTER]: 100,
|
||||
[TenantPlan.FREE]: 10,
|
||||
[TenantPlan.PAY_AS_YOU_GO]: 500,
|
||||
[TenantPlan.STARTER]: 2100,
|
||||
[TenantPlan.FLEET]: 5400,
|
||||
[TenantPlan.SCALE]: 15000,
|
||||
[TenantPlan.ENTERPRISE_FLEET]: 45000,
|
||||
[TenantPlan.NATIONAL_GRID]: 270000,
|
||||
[TenantPlan.PRO]: 500,
|
||||
[TenantPlan.ENTERPRISE]: 50000,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Controller, Get, Post, Delete, Body, Param, UseGuards, Req, ForbiddenException } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { CreateKeyDto } from './dto/management/create-key.dto';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiExcludeController } from '@nestjs/swagger';
|
||||
import { FirebaseAuthGuard } from './guards/firebase-auth.guard';
|
||||
|
||||
@ApiExcludeController()
|
||||
@ApiTags('auth')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(FirebaseAuthGuard)
|
||||
|
||||
@@ -3,9 +3,10 @@ import { BillingService } from './billing.service';
|
||||
import { PayMobProvider } from './providers/paymob.provider';
|
||||
import { BinanceProvider } from './providers/binance.provider';
|
||||
import { FirebaseAuthGuard } from '../auth/guards/firebase-auth.guard';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiExcludeController } from '@nestjs/swagger';
|
||||
import { PaymentProvider, PaymentStatus } from './entities/transaction.entity';
|
||||
|
||||
@ApiExcludeController()
|
||||
@ApiTags('billing')
|
||||
@Controller('billing')
|
||||
export class BillingController {
|
||||
@@ -33,11 +34,26 @@ export class BillingController {
|
||||
const tenantId = req.tenant.id;
|
||||
const plan = body.plan;
|
||||
|
||||
// Mapping prices to plans
|
||||
const pricing = {
|
||||
'STARTER': 29,
|
||||
// Mapping prices to plans (matching public site & sovereign tiers)
|
||||
const pricing: Record<string, number> = {
|
||||
'PAY_AS_YOU_GO': 0,
|
||||
'STARTER': 350,
|
||||
'FLEET': 750,
|
||||
'SCALE': 1400,
|
||||
'ENTERPRISE_FLEET': 2800,
|
||||
'NATIONAL_GRID': 9900,
|
||||
'PRO': 89,
|
||||
};
|
||||
|
||||
if (plan === 'PAY_AS_YOU_GO') {
|
||||
await this.billingService.processSuccessfulPayment(
|
||||
`payg_activate_${Date.now()}`,
|
||||
body.provider || PaymentProvider.PAYMOB,
|
||||
0,
|
||||
{ tenantId, plan: 'PAY_AS_YOU_GO' }
|
||||
);
|
||||
return { checkoutUrl: '/console/billing?status=payg_activated', orderId: `payg_${Date.now()}` };
|
||||
}
|
||||
|
||||
const amount = pricing[plan] || 0;
|
||||
|
||||
@@ -95,7 +111,7 @@ export class BillingController {
|
||||
}
|
||||
|
||||
// Redirect back to dashboard with status
|
||||
const targetUrl = `https://map-dashboard.intaleqapp.com/dashboard.html#billing?payment_status=${query.success === 'true' ? 'success' : 'failed'}&id=${query.id}`;
|
||||
const targetUrl = `https://map-dashboard.intaleqapp.com/console/billing?payment_status=${query.success === 'true' ? 'success' : 'failed'}&id=${query.id}`;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
|
||||
@@ -114,16 +114,21 @@ export class BillingService {
|
||||
await this.tenantRepository.update(tenantId, { plan });
|
||||
|
||||
// Update Subscription
|
||||
const limits = {
|
||||
[TenantPlan.FREE]: 5000,
|
||||
[TenantPlan.STARTER]: 25000,
|
||||
const limits: Record<string, number> = {
|
||||
[TenantPlan.FREE]: 10000,
|
||||
[TenantPlan.PAY_AS_YOU_GO]: 100000000,
|
||||
[TenantPlan.STARTER]: 2000000,
|
||||
[TenantPlan.FLEET]: 5000000,
|
||||
[TenantPlan.SCALE]: 15000000,
|
||||
[TenantPlan.ENTERPRISE_FLEET]: 40000000,
|
||||
[TenantPlan.NATIONAL_GRID]: 200000000,
|
||||
[TenantPlan.PRO]: 100000,
|
||||
[TenantPlan.ENTERPRISE]: 500000,
|
||||
[TenantPlan.ENTERPRISE]: 50000000,
|
||||
};
|
||||
|
||||
const sub = await this.getSubscription(tenantId);
|
||||
sub.plan = plan;
|
||||
sub.monthlyRequestLimit = limits[plan] || 8000;
|
||||
sub.monthlyRequestLimit = limits[plan] || 10000;
|
||||
sub.status = SubscriptionStatus.ACTIVE;
|
||||
sub.currentPeriodStart = new Date();
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export class BinanceProvider {
|
||||
goodsDetail: `Subscription to ${plan} plan for Maps SaaS`,
|
||||
},
|
||||
passThroughInfo: JSON.stringify({ tenantId, plan }),
|
||||
returnUrl: `https://map-dashboard.intaleqapp.com/dashboard.html#billing`,
|
||||
returnUrl: `https://map-dashboard.intaleqapp.com/console/billing`,
|
||||
};
|
||||
|
||||
const signature = this.generateSignature(timestamp, nonce, body);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Controller, Get, Post, Body, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiExcludeController } from '@nestjs/swagger';
|
||||
import { CommunityService } from './community.service';
|
||||
import { ReviewContributionDto } from './dto/review-contribution.dto';
|
||||
import { ApiKeyGuard } from '../common/guards/api-key.guard';
|
||||
|
||||
@ApiExcludeController()
|
||||
@Controller('v1/admin/moderation')
|
||||
@UseGuards(ApiKeyGuard)
|
||||
export class ModerationController {
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import { IsString, IsNumber, IsOptional, IsEnum, Min, Max } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsString, IsNumber, IsOptional, IsIn, Min, Max } from 'class-validator';
|
||||
import { Type, Transform } from 'class-transformer';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
const COUNTRY_MAP: Record<string, string> = {
|
||||
jo: 'jordan',
|
||||
jordan: 'jordan',
|
||||
iq: 'iraq',
|
||||
iraq: 'iraq',
|
||||
sy: 'syria',
|
||||
syria: 'syria',
|
||||
eg: 'egypt',
|
||||
egypt: 'egypt',
|
||||
};
|
||||
|
||||
export class SearchQueryDto {
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ description: 'Search query string' })
|
||||
@@ -28,7 +39,14 @@ export class SearchQueryDto {
|
||||
radius?: number = 20000;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['jordan', 'syria', 'egypt', 'iraq'])
|
||||
@Transform(({ value }) => {
|
||||
if (typeof value === 'string') {
|
||||
const clean = value.trim().toLowerCase();
|
||||
return COUNTRY_MAP[clean] || clean;
|
||||
}
|
||||
return value;
|
||||
})
|
||||
@IsIn(['jordan', 'syria', 'egypt', 'iraq'])
|
||||
@ApiPropertyOptional({ description: 'Country filter', enum: ['jordan', 'syria', 'egypt', 'iraq'] })
|
||||
country?: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Controller, Get, Post, Delete, Body, Query, UseGuards, Req, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiQuery, ApiHeader } from '@nestjs/swagger';
|
||||
import { ApiTags, ApiOperation, ApiQuery, ApiHeader, ApiExcludeEndpoint } from '@nestjs/swagger';
|
||||
import { GeocodingService } from './geocoding.service';
|
||||
import { AdminBoundariesService } from './admin-boundaries.service';
|
||||
import { JordanResearchService } from './jordan-research.service';
|
||||
@@ -56,6 +56,7 @@ export class GeocodingController {
|
||||
return this.refinementService.suggestPlace(placeData, userId);
|
||||
}
|
||||
|
||||
@ApiExcludeEndpoint()
|
||||
@Delete('places')
|
||||
@UseGuards(AdminGuard)
|
||||
@ApiOperation({ summary: 'Delete a place by name or ID' })
|
||||
@@ -76,6 +77,7 @@ export class GeocodingController {
|
||||
throw new HttpException('Name or ID required', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@ApiExcludeEndpoint()
|
||||
@Post('upsert-place')
|
||||
@UseGuards(AdminGuard)
|
||||
@ApiOperation({ summary: 'Add or Update a location (Automated Scraper)' })
|
||||
@@ -83,6 +85,7 @@ export class GeocodingController {
|
||||
return this.geocodingService.upsertPlace(placeData);
|
||||
}
|
||||
|
||||
@ApiExcludeEndpoint()
|
||||
@Post('upsert-batch')
|
||||
@UseGuards(AdminGuard)
|
||||
@ApiOperation({ summary: 'Add or Update multiple locations in bulk' })
|
||||
@@ -102,6 +105,7 @@ export class GeocodingController {
|
||||
return this.geocodingService.getAllPlacesGeoJSON();
|
||||
}
|
||||
|
||||
@ApiExcludeEndpoint()
|
||||
@Post('import-boundaries')
|
||||
@UseGuards(AdminGuard)
|
||||
@ApiOperation({ summary: 'Import administrative boundaries from a local GeoJSON file on the server' })
|
||||
@@ -114,12 +118,14 @@ export class GeocodingController {
|
||||
return this.adminBoundariesService.importFromFile(country, filePath);
|
||||
}
|
||||
|
||||
@ApiExcludeEndpoint()
|
||||
@Get('research/zarqa')
|
||||
@ApiOperation({ summary: 'Generate a research report for Zarqa, Jordan (Sample Data)' })
|
||||
async zarqaResearch() {
|
||||
return this.jordanResearchService.generateZarqaReport();
|
||||
}
|
||||
|
||||
@ApiExcludeEndpoint()
|
||||
@Post('admin/sync-neighborhoods')
|
||||
@UseGuards(AdminGuard)
|
||||
@ApiOperation({ summary: 'Sync neighborhood points from OSM for a bbox' })
|
||||
@@ -129,6 +135,7 @@ export class GeocodingController {
|
||||
return this.adminLinkingService.syncOsmNeighborhoodPoints(bbox, country);
|
||||
}
|
||||
|
||||
@ApiExcludeEndpoint()
|
||||
@Post('admin/generate-voronoi')
|
||||
@UseGuards(AdminGuard)
|
||||
@ApiOperation({ summary: 'Generate Voronoi polygons for neighborhoods' })
|
||||
@@ -137,6 +144,7 @@ export class GeocodingController {
|
||||
return this.adminLinkingService.generateVoronoiNeighborhoods(country);
|
||||
}
|
||||
|
||||
@ApiExcludeEndpoint()
|
||||
@Post('admin/link-places')
|
||||
@UseGuards(AdminGuard)
|
||||
@ApiOperation({ summary: 'Link places to administrative hierarchy' })
|
||||
|
||||
@@ -16,7 +16,7 @@ import { getElevationMeters } from '../common/gis.utils';
|
||||
export class GeocodingService {
|
||||
private readonly logger = new Logger(GeocodingService.name);
|
||||
|
||||
private readonly DB_TIMEOUT_MS = 1100;
|
||||
private readonly DB_TIMEOUT_MS = 3500;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(PlaceSyria)
|
||||
@@ -103,10 +103,13 @@ export class GeocodingService {
|
||||
const [normalizedQueryRes] = await this.osmPointsRepository.query(`SELECT normalize_arabic($1) as nq`, [cleanQuery]);
|
||||
const normalizedQuery = normalizedQueryRes?.nq || cleanQuery.toLowerCase();
|
||||
|
||||
let queryParams: any[] = [normalizedQuery];
|
||||
const tokens = normalizedQuery.split(/\s+/).filter(t => t.length >= 3);
|
||||
const tokenWildcards = tokens.map(t => `%${t}%`);
|
||||
|
||||
let queryParams: any[] = [normalizedQuery, tokenWildcards];
|
||||
let locationCondition = '';
|
||||
if (hasLocation) {
|
||||
locationCondition = `AND ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) <= $4`;
|
||||
locationCondition = `AND ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($4::float, $3::float), 4326)) <= $5`;
|
||||
queryParams.push(lat, lon, radius);
|
||||
}
|
||||
|
||||
@@ -122,7 +125,7 @@ export class GeocodingService {
|
||||
id, name, name_ar, category,
|
||||
'' as neighbourhood, '' as district, '' as governorate,
|
||||
latitude, longitude, address, region, source, popularity_score,
|
||||
${hasLocation ? 'ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326))' : '0'} as distance,
|
||||
${hasLocation ? 'ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($4::float, $3::float), 4326))' : '0'} as distance,
|
||||
GREATEST(
|
||||
similarity(normalized_name, $1),
|
||||
CASE WHEN normalized_name ILIKE $1 || '%' THEN 0.95 ELSE 0.0 END,
|
||||
@@ -133,11 +136,8 @@ export class GeocodingService {
|
||||
WHERE (
|
||||
normalized_name % $1
|
||||
OR normalized_name ILIKE '%' || $1 || '%'
|
||||
-- Token containment (e.g. "المدينة الطبية", "سوبرماركت المدينة", "مخبز جواد")
|
||||
OR (length($1) > 2 AND EXISTS (
|
||||
SELECT 1 FROM unnest(string_to_array($1, ' ')) token
|
||||
WHERE length(token) >= 3 AND normalized_name ILIKE '%' || token || '%'
|
||||
))
|
||||
-- High-performance Token containment using GIN trigram index
|
||||
${tokens.length > 0 ? 'OR normalized_name ILIKE ANY($2)' : ''}
|
||||
-- Generic Category Keywords Mapping (All major amenities)
|
||||
OR (category IN ('mosque', 'place_of_worship') AND ($1 ILIKE '%مسجد%' OR $1 ILIKE '%جامع%' OR $1 ILIKE '%مصلى%'))
|
||||
OR (category IN ('restaurant', 'fast_food', 'food') AND ($1 ILIKE '%مطعم%' OR $1 ILIKE '%شاورما%' OR $1 ILIKE '%وجب%' OR $1 ILIKE '%مشاو%' OR $1 ILIKE '%برغر%'))
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param, UseGuards, Req, Query } from '@nestjs/common';
|
||||
import { MapRefinementService } from './map-refinement.service';
|
||||
import { FirebaseAuthGuard } from '../auth/guards/firebase-auth.guard';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiHeader } from '@nestjs/swagger';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiHeader, ApiExcludeController } from '@nestjs/swagger';
|
||||
import { CandidateStatus } from './entities/map-candidate.entity';
|
||||
|
||||
@ApiExcludeController()
|
||||
@ApiTags('map-refinement-places')
|
||||
@Controller('map-refinement/places')
|
||||
export class MapRefinementController {
|
||||
|
||||
+18
-6
@@ -40,17 +40,29 @@ async function bootstrap() {
|
||||
|
||||
// Swagger Documentation Setup
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Jordan Map Platform API')
|
||||
.setDescription('Backend API for map services, routing, and driver telemetry for Jordan')
|
||||
.setTitle('Siro Maps Platform API')
|
||||
.setDescription('Sovereign Geospatial Developer APIs — Geocoding, Places, Routing, Matrix, Vector Tiles & Fleet Telemetry across MENA')
|
||||
.setVersion('1.0')
|
||||
.addApiKey({ type: 'apiKey', name: 'x-api-key', in: 'header' }, 'x-api-key')
|
||||
.addTag('maps')
|
||||
.addTag('geocoding')
|
||||
.addTag('telemetry')
|
||||
.addTag('geocoding', 'Forward geocoding, autocomplete, reverse geocoding, and places search')
|
||||
.addTag('maps', 'Map styles, vector tiles, routing, and fuel economy')
|
||||
.addTag('telemetry', 'Live driver telemetry, fleet tracking, and trajectory analytics')
|
||||
.addTag('heritage', 'Jordan & regional heritage landmarks and cultural sites')
|
||||
.addTag('weather', 'Real-time weather along routes and road condition impact')
|
||||
.addTag('community', 'Driver incident reporting and road hazard alerts')
|
||||
.addTag('tactical', 'Tactical routing, offline packages, and sovereign grid operations')
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('docs', app, document);
|
||||
// Mount Swagger at both /docs and /api/docs so both direct and proxied requests work seamlessly
|
||||
SwaggerModule.setup('docs', app, document, {
|
||||
swaggerOptions: { persistAuthorization: true },
|
||||
customSiteTitle: 'Siro Maps API Docs',
|
||||
});
|
||||
SwaggerModule.setup('api/docs', app, document, {
|
||||
swaggerOptions: { persistAuthorization: true },
|
||||
customSiteTitle: 'Siro Maps API Docs',
|
||||
});
|
||||
|
||||
const port = process.env.API_PORT || 3000;
|
||||
await app.listen(port);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Controller, Get, Post, Query, UseGuards, Res } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { ApiTags, ApiOperation, ApiExcludeEndpoint } from '@nestjs/swagger';
|
||||
import type { Response } from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
@@ -28,6 +28,7 @@ export class MapsController {
|
||||
return this.fuelPricingService.updateMonthlyPrices();
|
||||
}
|
||||
|
||||
@ApiExcludeEndpoint()
|
||||
@Post('sync-routes')
|
||||
@ApiOperation({ summary: 'Request GraphHopper routing sync 🔄' })
|
||||
async syncRoutes() {
|
||||
@@ -124,6 +125,23 @@ export class MapsController {
|
||||
});
|
||||
}
|
||||
|
||||
// Sanitize internal / external provider URLs to protect internal infrastructure
|
||||
if (styleObj.glyphs && (styleObj.glyphs.includes('protomaps.com') || styleObj.glyphs.includes('cdn.'))) {
|
||||
styleObj.glyphs = 'https://map-saas.intaleqapp.com/fonts/{fontstack}/{range}.pbf';
|
||||
}
|
||||
if (styleObj.sprite && styleObj.sprite.includes('demotiles.maplibre.org')) {
|
||||
styleObj.sprite = 'https://map-saas.intaleqapp.com/sprites/osm-bright';
|
||||
}
|
||||
// Sanitize sources to Siro branded endpoints
|
||||
if (styleObj.sources) {
|
||||
Object.keys(styleObj.sources).forEach(sourceKey => {
|
||||
const s = styleObj.sources[sourceKey];
|
||||
if (s && s.url && (s.url.includes('protomaps.com') || s.url.includes('demotiles'))) {
|
||||
s.url = 'https://map-saas.intaleqapp.com/tiles/v1.json';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(styleObj);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Controller, Get, Post, Patch, Delete, Body, Param, Query, UseGuards, HttpException, HttpStatus, Logger } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiQuery, ApiParam } from '@nestjs/swagger';
|
||||
import { ApiTags, ApiOperation, ApiQuery, ApiParam, ApiExcludeController } from '@nestjs/swagger';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { CandidateRoad } from './candidate-road.entity';
|
||||
@@ -7,6 +7,7 @@ import { RoadSegmentStat } from './road-stat.entity';
|
||||
import { ApiKeyGuard } from '../common/guards/api-key.guard';
|
||||
import { RedisService } from '../common/redis.service';
|
||||
|
||||
@ApiExcludeController()
|
||||
@ApiTags('map-refinement-roads')
|
||||
@Controller('map-refinement/roads')
|
||||
@UseGuards(ApiKeyGuard)
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Res,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiExcludeController, ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiKeyGuard } from '../common/guards/api-key.guard';
|
||||
import { TenantThrottlerGuard } from '../common/guards/rate-limiter.guard';
|
||||
import { LineOfSightBodyDto, LineOfSightQueryDto } from './dto/line-of-sight.dto';
|
||||
@@ -20,12 +20,7 @@ import { TacticalService } from './tactical.service';
|
||||
import { DemTileService } from './dem-tile.service';
|
||||
import { RoutingPackageService } from './routing-package.service';
|
||||
|
||||
@ApiTags('tactical')
|
||||
@ApiHeader({
|
||||
name: 'x-api-key',
|
||||
description: 'Multi-tenant API Key for Intaleq Maps SaaS',
|
||||
required: true,
|
||||
})
|
||||
@ApiExcludeController()
|
||||
@Controller('tactical')
|
||||
@UseGuards(ApiKeyGuard, TenantThrottlerGuard)
|
||||
export class TacticalController {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Controller, Get, Query, UseGuards, Req } from '@nestjs/common';
|
||||
import { UsageService } from './usage.service';
|
||||
import { FirebaseAuthGuard } from '../auth/guards/firebase-auth.guard';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiExcludeController } from '@nestjs/swagger';
|
||||
|
||||
@ApiExcludeController()
|
||||
@ApiTags('usage')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(FirebaseAuthGuard)
|
||||
|
||||
@@ -36,7 +36,7 @@ export class UsageInterceptor implements NestInterceptor {
|
||||
message: 'Monthly API usage quota exceeded',
|
||||
used: monthlyUsed,
|
||||
limit,
|
||||
upgrade_url: 'https://map-dashbord.intaleqapp.com/#billing'
|
||||
upgrade_url: 'https://map-dashboard.intaleqapp.com/console/billing'
|
||||
}, HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
|
||||
|
||||
@@ -111,13 +111,19 @@ export class UsageService {
|
||||
async getUsageSummary(tenantId: string, plan: string = 'FREE') {
|
||||
const monthlyUsage = await this.getMonthlyUsage(tenantId);
|
||||
|
||||
// Map of plans to limits (synced with interceptor)
|
||||
const QUOTA_LIMITS = {
|
||||
'FREE': 8000,
|
||||
'PRO': 50000,
|
||||
'ENTERPRISE': 1000000
|
||||
// Map of plans to limits (synced with interceptor and tenant entity)
|
||||
const QUOTA_LIMITS: Record<string, number> = {
|
||||
'FREE': 10000,
|
||||
'PAY_AS_YOU_GO': 100000000,
|
||||
'STARTER': 2000000,
|
||||
'FLEET': 5000000,
|
||||
'SCALE': 15000000,
|
||||
'ENTERPRISE_FLEET': 40000000,
|
||||
'NATIONAL_GRID': 200000000,
|
||||
'PRO': 100000,
|
||||
'ENTERPRISE': 50000000,
|
||||
};
|
||||
const monthlyLimit = QUOTA_LIMITS[plan] || 8000;
|
||||
const monthlyLimit = QUOTA_LIMITS[plan] || 10000;
|
||||
|
||||
// Get daily stats and performance metrics
|
||||
const stats = await this.usageRepository
|
||||
|
||||
Reference in New Issue
Block a user