import { LocateMode, UtilityType } from '@prisma/client'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { ArrayMaxSize, ArrayMinSize, IsArray, IsDateString, IsEnum, IsIn, IsInt, IsNumber, IsOptional, IsString, IsUUID, Max, MaxLength, Min, ValidateNested, } from 'class-validator'; export const APP_FIX_TYPES = [ 'AUTONOMOUS', 'DGPS', 'FLOAT', 'FIXED', 'NO_FIX', ] as const; export type AppFixType = (typeof APP_FIX_TYPES)[number]; // Frozen app MQTT wire profile v1 from meta/contracts/telemetry-schema.md. This is deliberately // separate from MqttPointDto: the app wire uses the normative fix tokens FLOAT/FIXED/NO_FIX, // while the legacy device path uses Prisma's FLOAT_RTK/FIXED_RTK/NONE storage tokens. export class AppLogPointDto { @ApiProperty({ format: 'uuid', description: 'Client-generated UUIDv7 idempotency key.', }) @IsUUID() pointId: string; @ApiProperty({ format: 'date-time' }) @IsDateString() createdAt: string; @IsIn(['APP']) origin: 'APP'; @IsIn(['APP_MQTT']) uploadPath: 'APP_MQTT'; @IsNumber() @Min(-90) @Max(90) lat: number; @IsNumber() @Min(-180) @Max(180) lng: number; @IsOptional() @IsNumber() alt?: number; @IsDateString() ts: string; @IsOptional() @IsIn(APP_FIX_TYPES) fix?: AppFixType; @IsOptional() @IsNumber() @Min(0) hAcc?: number; @IsOptional() @IsNumber() @Min(0) vAcc?: number; @IsOptional() @IsInt() @Min(0) sats?: number; @IsOptional() @IsNumber() @Min(0) hdop?: number; @IsOptional() @IsNumber() @Min(0) depth?: number; @IsOptional() @IsInt() @Min(0) freqHz?: number; @IsOptional() @IsNumber() @Min(0) currentMa?: number; @IsOptional() @IsNumber() signalDb?: number; @IsOptional() @IsNumber() gainDb?: number; @IsOptional() @IsEnum(LocateMode) mode?: LocateMode; @IsOptional() @IsNumber() phaseDeg?: number; @IsOptional() @IsNumber() @Min(0) @Max(360) compassDeg?: number; @IsOptional() @IsNumber() @Min(0) @Max(100) distortionPct?: number; @IsOptional() @IsEnum(UtilityType) utility?: UtilityType; @IsOptional() @IsInt() seq?: number; @IsIn(['IN_SPEC', 'OUT_OF_SPEC', 'MANUAL', 'PPK_CORRECTED', 'WAIVED', 'NONCOMPLIANT']) qualityFlag: 'IN_SPEC' | 'OUT_OF_SPEC' | 'MANUAL' | 'PPK_CORRECTED' | 'WAIVED' | 'NONCOMPLIANT'; } export class AppLogPointsMessageDto { @ApiPropertyOptional({ description: 'MQTT app-log wire profile version.' }) @IsString() @MaxLength(16) schemaVersion: string; @ApiPropertyOptional({ description: 'Job id; exactly one of jobId or ticket is required.', }) @IsOptional() @IsString() jobId?: string; @ApiPropertyOptional({ description: 'Ticket number; exactly one of jobId or ticket is required.', }) @IsOptional() @IsString() @MaxLength(64) ticket?: string; @ApiProperty({ type: [AppLogPointDto] }) @IsArray() @ArrayMinSize(1) @ArrayMaxSize(500) @ValidateNested({ each: true }) @Type(() => AppLogPointDto) points: AppLogPointDto[]; } export type AppLogAckOutcome = 'ACCEPTED' | 'DUPLICATE' | 'REJECTED'; export interface AppLogAckResult { pointId: string; outcome: AppLogAckOutcome; reasonCode?: string; } export interface AppLogAck { schemaVersion: string; results: AppLogAckResult[]; }