package training import ( "context" "time" "github.com/google/uuid" ) // timeNow is a package-level function for testing var timeNow = time.Now // ComputeAssignments calculates all necessary assignments for a user based on // their roles, existing assignments, and deadlines. Returns new assignments // that need to be created. func ComputeAssignments(ctx context.Context, store *Store, tenantID uuid.UUID, userID uuid.UUID, userName, userEmail string, roleCodes []string, trigger string) ([]TrainingAssignment, error) { if trigger == "" { trigger = string(TriggerManual) } // Get all required modules for the user's roles requiredModules, err := ComputeRequiredModules(ctx, store, tenantID, roleCodes) if err != nil { return nil, err } // Get existing active assignments for this user existingAssignments, _, err := store.ListAssignments(ctx, tenantID, &AssignmentFilters{ UserID: &userID, Limit: 1000, }) if err != nil { return nil, err } // Build a map of existing assignments by module_id for quick lookup existingByModule := make(map[uuid.UUID]*TrainingAssignment) for i := range existingAssignments { a := &existingAssignments[i] // Only consider non-expired, non-completed-and-expired assignments if a.Status != AssignmentStatusExpired { existingByModule[a.ModuleID] = a } } var newAssignments []TrainingAssignment now := timeNow().UTC() for _, module := range requiredModules { existing, hasExisting := existingByModule[module.ID] // Skip if there's an active, valid assignment if hasExisting { switch existing.Status { case AssignmentStatusCompleted: // Check if the completed assignment is still valid if existing.CompletedAt != nil { validUntil := existing.CompletedAt.AddDate(0, 0, module.ValidityDays) if validUntil.After(now) { continue // Still valid, skip } } case AssignmentStatusPending, AssignmentStatusInProgress: continue // Assignment exists and is active case AssignmentStatusOverdue: continue // Already tracked as overdue } } // Determine the role code for this assignment roleCode := "" for _, role := range roleCodes { entries, err := store.GetMatrixForRole(ctx, tenantID, role) if err != nil { return nil, err } for _, entry := range entries { if entry.ModuleID == module.ID { roleCode = role break } } if roleCode != "" { break } } // Calculate deadline based on frequency var deadline time.Time switch module.FrequencyType { case FrequencyOnboarding: deadline = now.AddDate(0, 0, 30) // 30 days for onboarding case FrequencyMicro: deadline = now.AddDate(0, 0, 14) // 14 days for micro default: deadline = now.AddDate(0, 0, 90) // 90 days default } assignment := TrainingAssignment{ TenantID: tenantID, ModuleID: module.ID, UserID: userID, UserName: userName, UserEmail: userEmail, RoleCode: roleCode, TriggerType: TriggerType(trigger), Status: AssignmentStatusPending, Deadline: deadline, ModuleCode: module.ModuleCode, ModuleTitle: module.Title, } // Create the assignment in the store if err := store.CreateAssignment(ctx, &assignment); err != nil { return nil, err } // Log the assignment store.LogAction(ctx, &AuditLogEntry{ TenantID: tenantID, UserID: &userID, Action: AuditActionAssigned, EntityType: AuditEntityAssignment, EntityID: &assignment.ID, Details: map[string]interface{}{ "module_code": module.ModuleCode, "trigger": trigger, "role_code": roleCode, "deadline": deadline.Format(time.RFC3339), }, }) newAssignments = append(newAssignments, assignment) } if newAssignments == nil { newAssignments = []TrainingAssignment{} } return newAssignments, nil } // BulkAssign assigns a module to all users with specific roles // Returns the number of assignments created func BulkAssign(ctx context.Context, store *Store, tenantID uuid.UUID, moduleID uuid.UUID, users []UserInfo, trigger string, deadline time.Time) (int, error) { if trigger == "" { trigger = string(TriggerManual) } count := 0 for _, user := range users { assignment := TrainingAssignment{ TenantID: tenantID, ModuleID: moduleID, UserID: user.UserID, UserName: user.UserName, UserEmail: user.UserEmail, RoleCode: user.RoleCode, TriggerType: TriggerType(trigger), Status: AssignmentStatusPending, Deadline: deadline, } if err := store.CreateAssignment(ctx, &assignment); err != nil { return count, err } count++ } return count, nil } // UserInfo contains basic user information for bulk operations type UserInfo struct { UserID uuid.UUID `json:"user_id"` UserName string `json:"user_name"` UserEmail string `json:"user_email"` RoleCode string `json:"role_code"` }