30 KiB
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