# 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>; // GET /api/v1/assets/:id @Get(':id') async findOne( @Param('id') id: UUID, @CurrentTenant() tenantId: UUID ): Promise; // POST /api/v1/assets @Post() async create( @Body() dto: CreateAssetDto, @CurrentTenant() tenantId: UUID, @CurrentUser() userId: UUID ): Promise; // PUT /api/v1/assets/:id @Put(':id') async update( @Param('id') id: UUID, @Body() dto: UpdateAssetDto, @CurrentTenant() tenantId: UUID ): Promise; // DELETE /api/v1/assets/:id @Delete(':id') async remove( @Param('id') id: UUID, @CurrentTenant() tenantId: UUID ): Promise; // GET /api/v1/assets/categories @Get('categories') async getCategories(@CurrentTenant() tenantId: UUID): Promise; // POST /api/v1/assets/categories @Post('categories') async createCategory( @Body() dto: CreateCategoryDto, @CurrentTenant() tenantId: UUID ): Promise; // GET /api/v1/assets/:id/history @Get(':id/history') async getHistory(@Param('id') id: UUID): Promise; // POST /api/v1/assets/:id/assign @Post(':id/assign') async assignAsset( @Param('id') id: UUID, @Body() dto: AssignAssetDto, @CurrentUser() userId: UUID ): Promise; // POST /api/v1/assets/:id/transfer @Post(':id/transfer') async transferAsset( @Param('id') id: UUID, @Body() dto: TransferAssetDto, @CurrentUser() userId: UUID ): Promise; // POST /api/v1/assets/:id/dispose @Post(':id/dispose') async disposeAsset( @Param('id') id: UUID, @Body() dto: DisposeAssetDto, @CurrentUser() userId: UUID ): Promise; // GET /api/v1/assets/:id/depreciation @Get(':id/depreciation') async getDepreciation(@Param('id') id: UUID): Promise; // 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; } ``` ### 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>; // GET /api/v1/assets/maintenance/schedules/:id @Get('schedules/:id') async getSchedule(@Param('id') id: UUID): Promise; // POST /api/v1/assets/maintenance/schedules @Post('schedules') async createSchedule( @Body() dto: CreateScheduleDto, @CurrentUser() userId: UUID ): Promise; // PUT /api/v1/assets/maintenance/schedules/:id @Put('schedules/:id') async updateSchedule( @Param('id') id: UUID, @Body() dto: UpdateScheduleDto ): Promise; // DELETE /api/v1/assets/maintenance/schedules/:id @Delete('schedules/:id') async deleteSchedule(@Param('id') id: UUID): Promise; // GET /api/v1/assets/maintenance/logs @Get('logs') async getLogs(@Query() query: LogQueryDto): Promise>; // POST /api/v1/assets/maintenance/logs @Post('logs') async createLog( @Body() dto: CreateLogDto, @CurrentUser() userId: UUID ): Promise; // GET /api/v1/assets/maintenance/upcoming @Get('upcoming') async getUpcoming(@Query() query: UpcomingQueryDto): Promise; // GET /api/v1/assets/maintenance/overdue @Get('overdue') async getOverdue(@CurrentTenant() tenantId: UUID): Promise; // 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; } ``` ### 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>; // GET /api/v1/assets/work-orders/:id @Get(':id') async findOne(@Param('id') id: UUID): Promise; // POST /api/v1/assets/work-orders @Post() async create( @Body() dto: CreateWorkOrderDto, @CurrentUser() userId: UUID ): Promise; // PUT /api/v1/assets/work-orders/:id @Put(':id') async update( @Param('id') id: UUID, @Body() dto: UpdateWorkOrderDto ): Promise; // POST /api/v1/assets/work-orders/:id/assign @Post(':id/assign') async assignTechnician( @Param('id') id: UUID, @Body() dto: AssignTechnicianDto ): Promise; // POST /api/v1/assets/work-orders/:id/start @Post(':id/start') async startWork(@Param('id') id: UUID, @CurrentUser() userId: UUID): Promise; // POST /api/v1/assets/work-orders/:id/complete @Post(':id/complete') async completeWork( @Param('id') id: UUID, @Body() dto: CompleteWorkOrderDto, @CurrentUser() userId: UUID ): Promise; // POST /api/v1/assets/work-orders/:id/cancel @Post(':id/cancel') async cancelWork( @Param('id') id: UUID, @Body() dto: CancelWorkOrderDto, @CurrentUser() userId: UUID ): Promise; // GET /api/v1/assets/work-orders/:id/tasks @Get(':id/tasks') async getTasks(@Param('id') id: UUID): Promise; // POST /api/v1/assets/work-orders/:id/tasks @Post(':id/tasks') async addTask( @Param('id') id: UUID, @Body() dto: CreateTaskDto ): Promise; // PUT /api/v1/assets/work-orders/tasks/:taskId @Put('tasks/:taskId') async updateTask( @Param('taskId') taskId: UUID, @Body() dto: UpdateTaskDto ): Promise; // POST /api/v1/assets/work-orders/:id/parts @Post(':id/parts') async addPart( @Param('id') id: UUID, @Body() dto: AddPartDto ): Promise; // POST /api/v1/assets/work-orders/:id/labor @Post(':id/labor') async addLabor( @Param('id') id: UUID, @Body() dto: AddLaborDto ): Promise; } ``` ### 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; // GET /api/v1/assets/tracking/geofences/:id @Get('geofences/:id') async getGeofence(@Param('id') id: UUID): Promise; // POST /api/v1/assets/tracking/geofences @Post('geofences') async createGeofence( @Body() dto: CreateGeofenceDto, @CurrentUser() userId: UUID ): Promise; // PUT /api/v1/assets/tracking/geofences/:id @Put('geofences/:id') async updateGeofence( @Param('id') id: UUID, @Body() dto: UpdateGeofenceDto ): Promise; // DELETE /api/v1/assets/tracking/geofences/:id @Delete('geofences/:id') async deleteGeofence(@Param('id') id: UUID): Promise; // GET /api/v1/assets/tracking/positions @Get('positions') async getCurrentPositions( @Query() query: PositionQueryDto, @CurrentTenant() tenantId: UUID ): Promise; // GET /api/v1/assets/tracking/:assetId/history @Get(':assetId/history') async getTrackingHistory( @Param('assetId') assetId: UUID, @Query() query: TrackingHistoryQueryDto ): Promise; // POST /api/v1/assets/tracking/position @Post('position') async recordPosition( @Body() dto: RecordPositionDto ): Promise; // GET /api/v1/assets/tracking/alerts @Get('alerts') async getGeofenceAlerts(@Query() query: AlertQueryDto): Promise; // GET /api/v1/assets/tracking/map-data @Get('map-data') async getMapData(@CurrentTenant() tenantId: UUID): Promise; } ``` ### 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; // GET /api/v1/assets/reports/depreciation @Get('depreciation') async getDepreciationReport(@Query() query: DepreciationReportQueryDto): Promise; // GET /api/v1/assets/reports/maintenance-cost @Get('maintenance-cost') async getMaintenanceCostReport(@Query() query: MaintenanceCostQueryDto): Promise; // GET /api/v1/assets/reports/utilization @Get('utilization') async getUtilizationReport(@Query() query: UtilizationQueryDto): Promise; // GET /api/v1/assets/reports/lifecycle @Get('lifecycle') async getLifecycleReport(@Query() query: LifecycleQueryDto): Promise; // GET /api/v1/assets/reports/work-order-summary @Get('work-order-summary') async getWorkOrderSummary(@Query() query: WOSummaryQueryDto): Promise; // GET /api/v1/assets/reports/export/:type @Get('export/:type') async exportReport( @Param('type') type: 'inventory' | 'depreciation' | 'maintenance', @Query() query: ExportQueryDto ): Promise; } ``` --- ## Services ### AssetService ```typescript @Injectable() export class AssetService { constructor( @InjectRepository(Asset) private readonly assetRepo: Repository, @InjectRepository(AssetCategory) private readonly categoryRepo: Repository, @InjectRepository(AssetAssignment) private readonly assignmentRepo: Repository, @InjectRepository(AssetMovement) private readonly movementRepo: Repository, private readonly eventEmitter: EventEmitter2 ) {} async findAll(tenantId: UUID, query: AssetQueryDto): Promise>; async findOne(tenantId: UUID, id: UUID): Promise; async create(tenantId: UUID, userId: UUID, dto: CreateAssetDto): Promise; async update(tenantId: UUID, id: UUID, dto: UpdateAssetDto): Promise; async remove(tenantId: UUID, id: UUID): Promise; async assignAsset(assetId: UUID, userId: UUID, dto: AssignAssetDto): Promise; async transferAsset(assetId: UUID, userId: UUID, dto: TransferAssetDto): Promise; async disposeAsset(assetId: UUID, userId: UUID, dto: DisposeAssetDto): Promise; async getHistory(assetId: UUID): Promise; async calculateDepreciationSchedule(assetId: UUID): Promise; async bulkImport(tenantId: UUID, userId: UUID, data: AssetImportRow[]): Promise; } ``` ### MaintenanceService ```typescript @Injectable() export class MaintenanceService { constructor( @InjectRepository(MaintenanceSchedule) private readonly scheduleRepo: Repository, @InjectRepository(MaintenanceLog) private readonly logRepo: Repository, private readonly notificationService: NotificationService, private readonly eventEmitter: EventEmitter2 ) {} async getSchedules(tenantId: UUID, query: ScheduleQueryDto): Promise>; async createSchedule(tenantId: UUID, userId: UUID, dto: CreateScheduleDto): Promise; async updateSchedule(scheduleId: UUID, dto: UpdateScheduleDto): Promise; async deleteSchedule(scheduleId: UUID): Promise; async executeSchedule(scheduleId: UUID, userId: UUID, dto: ExecuteMaintenanceDto): Promise; async getLogs(tenantId: UUID, query: LogQueryDto): Promise>; async createLog(tenantId: UUID, userId: UUID, dto: CreateLogDto): Promise; async getUpcoming(tenantId: UUID, days: number): Promise; async getOverdue(tenantId: UUID): Promise; async calculateNextDueDate(schedule: MaintenanceSchedule): Date; } ``` ### WorkOrderService ```typescript @Injectable() export class WorkOrderService { constructor( @InjectRepository(WorkOrder) private readonly workOrderRepo: Repository, @InjectRepository(WorkOrderTask) private readonly taskRepo: Repository, private readonly eventEmitter: EventEmitter2 ) {} async findAll(tenantId: UUID, query: WorkOrderQueryDto): Promise>; async findOne(tenantId: UUID, id: UUID): Promise; async create(tenantId: UUID, userId: UUID, dto: CreateWorkOrderDto): Promise; async update(workOrderId: UUID, dto: UpdateWorkOrderDto): Promise; async assignTechnician(workOrderId: UUID, dto: AssignTechnicianDto): Promise; async startWork(workOrderId: UUID, userId: UUID): Promise; async completeWork(workOrderId: UUID, userId: UUID, dto: CompleteWorkOrderDto): Promise; async cancelWork(workOrderId: UUID, userId: UUID, dto: CancelWorkOrderDto): Promise; async addTask(workOrderId: UUID, dto: CreateTaskDto): Promise; async updateTask(taskId: UUID, dto: UpdateTaskDto): Promise; async addPart(workOrderId: UUID, dto: AddPartDto): Promise; async addLabor(workOrderId: UUID, dto: AddLaborDto): Promise; async calculateTotalCost(workOrderId: UUID): Promise<{ parts: number; labor: number; total: number }>; } ``` ### TrackingService ```typescript @Injectable() export class TrackingService { constructor( @InjectRepository(GPSTracking) private readonly trackingRepo: Repository, private readonly geofenceService: GeofenceService, private readonly eventEmitter: EventEmitter2 ) {} async getCurrentPositions(tenantId: UUID, query: PositionQueryDto): Promise; async getTrackingHistory(assetId: UUID, query: TrackingHistoryQueryDto): Promise; async recordPosition(dto: RecordPositionDto): Promise; async checkGeofenceViolations(assetId: UUID, position: Point): Promise; async getMapData(tenantId: UUID): Promise; } ``` ### GeofenceService ```typescript @Injectable() export class GeofenceService { constructor( @InjectRepository(Geofence) private readonly geofenceRepo: Repository, private readonly dataSource: DataSource ) {} async findAll(tenantId: UUID): Promise; async findOne(id: UUID): Promise; async create(tenantId: UUID, userId: UUID, dto: CreateGeofenceDto): Promise; async update(id: UUID, dto: UpdateGeofenceDto): Promise; async delete(id: UUID): Promise; async checkPointInGeofence(point: Point, geofenceId: UUID): Promise; async getGeofencesContainingPoint(tenantId: UUID, point: Point): Promise; async getAssetsInGeofence(geofenceId: UUID): Promise; } ``` --- ## 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 ) {} @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 { 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 { // 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 { // 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*