erp-construccion/docs/05-backend-specs/modules/SPEC-assets.md

30 KiB

Backend Specification: Assets Module

Version: 1.0.0 Fecha: 2025-12-05 Modulos: MAE-015 (Gestion de Activos y Mantenimiento)


Resumen

Metrica Valor
Controllers 5
Services 6
Entities 12
Endpoints 45+
Tests Requeridos 50+

Estructura del Modulo

modules/assets/
├── assets.module.ts
├── controllers/
│   ├── asset.controller.ts
│   ├── maintenance.controller.ts
│   ├── work-order.controller.ts
│   ├── tracking.controller.ts
│   └── report.controller.ts
├── services/
│   ├── asset.service.ts
│   ├── maintenance.service.ts
│   ├── work-order.service.ts
│   ├── tracking.service.ts
│   ├── geofence.service.ts
│   └── report.service.ts
├── entities/
│   ├── asset.entity.ts
│   ├── asset-category.entity.ts
│   ├── asset-location.entity.ts
│   ├── asset-assignment.entity.ts
│   ├── asset-movement.entity.ts
│   ├── maintenance-schedule.entity.ts
│   ├── maintenance-log.entity.ts
│   ├── work-order.entity.ts
│   ├── work-order-task.entity.ts
│   ├── geofence.entity.ts
│   ├── gps-tracking.entity.ts
│   └── depreciation.entity.ts
├── dto/
├── events/
└── jobs/
    ├── maintenance-alerts.job.ts
    └── depreciation-calc.job.ts

Controllers

1. AssetController

@Controller('api/v1/assets')
@UseGuards(AuthGuard, TenantGuard)
@ApiTags('assets')
export class AssetController {

  // GET /api/v1/assets
  @Get()
  @ApiOperation({ summary: 'List all assets' })
  async findAll(
    @Query() query: AssetQueryDto,
    @CurrentTenant() tenantId: UUID
  ): Promise<PaginatedResponse<AssetDto>>;

  // GET /api/v1/assets/:id
  @Get(':id')
  async findOne(
    @Param('id') id: UUID,
    @CurrentTenant() tenantId: UUID
  ): Promise<AssetDetailDto>;

  // POST /api/v1/assets
  @Post()
  async create(
    @Body() dto: CreateAssetDto,
    @CurrentTenant() tenantId: UUID,
    @CurrentUser() userId: UUID
  ): Promise<AssetDto>;

  // PUT /api/v1/assets/:id
  @Put(':id')
  async update(
    @Param('id') id: UUID,
    @Body() dto: UpdateAssetDto,
    @CurrentTenant() tenantId: UUID
  ): Promise<AssetDto>;

  // DELETE /api/v1/assets/:id
  @Delete(':id')
  async remove(
    @Param('id') id: UUID,
    @CurrentTenant() tenantId: UUID
  ): Promise<void>;

  // GET /api/v1/assets/categories
  @Get('categories')
  async getCategories(@CurrentTenant() tenantId: UUID): Promise<CategoryTreeDto[]>;

  // POST /api/v1/assets/categories
  @Post('categories')
  async createCategory(
    @Body() dto: CreateCategoryDto,
    @CurrentTenant() tenantId: UUID
  ): Promise<CategoryDto>;

  // GET /api/v1/assets/:id/history
  @Get(':id/history')
  async getHistory(@Param('id') id: UUID): Promise<AssetHistoryDto[]>;

  // POST /api/v1/assets/:id/assign
  @Post(':id/assign')
  async assignAsset(
    @Param('id') id: UUID,
    @Body() dto: AssignAssetDto,
    @CurrentUser() userId: UUID
  ): Promise<AssignmentDto>;

  // POST /api/v1/assets/:id/transfer
  @Post(':id/transfer')
  async transferAsset(
    @Param('id') id: UUID,
    @Body() dto: TransferAssetDto,
    @CurrentUser() userId: UUID
  ): Promise<MovementDto>;

  // POST /api/v1/assets/:id/dispose
  @Post(':id/dispose')
  async disposeAsset(
    @Param('id') id: UUID,
    @Body() dto: DisposeAssetDto,
    @CurrentUser() userId: UUID
  ): Promise<AssetDto>;

  // GET /api/v1/assets/:id/depreciation
  @Get(':id/depreciation')
  async getDepreciation(@Param('id') id: UUID): Promise<DepreciationScheduleDto>;

  // POST /api/v1/assets/bulk-import
  @Post('bulk-import')
  @UseInterceptors(FileInterceptor('file'))
  async bulkImport(
    @UploadedFile() file: Express.Multer.File,
    @CurrentTenant() tenantId: UUID,
    @CurrentUser() userId: UUID
  ): Promise<ImportResultDto>;
}

2. MaintenanceController

@Controller('api/v1/assets/maintenance')
@ApiTags('asset-maintenance')
export class MaintenanceController {

  // GET /api/v1/assets/maintenance/schedules
  @Get('schedules')
  async getSchedules(@Query() query: ScheduleQueryDto): Promise<PaginatedResponse<ScheduleDto>>;

  // GET /api/v1/assets/maintenance/schedules/:id
  @Get('schedules/:id')
  async getSchedule(@Param('id') id: UUID): Promise<ScheduleDetailDto>;

  // POST /api/v1/assets/maintenance/schedules
  @Post('schedules')
  async createSchedule(
    @Body() dto: CreateScheduleDto,
    @CurrentUser() userId: UUID
  ): Promise<ScheduleDto>;

  // PUT /api/v1/assets/maintenance/schedules/:id
  @Put('schedules/:id')
  async updateSchedule(
    @Param('id') id: UUID,
    @Body() dto: UpdateScheduleDto
  ): Promise<ScheduleDto>;

  // DELETE /api/v1/assets/maintenance/schedules/:id
  @Delete('schedules/:id')
  async deleteSchedule(@Param('id') id: UUID): Promise<void>;

  // GET /api/v1/assets/maintenance/logs
  @Get('logs')
  async getLogs(@Query() query: LogQueryDto): Promise<PaginatedResponse<MaintenanceLogDto>>;

  // POST /api/v1/assets/maintenance/logs
  @Post('logs')
  async createLog(
    @Body() dto: CreateLogDto,
    @CurrentUser() userId: UUID
  ): Promise<MaintenanceLogDto>;

  // GET /api/v1/assets/maintenance/upcoming
  @Get('upcoming')
  async getUpcoming(@Query() query: UpcomingQueryDto): Promise<UpcomingMaintenanceDto[]>;

  // GET /api/v1/assets/maintenance/overdue
  @Get('overdue')
  async getOverdue(@CurrentTenant() tenantId: UUID): Promise<OverdueMaintenanceDto[]>;

  // POST /api/v1/assets/maintenance/schedules/:id/execute
  @Post('schedules/:id/execute')
  async executeSchedule(
    @Param('id') id: UUID,
    @Body() dto: ExecuteMaintenanceDto,
    @CurrentUser() userId: UUID
  ): Promise<MaintenanceLogDto>;
}

3. WorkOrderController

@Controller('api/v1/assets/work-orders')
@ApiTags('work-orders')
export class WorkOrderController {

  // GET /api/v1/assets/work-orders
  @Get()
  async findAll(@Query() query: WorkOrderQueryDto): Promise<PaginatedResponse<WorkOrderDto>>;

  // GET /api/v1/assets/work-orders/:id
  @Get(':id')
  async findOne(@Param('id') id: UUID): Promise<WorkOrderDetailDto>;

  // POST /api/v1/assets/work-orders
  @Post()
  async create(
    @Body() dto: CreateWorkOrderDto,
    @CurrentUser() userId: UUID
  ): Promise<WorkOrderDto>;

  // PUT /api/v1/assets/work-orders/:id
  @Put(':id')
  async update(
    @Param('id') id: UUID,
    @Body() dto: UpdateWorkOrderDto
  ): Promise<WorkOrderDto>;

  // POST /api/v1/assets/work-orders/:id/assign
  @Post(':id/assign')
  async assignTechnician(
    @Param('id') id: UUID,
    @Body() dto: AssignTechnicianDto
  ): Promise<WorkOrderDto>;

  // POST /api/v1/assets/work-orders/:id/start
  @Post(':id/start')
  async startWork(@Param('id') id: UUID, @CurrentUser() userId: UUID): Promise<WorkOrderDto>;

  // POST /api/v1/assets/work-orders/:id/complete
  @Post(':id/complete')
  async completeWork(
    @Param('id') id: UUID,
    @Body() dto: CompleteWorkOrderDto,
    @CurrentUser() userId: UUID
  ): Promise<WorkOrderDto>;

  // POST /api/v1/assets/work-orders/:id/cancel
  @Post(':id/cancel')
  async cancelWork(
    @Param('id') id: UUID,
    @Body() dto: CancelWorkOrderDto,
    @CurrentUser() userId: UUID
  ): Promise<WorkOrderDto>;

  // GET /api/v1/assets/work-orders/:id/tasks
  @Get(':id/tasks')
  async getTasks(@Param('id') id: UUID): Promise<WorkOrderTaskDto[]>;

  // POST /api/v1/assets/work-orders/:id/tasks
  @Post(':id/tasks')
  async addTask(
    @Param('id') id: UUID,
    @Body() dto: CreateTaskDto
  ): Promise<WorkOrderTaskDto>;

  // PUT /api/v1/assets/work-orders/tasks/:taskId
  @Put('tasks/:taskId')
  async updateTask(
    @Param('taskId') taskId: UUID,
    @Body() dto: UpdateTaskDto
  ): Promise<WorkOrderTaskDto>;

  // POST /api/v1/assets/work-orders/:id/parts
  @Post(':id/parts')
  async addPart(
    @Param('id') id: UUID,
    @Body() dto: AddPartDto
  ): Promise<WorkOrderPartDto>;

  // POST /api/v1/assets/work-orders/:id/labor
  @Post(':id/labor')
  async addLabor(
    @Param('id') id: UUID,
    @Body() dto: AddLaborDto
  ): Promise<WorkOrderLaborDto>;
}

4. TrackingController

@Controller('api/v1/assets/tracking')
@ApiTags('asset-tracking')
export class TrackingController {

  // GET /api/v1/assets/tracking/geofences
  @Get('geofences')
  async getGeofences(@CurrentTenant() tenantId: UUID): Promise<GeofenceDto[]>;

  // GET /api/v1/assets/tracking/geofences/:id
  @Get('geofences/:id')
  async getGeofence(@Param('id') id: UUID): Promise<GeofenceDetailDto>;

  // POST /api/v1/assets/tracking/geofences
  @Post('geofences')
  async createGeofence(
    @Body() dto: CreateGeofenceDto,
    @CurrentUser() userId: UUID
  ): Promise<GeofenceDto>;

  // PUT /api/v1/assets/tracking/geofences/:id
  @Put('geofences/:id')
  async updateGeofence(
    @Param('id') id: UUID,
    @Body() dto: UpdateGeofenceDto
  ): Promise<GeofenceDto>;

  // DELETE /api/v1/assets/tracking/geofences/:id
  @Delete('geofences/:id')
  async deleteGeofence(@Param('id') id: UUID): Promise<void>;

  // GET /api/v1/assets/tracking/positions
  @Get('positions')
  async getCurrentPositions(
    @Query() query: PositionQueryDto,
    @CurrentTenant() tenantId: UUID
  ): Promise<AssetPositionDto[]>;

  // GET /api/v1/assets/tracking/:assetId/history
  @Get(':assetId/history')
  async getTrackingHistory(
    @Param('assetId') assetId: UUID,
    @Query() query: TrackingHistoryQueryDto
  ): Promise<GPSTrackingDto[]>;

  // POST /api/v1/assets/tracking/position
  @Post('position')
  async recordPosition(
    @Body() dto: RecordPositionDto
  ): Promise<GPSTrackingDto>;

  // GET /api/v1/assets/tracking/alerts
  @Get('alerts')
  async getGeofenceAlerts(@Query() query: AlertQueryDto): Promise<GeofenceAlertDto[]>;

  // GET /api/v1/assets/tracking/map-data
  @Get('map-data')
  async getMapData(@CurrentTenant() tenantId: UUID): Promise<MapDataDto>;
}

5. ReportController

@Controller('api/v1/assets/reports')
@ApiTags('asset-reports')
export class ReportController {

  // GET /api/v1/assets/reports/inventory
  @Get('inventory')
  async getInventoryReport(@Query() query: InventoryReportQueryDto): Promise<InventoryReportDto>;

  // GET /api/v1/assets/reports/depreciation
  @Get('depreciation')
  async getDepreciationReport(@Query() query: DepreciationReportQueryDto): Promise<DepreciationReportDto>;

  // GET /api/v1/assets/reports/maintenance-cost
  @Get('maintenance-cost')
  async getMaintenanceCostReport(@Query() query: MaintenanceCostQueryDto): Promise<MaintenanceCostReportDto>;

  // GET /api/v1/assets/reports/utilization
  @Get('utilization')
  async getUtilizationReport(@Query() query: UtilizationQueryDto): Promise<UtilizationReportDto>;

  // GET /api/v1/assets/reports/lifecycle
  @Get('lifecycle')
  async getLifecycleReport(@Query() query: LifecycleQueryDto): Promise<LifecycleReportDto>;

  // GET /api/v1/assets/reports/work-order-summary
  @Get('work-order-summary')
  async getWorkOrderSummary(@Query() query: WOSummaryQueryDto): Promise<WorkOrderSummaryDto>;

  // GET /api/v1/assets/reports/export/:type
  @Get('export/:type')
  async exportReport(
    @Param('type') type: 'inventory' | 'depreciation' | 'maintenance',
    @Query() query: ExportQueryDto
  ): Promise<StreamableFile>;
}

Services

AssetService

@Injectable()
export class AssetService {
  constructor(
    @InjectRepository(Asset)
    private readonly assetRepo: Repository<Asset>,
    @InjectRepository(AssetCategory)
    private readonly categoryRepo: Repository<AssetCategory>,
    @InjectRepository(AssetAssignment)
    private readonly assignmentRepo: Repository<AssetAssignment>,
    @InjectRepository(AssetMovement)
    private readonly movementRepo: Repository<AssetMovement>,
    private readonly eventEmitter: EventEmitter2
  ) {}

  async findAll(tenantId: UUID, query: AssetQueryDto): Promise<PaginatedResponse<AssetDto>>;
  async findOne(tenantId: UUID, id: UUID): Promise<AssetDetailDto>;
  async create(tenantId: UUID, userId: UUID, dto: CreateAssetDto): Promise<AssetDto>;
  async update(tenantId: UUID, id: UUID, dto: UpdateAssetDto): Promise<AssetDto>;
  async remove(tenantId: UUID, id: UUID): Promise<void>;
  async assignAsset(assetId: UUID, userId: UUID, dto: AssignAssetDto): Promise<AssignmentDto>;
  async transferAsset(assetId: UUID, userId: UUID, dto: TransferAssetDto): Promise<MovementDto>;
  async disposeAsset(assetId: UUID, userId: UUID, dto: DisposeAssetDto): Promise<AssetDto>;
  async getHistory(assetId: UUID): Promise<AssetHistoryDto[]>;
  async calculateDepreciationSchedule(assetId: UUID): Promise<DepreciationScheduleDto>;
  async bulkImport(tenantId: UUID, userId: UUID, data: AssetImportRow[]): Promise<ImportResultDto>;
}

MaintenanceService

@Injectable()
export class MaintenanceService {
  constructor(
    @InjectRepository(MaintenanceSchedule)
    private readonly scheduleRepo: Repository<MaintenanceSchedule>,
    @InjectRepository(MaintenanceLog)
    private readonly logRepo: Repository<MaintenanceLog>,
    private readonly notificationService: NotificationService,
    private readonly eventEmitter: EventEmitter2
  ) {}

  async getSchedules(tenantId: UUID, query: ScheduleQueryDto): Promise<PaginatedResponse<ScheduleDto>>;
  async createSchedule(tenantId: UUID, userId: UUID, dto: CreateScheduleDto): Promise<ScheduleDto>;
  async updateSchedule(scheduleId: UUID, dto: UpdateScheduleDto): Promise<ScheduleDto>;
  async deleteSchedule(scheduleId: UUID): Promise<void>;
  async executeSchedule(scheduleId: UUID, userId: UUID, dto: ExecuteMaintenanceDto): Promise<MaintenanceLogDto>;
  async getLogs(tenantId: UUID, query: LogQueryDto): Promise<PaginatedResponse<MaintenanceLogDto>>;
  async createLog(tenantId: UUID, userId: UUID, dto: CreateLogDto): Promise<MaintenanceLogDto>;
  async getUpcoming(tenantId: UUID, days: number): Promise<UpcomingMaintenanceDto[]>;
  async getOverdue(tenantId: UUID): Promise<OverdueMaintenanceDto[]>;
  async calculateNextDueDate(schedule: MaintenanceSchedule): Date;
}

WorkOrderService

@Injectable()
export class WorkOrderService {
  constructor(
    @InjectRepository(WorkOrder)
    private readonly workOrderRepo: Repository<WorkOrder>,
    @InjectRepository(WorkOrderTask)
    private readonly taskRepo: Repository<WorkOrderTask>,
    private readonly eventEmitter: EventEmitter2
  ) {}

  async findAll(tenantId: UUID, query: WorkOrderQueryDto): Promise<PaginatedResponse<WorkOrderDto>>;
  async findOne(tenantId: UUID, id: UUID): Promise<WorkOrderDetailDto>;
  async create(tenantId: UUID, userId: UUID, dto: CreateWorkOrderDto): Promise<WorkOrderDto>;
  async update(workOrderId: UUID, dto: UpdateWorkOrderDto): Promise<WorkOrderDto>;
  async assignTechnician(workOrderId: UUID, dto: AssignTechnicianDto): Promise<WorkOrderDto>;
  async startWork(workOrderId: UUID, userId: UUID): Promise<WorkOrderDto>;
  async completeWork(workOrderId: UUID, userId: UUID, dto: CompleteWorkOrderDto): Promise<WorkOrderDto>;
  async cancelWork(workOrderId: UUID, userId: UUID, dto: CancelWorkOrderDto): Promise<WorkOrderDto>;
  async addTask(workOrderId: UUID, dto: CreateTaskDto): Promise<WorkOrderTaskDto>;
  async updateTask(taskId: UUID, dto: UpdateTaskDto): Promise<WorkOrderTaskDto>;
  async addPart(workOrderId: UUID, dto: AddPartDto): Promise<WorkOrderPartDto>;
  async addLabor(workOrderId: UUID, dto: AddLaborDto): Promise<WorkOrderLaborDto>;
  async calculateTotalCost(workOrderId: UUID): Promise<{ parts: number; labor: number; total: number }>;
}

TrackingService

@Injectable()
export class TrackingService {
  constructor(
    @InjectRepository(GPSTracking)
    private readonly trackingRepo: Repository<GPSTracking>,
    private readonly geofenceService: GeofenceService,
    private readonly eventEmitter: EventEmitter2
  ) {}

  async getCurrentPositions(tenantId: UUID, query: PositionQueryDto): Promise<AssetPositionDto[]>;
  async getTrackingHistory(assetId: UUID, query: TrackingHistoryQueryDto): Promise<GPSTrackingDto[]>;
  async recordPosition(dto: RecordPositionDto): Promise<GPSTrackingDto>;
  async checkGeofenceViolations(assetId: UUID, position: Point): Promise<GeofenceAlertDto[]>;
  async getMapData(tenantId: UUID): Promise<MapDataDto>;
}

GeofenceService

@Injectable()
export class GeofenceService {
  constructor(
    @InjectRepository(Geofence)
    private readonly geofenceRepo: Repository<Geofence>,
    private readonly dataSource: DataSource
  ) {}

  async findAll(tenantId: UUID): Promise<GeofenceDto[]>;
  async findOne(id: UUID): Promise<GeofenceDetailDto>;
  async create(tenantId: UUID, userId: UUID, dto: CreateGeofenceDto): Promise<GeofenceDto>;
  async update(id: UUID, dto: UpdateGeofenceDto): Promise<GeofenceDto>;
  async delete(id: UUID): Promise<void>;
  async checkPointInGeofence(point: Point, geofenceId: UUID): Promise<boolean>;
  async getGeofencesContainingPoint(tenantId: UUID, point: Point): Promise<GeofenceDto[]>;
  async getAssetsInGeofence(geofenceId: UUID): Promise<AssetDto[]>;
}

DTOs

Asset DTOs

export class CreateAssetDto {
  @IsString() @MaxLength(30)
  code: string;

  @IsString() @MaxLength(200)
  name: string;

  @IsOptional() @IsString()
  description?: string;

  @IsUUID()
  categoryId: string;

  @IsOptional() @IsString()
  serialNumber?: string;

  @IsOptional() @IsString()
  manufacturer?: string;

  @IsOptional() @IsString()
  model?: string;

  @IsOptional() @IsDateString()
  purchaseDate?: string;

  @IsOptional() @IsNumber()
  purchaseCost?: number;

  @IsOptional() @IsNumber()
  currentValue?: number;

  @IsOptional() @IsEnum(DepreciationMethod)
  depreciationMethod?: DepreciationMethod;

  @IsOptional() @IsNumber()
  usefulLifeYears?: number;

  @IsOptional() @IsNumber()
  salvageValue?: number;

  @IsOptional() @IsUUID()
  locationId?: string;

  @IsOptional() @IsBoolean()
  isGPSEnabled?: boolean;
}

export class AssetQueryDto extends PaginationDto {
  @IsOptional() @IsUUID()
  categoryId?: string;

  @IsOptional() @IsEnum(AssetStatus)
  status?: AssetStatus;

  @IsOptional() @IsUUID()
  locationId?: string;

  @IsOptional() @IsUUID()
  assignedTo?: string;

  @IsOptional() @IsString()
  search?: string;

  @IsOptional() @IsBoolean()
  includeDisposed?: boolean;
}

export class AssignAssetDto {
  @IsUUID()
  assignedToId: string;

  @IsEnum(AssigneeType)
  assigneeType: AssigneeType;

  @IsOptional() @IsDateString()
  assignedFrom?: string;

  @IsOptional() @IsDateString()
  assignedUntil?: string;

  @IsOptional() @IsString()
  notes?: string;
}

export class TransferAssetDto {
  @IsUUID()
  fromLocationId: string;

  @IsUUID()
  toLocationId: string;

  @IsOptional() @IsString()
  reason?: string;

  @IsOptional() @IsString()
  notes?: string;
}

export class DisposeAssetDto {
  @IsEnum(DisposalType)
  disposalType: DisposalType;

  @IsDateString()
  disposalDate: string;

  @IsOptional() @IsNumber()
  disposalValue?: number;

  @IsString()
  reason: string;

  @IsOptional() @IsString()
  notes?: string;
}

Maintenance DTOs

export class CreateScheduleDto {
  @IsUUID()
  assetId: string;

  @IsEnum(MaintenanceType)
  maintenanceType: MaintenanceType;

  @IsString() @MaxLength(200)
  name: string;

  @IsOptional() @IsString()
  description?: string;

  @IsEnum(FrequencyType)
  frequencyType: FrequencyType;

  @IsNumber()
  frequencyValue: number;

  @IsOptional() @IsNumber()
  meterBasedValue?: number;

  @IsDateString()
  nextDueDate: string;

  @IsOptional() @IsNumber()
  estimatedDuration?: number;

  @IsOptional() @IsNumber()
  estimatedCost?: number;

  @IsOptional() @IsUUID()
  assignedTo?: string;
}

export class ExecuteMaintenanceDto {
  @IsDateString()
  executionDate: string;

  @IsOptional() @IsNumber()
  actualDuration?: number;

  @IsOptional() @IsNumber()
  actualCost?: number;

  @IsOptional() @IsString()
  notes?: string;

  @IsOptional() @IsArray()
  partsUsed?: MaintenancePartDto[];

  @IsOptional() @IsNumber()
  meterReading?: number;
}

Work Order DTOs

export class CreateWorkOrderDto {
  @IsUUID()
  assetId: string;

  @IsEnum(WorkOrderType)
  workOrderType: WorkOrderType;

  @IsEnum(WorkOrderPriority)
  priority: WorkOrderPriority;

  @IsString() @MaxLength(200)
  title: string;

  @IsString()
  description: string;

  @IsOptional() @IsDateString()
  scheduledDate?: string;

  @IsOptional() @IsDateString()
  dueDate?: string;

  @IsOptional() @IsUUID()
  assignedTo?: string;

  @IsOptional() @IsNumber()
  estimatedHours?: number;

  @IsOptional() @IsNumber()
  budgetAmount?: number;

  @IsOptional() @IsUUID()
  maintenanceScheduleId?: string;
}

export class CompleteWorkOrderDto {
  @IsDateString()
  completedAt: string;

  @IsOptional() @IsNumber()
  actualHours?: number;

  @IsOptional() @IsNumber()
  totalCost?: number;

  @IsOptional() @IsString()
  resolution?: string;

  @IsOptional() @IsString()
  notes?: string;
}

export class WorkOrderQueryDto extends PaginationDto {
  @IsOptional() @IsUUID()
  assetId?: string;

  @IsOptional() @IsEnum(WorkOrderStatus)
  status?: WorkOrderStatus;

  @IsOptional() @IsEnum(WorkOrderType)
  workOrderType?: WorkOrderType;

  @IsOptional() @IsEnum(WorkOrderPriority)
  priority?: WorkOrderPriority;

  @IsOptional() @IsUUID()
  assignedTo?: string;

  @IsOptional() @IsDateString()
  fromDate?: string;

  @IsOptional() @IsDateString()
  toDate?: string;
}

Geofence DTOs

export class CreateGeofenceDto {
  @IsString() @MaxLength(100)
  name: string;

  @IsOptional() @IsString()
  description?: string;

  @IsEnum(GeofenceType)
  geofenceType: GeofenceType;

  // GeoJSON format
  @IsObject()
  geometry: {
    type: 'Polygon' | 'Circle';
    coordinates: number[][] | { center: number[]; radius: number };
  };

  @IsOptional() @IsBoolean()
  alertOnEntry?: boolean;

  @IsOptional() @IsBoolean()
  alertOnExit?: boolean;

  @IsOptional() @IsBoolean()
  isActive?: boolean;
}

export class RecordPositionDto {
  @IsUUID()
  assetId: string;

  @IsNumber()
  latitude: number;

  @IsNumber()
  longitude: number;

  @IsOptional() @IsNumber()
  altitude?: number;

  @IsOptional() @IsNumber()
  speed?: number;

  @IsOptional() @IsNumber()
  heading?: number;

  @IsOptional() @IsNumber()
  accuracy?: number;

  @IsOptional() @IsDateString()
  timestamp?: string;
}

Scheduled Jobs

MaintenanceAlertsJob

@Injectable()
export class MaintenanceAlertsJob {
  constructor(
    private readonly maintenanceService: MaintenanceService,
    private readonly notificationService: NotificationService
  ) {}

  @Cron('0 7 * * *')  // Daily at 7 AM
  async generateMaintenanceAlerts() {
    // Find maintenance due in next 7 days
    const upcoming = await this.maintenanceService.getUpcoming(null, 7);

    for (const item of upcoming) {
      await this.notificationService.send({
        type: 'maintenance_due',
        recipients: item.responsibleUsers,
        data: {
          assetName: item.assetName,
          maintenanceType: item.maintenanceType,
          dueDate: item.dueDate,
          daysRemaining: item.daysRemaining
        }
      });
    }

    // Find overdue maintenance
    const overdue = await this.maintenanceService.getOverdue(null);

    for (const item of overdue) {
      await this.notificationService.send({
        type: 'maintenance_overdue',
        severity: 'high',
        recipients: item.responsibleUsers,
        data: {
          assetName: item.assetName,
          maintenanceType: item.maintenanceType,
          dueDate: item.dueDate,
          daysOverdue: item.daysOverdue
        }
      });
    }
  }
}

DepreciationCalcJob

@Injectable()
export class DepreciationCalcJob {
  constructor(
    private readonly assetService: AssetService,
    @InjectRepository(Depreciation)
    private readonly depreciationRepo: Repository<Depreciation>
  ) {}

  @Cron('0 1 1 * *')  // Monthly on 1st at 1 AM
  async calculateMonthlyDepreciation() {
    const assets = await this.assetService.findAssetsForDepreciation();

    for (const asset of assets) {
      const depreciation = this.calculateDepreciation(asset);

      await this.depreciationRepo.save({
        assetId: asset.id,
        periodStart: startOfMonth(new Date()),
        periodEnd: endOfMonth(new Date()),
        depreciationAmount: depreciation,
        accumulatedDepreciation: asset.accumulatedDepreciation + depreciation,
        bookValue: asset.currentValue - depreciation
      });

      // Update asset current value
      await this.assetService.updateCurrentValue(asset.id, asset.currentValue - depreciation);
    }
  }

  private calculateDepreciation(asset: Asset): number {
    switch (asset.depreciationMethod) {
      case DepreciationMethod.STRAIGHT_LINE:
        return (asset.purchaseCost - asset.salvageValue) / (asset.usefulLifeYears * 12);
      case DepreciationMethod.DECLINING_BALANCE:
        const rate = 2 / asset.usefulLifeYears;
        return asset.currentValue * rate / 12;
      default:
        return 0;
    }
  }
}

Events

export class AssetCreatedEvent {
  constructor(
    public readonly assetId: string,
    public readonly tenantId: string,
    public readonly code: string,
    public readonly name: string,
    public readonly categoryId: string,
    public readonly timestamp: Date = new Date()
  ) {}
}

export class AssetAssignedEvent {
  constructor(
    public readonly assetId: string,
    public readonly assignedToId: string,
    public readonly assigneeType: AssigneeType,
    public readonly assignedBy: string,
    public readonly timestamp: Date = new Date()
  ) {}
}

export class AssetTransferredEvent {
  constructor(
    public readonly assetId: string,
    public readonly fromLocationId: string,
    public readonly toLocationId: string,
    public readonly transferredBy: string,
    public readonly timestamp: Date = new Date()
  ) {}
}

export class AssetDisposedEvent {
  constructor(
    public readonly assetId: string,
    public readonly disposalType: DisposalType,
    public readonly disposalValue: number,
    public readonly disposedBy: string,
    public readonly timestamp: Date = new Date()
  ) {}
}

export class MaintenanceCompletedEvent {
  constructor(
    public readonly maintenanceLogId: string,
    public readonly assetId: string,
    public readonly maintenanceType: MaintenanceType,
    public readonly actualCost: number,
    public readonly completedBy: string,
    public readonly timestamp: Date = new Date()
  ) {}
}

export class WorkOrderCompletedEvent {
  constructor(
    public readonly workOrderId: string,
    public readonly assetId: string,
    public readonly totalCost: number,
    public readonly actualHours: number,
    public readonly completedBy: string,
    public readonly timestamp: Date = new Date()
  ) {}
}

export class GeofenceViolationEvent {
  constructor(
    public readonly assetId: string,
    public readonly geofenceId: string,
    public readonly violationType: 'entry' | 'exit',
    public readonly position: { latitude: number; longitude: number },
    public readonly timestamp: Date = new Date()
  ) {}
}

Integraciones

GPS Tracking Integration

@Injectable()
export class GPSIntegrationService {
  constructor(
    private readonly trackingService: TrackingService,
    private readonly httpService: HttpService,
    private readonly configService: ConfigService
  ) {}

  // Webhook endpoint for GPS device data
  async processGPSData(payload: GPSDevicePayload): Promise<void> {
    const position = await this.trackingService.recordPosition({
      assetId: payload.deviceId, // mapped to asset
      latitude: payload.lat,
      longitude: payload.lng,
      altitude: payload.alt,
      speed: payload.speed,
      heading: payload.heading,
      timestamp: payload.timestamp
    });

    // Check geofence violations asynchronously
    await this.trackingService.checkGeofenceViolations(
      payload.deviceId,
      { type: 'Point', coordinates: [payload.lng, payload.lat] }
    );
  }
}

Finance Integration

@Injectable()
export class AssetFinanceIntegration {
  @OnEvent('asset.disposed')
  async handleAssetDisposal(event: AssetDisposedEvent): Promise<void> {
    // Create journal entry for asset disposal
    await this.financeService.createJournalEntry({
      type: 'asset_disposal',
      date: new Date(),
      entries: [
        { account: 'accumulated_depreciation', debit: event.accumulatedDepreciation },
        { account: 'cash', debit: event.disposalValue },
        { account: 'fixed_assets', credit: event.originalCost },
        { account: event.gainLoss > 0 ? 'gain_on_disposal' : 'loss_on_disposal',
          credit: event.gainLoss > 0 ? event.gainLoss : 0,
          debit: event.gainLoss < 0 ? Math.abs(event.gainLoss) : 0 }
      ],
      reference: `DISP-${event.assetId}`
    });
  }

  @OnEvent('depreciation.calculated')
  async handleDepreciation(event: DepreciationCalculatedEvent): Promise<void> {
    // Create monthly depreciation journal entry
    await this.financeService.createJournalEntry({
      type: 'depreciation',
      date: event.periodEnd,
      entries: [
        { account: 'depreciation_expense', debit: event.totalDepreciation },
        { account: 'accumulated_depreciation', credit: event.totalDepreciation }
      ],
      reference: `DEP-${event.period}`
    });
  }
}

Referencias


Ultima actualizacion: 2025-12-05