src/tracking/geofence.controller.ts
geofences
Methods |
| Async create | |||||||||
create(orgId: string, body: CreateGeofenceBody)
|
|||||||||
Decorators :
@Post()
|
|||||||||
|
Defined in src/tracking/geofence.controller.ts:47
|
|||||||||
|
Parameters :
Returns :
unknown
|
| Async delete |
delete(orgId: string, id: string)
|
Decorators :
@Delete(':id')
|
|
Defined in src/tracking/geofence.controller.ts:146
|
|
Returns :
unknown
|
| Async get |
get(orgId: string, id: string)
|
Decorators :
@Get(':id')
|
|
Defined in src/tracking/geofence.controller.ts:102
|
|
Returns :
unknown
|
| Async list | ||||||
list(orgId: string)
|
||||||
Decorators :
@Get()
|
||||||
|
Defined in src/tracking/geofence.controller.ts:93
|
||||||
|
Parameters :
Returns :
unknown
|
| Async update | ||||||||||||
update(orgId: string, id: string, body: CreateGeofenceBody)
|
||||||||||||
Decorators :
@Patch(':id')
|
||||||||||||
|
Defined in src/tracking/geofence.controller.ts:116
|
||||||||||||
|
Parameters :
Returns :
unknown
|
import {
BadRequestException,
Body,
Controller,
Delete,
Get,
NotFoundException,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CombinedAuthGuard } from '../auth/guards/combined-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { PrismaService } from '../prisma/prisma.service';
/**
* Geofence CRUD. Reads are open to all authenticated staff (dispatchers
* and drivers need to know the operating envelope) but only operations
* leadership can edit the geofence fabric.
*/
const GEOFENCE_WRITERS = ['SUPER_ADMIN', 'ADMIN', 'OPERATIONS_MANAGER'] as const;
interface CreateGeofenceBody {
name?: string;
type?: string;
centerLat?: number;
centerLng?: number;
radiusMeters?: number;
polygon?: any;
geofenceType?: string;
}
@ApiTags('Geofences')
@ApiBearerAuth()
@UseGuards(CombinedAuthGuard, RolesGuard)
@Controller('geofences')
export class GeofenceController {
constructor(private readonly prisma: PrismaService) {}
@Post()
@Roles(...GEOFENCE_WRITERS)
@ApiOperation({ summary: 'Create a geofence' })
async create(
@CurrentUser('organizationId') orgId: string,
@Body() body: CreateGeofenceBody,
) {
if (!body?.name || typeof body.name !== 'string' || body.name.trim().length === 0) {
throw new BadRequestException('Geofence name is required');
}
if (body.name.length > 120) {
throw new BadRequestException('Geofence name must be 120 characters or fewer');
}
// Either circular (centerLat + centerLng + radius) or polygon
const isCircle = body.centerLat != null && body.centerLng != null && body.radiusMeters != null;
const isPolygon = body.polygon && Array.isArray(body.polygon) && body.polygon.length >= 3;
if (!isCircle && !isPolygon) {
throw new BadRequestException(
'Geofence must have either centerLat/centerLng/radiusMeters (circle) or a polygon array of at least 3 points',
);
}
if (isCircle) {
if (body.centerLat! < -90 || body.centerLat! > 90) {
throw new BadRequestException('centerLat must be between -90 and 90');
}
if (body.centerLng! < -180 || body.centerLng! > 180) {
throw new BadRequestException('centerLng must be between -180 and 180');
}
if (body.radiusMeters! <= 0 || body.radiusMeters! > 1_000_000) {
throw new BadRequestException('radiusMeters must be between 1 and 1,000,000');
}
}
return this.prisma.geofence.create({
data: {
organizationId: orgId,
name: body.name.trim(),
type: body.type || 'circle',
centerLat: body.centerLat,
centerLng: body.centerLng,
radiusMeters: body.radiusMeters,
polygon: body.polygon,
geofenceType: body.geofenceType,
},
});
}
@Get()
@ApiOperation({ summary: 'List all geofences (org-scoped)' })
async list(@CurrentUser('organizationId') orgId: string) {
return this.prisma.geofence.findMany({
where: { organizationId: orgId },
orderBy: { createdAt: 'desc' },
});
}
@Get(':id')
@ApiOperation({ summary: 'Get geofence by ID (org-scoped)' })
async get(
@CurrentUser('organizationId') orgId: string,
@Param('id') id: string,
) {
const geofence = await this.prisma.geofence.findFirst({
where: { id, organizationId: orgId },
});
if (!geofence) throw new NotFoundException('Geofence not found');
return geofence;
}
@Patch(':id')
@Roles(...GEOFENCE_WRITERS)
@ApiOperation({ summary: 'Update a geofence (org-scoped)' })
async update(
@CurrentUser('organizationId') orgId: string,
@Param('id') id: string,
@Body() body: CreateGeofenceBody,
) {
// Org-scoped existence check before update
const existing = await this.prisma.geofence.findFirst({
where: { id, organizationId: orgId },
});
if (!existing) throw new NotFoundException('Geofence not found');
if (body.centerLat !== undefined && (body.centerLat < -90 || body.centerLat > 90)) {
throw new BadRequestException('centerLat must be between -90 and 90');
}
if (body.centerLng !== undefined && (body.centerLng < -180 || body.centerLng > 180)) {
throw new BadRequestException('centerLng must be between -180 and 180');
}
if (body.radiusMeters !== undefined && (body.radiusMeters <= 0 || body.radiusMeters > 1_000_000)) {
throw new BadRequestException('radiusMeters must be between 1 and 1,000,000');
}
if (body.name !== undefined && body.name.length > 120) {
throw new BadRequestException('Geofence name must be 120 characters or fewer');
}
return this.prisma.geofence.update({ where: { id }, data: body });
}
@Delete(':id')
@Roles(...GEOFENCE_WRITERS)
@ApiOperation({ summary: 'Delete a geofence (org-scoped)' })
async delete(
@CurrentUser('organizationId') orgId: string,
@Param('id') id: string,
) {
const existing = await this.prisma.geofence.findFirst({
where: { id, organizationId: orgId },
});
if (!existing) throw new NotFoundException('Geofence not found');
await this.prisma.geofence.delete({ where: { id } });
return { deleted: true };
}
}