1132 lines
30 KiB
Markdown
1132 lines
30 KiB
Markdown
# 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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
- [DDL-SPEC-assets.md](../../04-modelado/database-design/schemas/DDL-SPEC-assets.md)
|
|
- [ASSETS-CONTEXT.md](../../04-modelado/domain-models/ASSETS-CONTEXT.md)
|
|
- [EPIC-MAE-015](../../08-epicas/EPIC-MAE-015-assets.md)
|
|
|
|
---
|
|
|
|
*Ultima actualizacion: 2025-12-05*
|