diff --git a/composer.json b/composer.json
index 5d78c356e1f3d1189dffdaf807f43c7cc6c2686d..a1a81f7353ae69055ce3940ddbc024bb603da469 100644
--- a/composer.json
+++ b/composer.json
@@ -7,6 +7,19 @@
             "type": "composer",
             "url": "https://packages.drupal.org/8"
         },
+        {
+          "type": "package",
+          "package": {
+            "name": "asc-web-services/asc_courses",
+            "version": "1.0.0",
+            "type": "drupal-module",
+            "source": {
+              "url": "git@code.osu.edu:asc-web-services/asc-courses.git",
+              "type": "git",
+              "reference": "1.0.0"
+            }
+          }
+        },
         {
           "type": "package",
           "package": {
@@ -79,6 +92,7 @@
     ],
     "require": {
         "php": ">=7.3",
+        "asc-web-services/asc_courses": "1.0.0",
         "browserstate/history.js": "1.8",
         "ckeditor/indentblock": "4.8.0",
         "cweagans/composer-patches": "^1.0",
@@ -221,14 +235,16 @@
             "DrupalProject\\composer\\ScriptHandler::createRequiredFiles"
         ],
         "post-package-install": [
-            "rm -rf web/modules/smtp/.git"
+            "rm -rf web/modules/smtp/.git",
+            "rm -rf web/modules/asc_courses/.git"
         ],
         "post-update-cmd": [
             "rm -rf vendor/simplesamlphp/simplesamlphp/config",
             "cp -r config/simplesamlphp/config vendor/simplesamlphp/simplesamlphp/config",
             "rm -f web/simplesaml",
             "ln -s ../vendor/simplesamlphp/simplesamlphp/www web/simplesaml",
-            "DrupalProject\\composer\\ScriptHandler::createRequiredFiles"
+            "DrupalProject\\composer\\ScriptHandler::createRequiredFiles",
+            "rm -rf web/modules/asc_courses/.git"
         ],
         "post-create-project-cmd": [
             "@drupal-scaffold",
diff --git a/composer.lock b/composer.lock
index 98b84e5a807ffa3ed825d45f765f267657ebaf66..68a3a0b80ee85b11eae49d0addaf8213c0e7f4ee 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "cffdbf734b0017b2d55c9510d4c64e54",
+    "content-hash": "acc4e1de572412122fced20a1abe958f",
     "packages": [
         {
             "name": "alchemy/zippy",
@@ -70,6 +70,16 @@
             ],
             "time": "2016-11-03T16:10:31+00:00"
         },
+        {
+            "name": "asc-web-services/asc_courses",
+            "version": "1.0.0",
+            "source": {
+                "type": "git",
+                "url": "git@code.osu.edu:asc-web-services/asc-courses.git",
+                "reference": "1.0.0"
+            },
+            "type": "drupal-module"
+        },
         {
             "name": "asm89/stack-cors",
             "version": "1.3.0",
@@ -13518,5 +13528,6 @@
     "platform-dev": [],
     "platform-overrides": {
         "php": "7.3"
-    }
+    },
+    "plugin-api-version": "1.1.0"
 }
diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE
index f27399a042d95c4708af3a8c74d35d338763cf8f..62ecfd8d0046b60517ea7370300f52744f1ab85d 100644
--- a/vendor/composer/LICENSE
+++ b/vendor/composer/LICENSE
@@ -1,4 +1,3 @@
-
 Copyright (c) Nils Adermann, Jordi Boggiano
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -18,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
-
diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php
index 126478a28269276956b4022aaedb7709a0e40256..fe8678ed14c292b8c03c7a03862bd1263f875bd8 100644
--- a/vendor/composer/autoload_real.php
+++ b/vendor/composer/autoload_real.php
@@ -13,6 +13,9 @@ public static function loadClassLoader($class)
         }
     }
 
+    /**
+     * @return \Composer\Autoload\ClassLoader
+     */
     public static function getLoader()
     {
         if (null !== self::$loader) {
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index 6ba33e8b6106a6fe9f2afb80fc6d114daf129ec9..f156528156390f8cae57b3e57859ecfe2d7c4dfb 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -65,6 +65,18 @@
             "zip"
         ]
     },
+    {
+        "name": "asc-web-services/asc_courses",
+        "version": "1.0.0",
+        "version_normalized": "1.0.0.0",
+        "source": {
+            "type": "git",
+            "url": "git@code.osu.edu:asc-web-services/asc-courses.git",
+            "reference": "1.0.0"
+        },
+        "type": "drupal-module",
+        "installation-source": "source"
+    },
     {
         "name": "asm89/stack-cors",
         "version": "1.3.0",
diff --git a/web/modules/asc_courses/README.md b/web/modules/asc_courses/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..bda1904792cd3ec4e8b7d9754df27bedd73ababe
--- /dev/null
+++ b/web/modules/asc_courses/README.md
@@ -0,0 +1 @@
+Drupal 8 re-implementation of the 'asc_courses' module
diff --git a/web/modules/asc_courses/asc_courses.info.yml b/web/modules/asc_courses/asc_courses.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6baa0d7eb89532046e471ea1b31679ef2368ee90
--- /dev/null
+++ b/web/modules/asc_courses/asc_courses.info.yml
@@ -0,0 +1,15 @@
+name: ASC Courses
+description: Import Course information from KMData's replacement
+package: Custom
+
+type: module
+core: 8.x
+
+php: 7.2
+
+dependencies:
+  - migrate
+  - migrate_drupal
+  - migrate_plus
+  - migrate_tools
+  
\ No newline at end of file
diff --git a/web/modules/asc_courses/asc_courses.install b/web/modules/asc_courses/asc_courses.install
new file mode 100644
index 0000000000000000000000000000000000000000..e25d49e9d3c4517064e8441baebe20833d05816c
--- /dev/null
+++ b/web/modules/asc_courses/asc_courses.install
@@ -0,0 +1,163 @@
+<?php
+
+/**
+ * @file
+ * Installation functions for ASC Courses module
+ */
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\node\NodeInterface;
+use Drupal\taxonomy\TermInterface;
+use Drupal\user\RoleInterface;
+
+
+
+function asc_courses_update_8001(&$sandbox) {
+  $content_type = 'course';
+
+  /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
+  $entity_type_manager = \Drupal::entityTypeManager(); 
+
+  /** @var \Drupal\Core\Entity\EntityStorageInterface $node_storage */
+  $node_storage = $entity_type_manager->getStorage('node');
+
+  if (!isset($sandbox['total'])) {
+    $sandbox['current'] = 0;
+    $sandbox['count'] = 0;
+    $sandbox['total'] = $node_storage->getQuery()
+      ->accessCheck(FALSE)
+      ->condition('type', $content_type)
+      ->count()->execute();
+  }
+
+  $nids = $node_storage->getQuery()
+    ->accessCheck(FALSE)
+    ->range(0, 50)
+    ->condition('type', $content_type)
+    ->condition('nid', $sandbox['current'], '>')
+    ->execute();
+
+  $nodes = $node_storage->loadMultiple($nids);
+
+  /** @var \Drupal\node\NodeInterface $node */
+  foreach ($nodes as $node) {
+
+    $node_body = $node->body->value;
+    $node->field_course_description->value = $node_body;
+
+    $node->setNewRevision();
+    $node->save();
+
+    $sandbox['current'] = $node->id();
+    $sandbox['count']++;
+  }
+  $sandbox['#finished'] = empty($sandbox['total']) ? 1 : $sandbox['current'] / $sandbox['total'];
+}
+
+/**
+ * Implements hook_schema().
+ *
+ * This defines the database table which will hold the example item info.
+ *
+ * @ingroup tabledrag_example
+ */
+function asc_courses_schema() {
+  $schema['asc_course_data'] = [
+    'description' => 'Stores courses json data.',
+    'fields' => [
+      'id' => [
+        'description' => 'The primary identifier for each item',
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ],
+      'date' => [
+        'description' => 'The date this data was fetched',
+        'type' => 'int',
+        'length' => 11,
+        'not_null' => TRUE,
+      ],
+      'dept_org' => [
+        'description' => 'D_ORG number used in request',
+        'type' => 'varchar',
+        'length' => 8,
+        'not_null' => TRUE
+      ],
+      'raw_json' => [
+        'description' => 'Verbatim JSON blob from EIP API',
+        'type' => 'text',
+        'length' => 16777215,
+        'not_null' => TRUE
+      ],
+    //   'name' => [
+    //     'description' => 'A name for this item',
+    //     'type' => 'varchar',
+    //     'length' => 32,
+    //     'not null' => TRUE,
+    //     'default' => '',
+    //   ],
+    //   'description' => [
+    //     'description' => 'A description for this item',
+    //     'type' => 'varchar',
+    //     'length' => 255,
+    //     'not null' => TRUE,
+    //     'default' => '',
+    //   ],
+    //   'itemgroup' => [
+    //     'description' => 'The group this item belongs to',
+    //     'type' => 'varchar',
+    //     'length' => 32,
+    //     'not null' => TRUE,
+    //     'default' => '',
+    //   ],
+    //   'weight' => [
+    //     'description' => 'The sortable weight for this item',
+    //     'type' => 'int',
+    //     'length' => 11,
+    //     'not null' => TRUE,
+    //     'default' => 0,
+    //   ],
+    //   'pid' => [
+    //     'description' => 'The primary id of the parent for this item',
+    //     'type' => 'int',
+    //     'length' => 11,
+    //     'unsigned' => TRUE,
+    //     'not null' => TRUE,
+    //     'default' => 0,
+    //   ],
+    ],
+    'primary key' => ['id'],
+  ];
+  return $schema;
+}
+
+// /**
+//  * Implements hook_install().
+//  *
+//  * Populates newly created database table with fixtures for all module's
+//  * examples. This hook executed after hook_schema() tables are created by core.
+//  *
+//  * @see \Drupal\Core\Extension\ModuleInstaller::install()
+//  *
+//  * @ingroup tabledrag_example
+//  */
+// function tabledrag_example_install() {
+//   /** @var \Drupal\Core\Database\Connection $connection */
+//   $connection = \Drupal::database();
+//   $rows = Fixtures::getSampleItems();
+//   foreach ($rows as $row) {
+//     $connection->insert('tabledrag_example')->fields($row)->execute();
+//   }
+// }
+
+
+
+
+/**
+ * implements hook_install()
+ */
+// function asc_courses_install() {
+
+// }
+
diff --git a/web/modules/asc_courses/asc_courses.links.menu.yml b/web/modules/asc_courses/asc_courses.links.menu.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cdec8e392d56210f32e49a215f5ea54ffe4d3b0a
--- /dev/null
+++ b/web/modules/asc_courses/asc_courses.links.menu.yml
@@ -0,0 +1,11 @@
+asc_courses.settings:
+  title: 'ASC Courses Settings'
+  description: 'Configure settings for the ASC Courses module'
+  route_name: asc_courses.settings
+  parent: 'system.admin_config_content'
+
+asc_courses.api_settings:
+  title: 'ASC Courses Admin (links.menu.yml)'
+  description: 'Administrate API settings (links.menu.yml)'
+  route_name: asc_courses.api_settings
+  parent: 'asc_courses.settings'
diff --git a/web/modules/asc_courses/asc_courses.links.task.yml b/web/modules/asc_courses/asc_courses.links.task.yml
new file mode 100644
index 0000000000000000000000000000000000000000..83bde4d42fd27d984d7ae72e589ebb1816369826
--- /dev/null
+++ b/web/modules/asc_courses/asc_courses.links.task.yml
@@ -0,0 +1,11 @@
+asc_courses.settings:
+  title: 'Course Selection'
+  description: 'Configure ASC Courses module backend'
+  route_name: asc_courses.settings
+  base_route: asc_courses.settings
+
+asc_courses.api_settings:
+  title: 'API'
+  description: 'Configure ASC Courses API backend (links.task.yml)'
+  route_name: asc_courses.api_settings
+  base_route: asc_courses.settings
diff --git a/web/modules/asc_courses/asc_courses.module b/web/modules/asc_courses/asc_courses.module
new file mode 100644
index 0000000000000000000000000000000000000000..7af0d0adf66591c7a37fd71d87282d2e389fc69c
--- /dev/null
+++ b/web/modules/asc_courses/asc_courses.module
@@ -0,0 +1,24 @@
+<?php
+
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * implements hook_help()
+ */
+function asc_courses_help($route_name, RouteMatchInterface $route_match) {
+  switch($route_name) {
+    case 'help.page.asc_courses':
+      return t('
+        <2>ASC Courses Module</h2>
+      ');
+  }
+}
+
+function asc_courses_form_alter(&$form, &$form_state, $form_id) {
+  if(in_array($form_id,['node_course_form','node_course_edit_form'])) {
+    $form['field_course_description']['#disabled'] = 'disabled';
+    $form['field_credit_hours']['#disabled'] = 'disabled';
+    $form['field_subject_abbreviation']['#disabled'] = 'disabled';
+    $form['field_course_number']['#disabled'] = 'disabled';
+  }
+}
diff --git a/web/modules/asc_courses/asc_courses.permissions.yml b/web/modules/asc_courses/asc_courses.permissions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..48eb6376918450940594dca287df54443476e896
--- /dev/null
+++ b/web/modules/asc_courses/asc_courses.permissions.yml
@@ -0,0 +1,5 @@
+manage courses:
+  title: 'Manage ASC Courses'
+
+admin courses:
+  title: 'Manage ASC Courses API settings'
diff --git a/web/modules/asc_courses/asc_courses.routing.yml b/web/modules/asc_courses/asc_courses.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8330ca8014f50ad99c40656af0a99b84e4fa606b
--- /dev/null
+++ b/web/modules/asc_courses/asc_courses.routing.yml
@@ -0,0 +1,26 @@
+asc_courses.content:
+  path: '/courses'
+  defaults:
+    _controller: '\Drupal\asc_courses\Controller\CoursesController::content'
+    _title: 'Hello World'
+  requirements:
+    _permission: 'access content'
+
+asc_courses.settings:
+  path: '/admin/config/content/asc-courses'
+  defaults:
+    _form: 'Drupal\asc_courses\Form\SettingsForm'
+    _title: 'ASC Courses Settings (routing)'
+    link_id: 'asc_courses.settings'
+  requirements:
+    _permission: 'manage courses'
+
+asc_courses.api_settings:
+  path: '/admin/config/content/asc-courses/api_settings'
+  defaults:
+    _form: 'Drupal\asc_courses\Form\ApiSettingsForm'
+    _title: 'ASC Courses API (routing)'
+    link_id: 'asc_courses.api_settings'
+  requirements:
+    _permission: 'admin courses'
+    
\ No newline at end of file
diff --git a/web/modules/asc_courses/asc_courses.schema.yml b/web/modules/asc_courses/asc_courses.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a09c753dd873f0fcb1d372eeab154ff7bb0d85a3
--- /dev/null
+++ b/web/modules/asc_courses/asc_courses.schema.yml
@@ -0,0 +1,26 @@
+asc_courses.settings:
+  type: config_object
+  label: 'ASC Courses Settings'
+  mapping:
+    asc_courses:
+      type: mapping
+      mapping:
+        qa_consumer_key:
+          type: text
+          label: 'QA Consumer Key'
+        qa_consumer_secret:
+          type: text
+          label: 'QA Consumer Secret'
+        prod_consumer_key:
+          type: text
+          label: 'Production Consumer Key'
+        prod_consumer_secret:
+          type: text
+          label: 'Production Consumer Secret'
+        dept_org:
+          type: text
+          label: 'D-Org number (e.g. D1435)'
+        eip_environment:
+          type: text
+          label: 'EIP Environment (schema)'
+
diff --git a/web/modules/asc_courses/composer.json b/web/modules/asc_courses/composer.json
new file mode 100644
index 0000000000000000000000000000000000000000..9002e32879b01afed303550d441fcbc469999f57
--- /dev/null
+++ b/web/modules/asc_courses/composer.json
@@ -0,0 +1,21 @@
+{
+    "name": "org/asc_courses",
+    "description": "This extension provides new commands for Drush.",
+    "type": "drupal-drush",
+    "authors": [
+        {
+            "name": "Brian Weaver",
+            "email": "weaver.299@osu.edu"
+        }
+    ],
+    "require": {
+        "php": ">=5.6.0"
+    },
+    "extra": {
+        "drush": {
+            "services": {
+                "drush.services.yml": "^9"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/web/modules/asc_courses/config/install/asc_courses.settings.yml b/web/modules/asc_courses/config/install/asc_courses.settings.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7fd759c596c3faf8496fa4b2435f3feb764c96f3
--- /dev/null
+++ b/web/modules/asc_courses/config/install/asc_courses.settings.yml
@@ -0,0 +1,7 @@
+asc_courses:
+  qa_consumer_key: 'asdf1'
+  qa_consumer_secret: "asdf2"
+  prod_consumer_key: 'asdf3'
+  prod_consumer_secret: "asdf4"
+  eip_environment: "qa"
+  
\ No newline at end of file
diff --git a/web/modules/asc_courses/config/install/migrate_plus.migration.asc_courses.yml b/web/modules/asc_courses/config/install/migrate_plus.migration.asc_courses.yml
new file mode 100644
index 0000000000000000000000000000000000000000..80d56886dd852ee6e61d19bf1bafdd584e8863fe
--- /dev/null
+++ b/web/modules/asc_courses/config/install/migrate_plus.migration.asc_courses.yml
@@ -0,0 +1,61 @@
+id: asc_courses
+label: Import Courses
+
+
+migration_tags:
+  - Custom
+  - ASC
+
+source:
+  plugin: url
+  data_fetcher_plugin: http
+  headers:
+    Accept: application/json
+    Authorization: Bearer ffec70c4-fc10-3d76-a008-af0936ece23d
+  data_parser_plugin: json
+  urls: 
+    - https://apig-qa.eip.osu.edu/crseinfo/1.0.0/getCatalogInfo?campus=COL&acad_org=D0537
+  item_selector: getCourseCatalogResponse/catalog/course
+  fields:
+    - 
+      name: crse_id
+      label: 'Course ID'
+      selector: crse-id
+    - 
+      name: course_name
+      label: 'Course Name'
+      selector: course-title-long
+    - 
+      name: description
+      label: 'Course Description'
+      selector: descrlong
+    -
+      name: credit_hours
+      label: 'Credit Hours'
+      selector: acad-prog
+    -
+      name: course_number
+      label: 'Course Number'
+      selector: catalog-nbr
+    -
+      name: subject_abbreviation
+      label: 'Subject Abbreviation'
+      selector: subject
+  ids:
+    crse_id:
+      type: integer
+
+destination:
+  plugin: entity:node
+
+process:
+  type:
+    plugin: default_value
+    default_value: course
+  
+  title: course_name
+  body: description
+  field_course_number: course_number
+  field_credit_hours: credit_hours
+  field_subject_abbreviation: subject_abbreviation
+
diff --git a/web/modules/asc_courses/config/schema/asc_courses.schema.yml b/web/modules/asc_courses/config/schema/asc_courses.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..688d53297bba322621df4fe2f07c4b9402018559
--- /dev/null
+++ b/web/modules/asc_courses/config/schema/asc_courses.schema.yml
@@ -0,0 +1,14 @@
+asc_courses.settings:
+  type: config_object
+  label: 'ASC Courses Settings'
+  mapping:
+    asc_courses:
+      type: mapping
+      mapping:
+        consumer_key:
+          type: text
+          label: 'Consumer Key'
+        consumer_secret:
+          type: text
+          label: 'Consumer Secret'
+          
diff --git a/web/modules/asc_courses/drush.services.yml b/web/modules/asc_courses/drush.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..61f584f91e2416b3eb113c66780d1778c39f1411
--- /dev/null
+++ b/web/modules/asc_courses/drush.services.yml
@@ -0,0 +1,5 @@
+services:
+  asc_courses.commands:
+    class: \Drupal\asc_courses\Commands\AscCoursesCommands
+    tags:
+      - { name: drush.command }
diff --git a/web/modules/asc_courses/src/AscCoursesApi.php b/web/modules/asc_courses/src/AscCoursesApi.php
new file mode 100644
index 0000000000000000000000000000000000000000..ebc6c7b6f8e0cc9c814ab96c1d74ecaed676864c
--- /dev/null
+++ b/web/modules/asc_courses/src/AscCoursesApi.php
@@ -0,0 +1,271 @@
+<?php
+namespace Drupal\asc_courses;
+
+// use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
+// use Drush\Commands\DrushCommands;
+use \Drupal\Core\Database\Database;
+use \Drupal\Core\Config;
+
+class AscCoursesApi {
+  protected $debug = 0;
+  protected $consumer_key;
+  protected $consumer_secret;
+  public $config;
+  public $soip_constant_name;
+  public $environment;
+  public $base_url;
+  public $access_token;
+  public $access_token_data;
+  public $resolve_host;
+
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct($config = null, $environment = '') {
+    // Get settings
+    if (!empty($config)) {
+      $this->config = $config;
+    }
+    else {
+      $this->config = $config = \Drupal::service('config.factory')->get('asc_courses.settings');
+    }
+    
+    if ($this->debug) echo "\nAPI connector class instantiated\n";
+
+    // Get environment if not overridden
+    if(empty($environment)) {
+      if ($this->debug) echo "Environment argument was empty. Reading it from configuration.\n";
+      $environment = $this->config->get('asc_courses.eip_environment');
+    }
+    if ($this->debug) echo "Environment: " . $environment . "\n";
+
+    // HARD CODED VALUES
+    if ($environment == 'prod') {
+      $this->environment = $environment;
+      $this->base_url = "https://apig.eip.osu.edu/";
+      $this->consumer_key = $config->get('asc_courses.prod_consumer_key');
+      $this->consumer_secret = $config->get('asc_courses.prod_consumer_secret');
+      $this->soip_constant_name = "PANTHEON_SOIP_EIP_PROD";
+    }
+    else {
+      $this->environment = $environment;
+      $this->base_url = "https://apig-qa.eip.osu.edu/";
+      $this->consumer_key = $config->get('asc_courses.qa_consumer_key');
+      $this->consumer_secret = $config->get('asc_courses.qa_consumer_secret');
+      $this->soip_constant_name = "PANTHEON_SOIP_EIP";  
+    }
+
+    if(isset($_ENV['PANTHEON_ENVIRONMENT']) && $_ENV['PANTHEON_ENVIRONMENT'] != "lando") {
+      if ($this->debug) echo "Pantheon environment: " . $_ENV['PANTHEON_ENVIRONMENT'] . "\n";
+      $host = parse_url($this->base_url, PHP_URL_HOST);
+      if ($this->debug) echo "host: $host\n";
+      $localhost = "127.0.0.1";
+      $this->resolve_host = array(sprintf("%s:%d:%s", $host, constant($this->soip_constant_name), $localhost));
+      if ($this->debug) echo "\n resolve_host : " . implode($this->resolve_host) . "\n";
+      // error_log("resolve_host : " . implode($this->resolve_host));
+    }
+
+    if ($this->debug) echo "\n\n";
+  }
+
+  /**
+   * Return an access token if possible
+   */
+  public function getAccessToken() {
+    if($this->debug) echo "============ getAccessToken =============\n";
+    $now = time();
+    if($this->debug) echo "Current time: " . date("Y-m-d h:i:s", $now) . "\n";
+
+    // First check object variables
+    if(!empty($this->access_token_data) 
+       && (($this->access_token_data->expiration - $now) > 30)) 
+    {
+      if($this->debug) echo "Object variables were set and not stale\n";
+      $this->access_token = $this->access_token_data->access_token;        
+    }
+    else {
+      // fetch configuration
+      $env_access_token_setting = 'asc_courses.' . $this->environment . '_access_token';
+      if($this->debug) echo "env_access_token_setting: $env_access_token_setting\n";
+      $access_token_data = unserialize($this->config->get($env_access_token_setting));
+      if($this->debug) echo "Config access token: " . print_r($access_token_data, true) . "\n";
+
+      if(!empty($access_token_data)
+         && (($access_token_data->expiration - $now) > 30)) 
+      {
+        if($this->debug) echo "Config token was available and not stale.\n";
+        $this->access_token_data = $access_token_data;
+        $this->access_token = $access_token_data->access_token;
+      }
+      else {
+        if($this->debug) echo "Config token was unset or stale.\n";
+        $access_token_data = $this->fetchAccessToken();
+
+        if(!empty($access_token_data->access_token)) {
+          $this->access_token_data = $access_token_data;
+          $this->access_token = $access_token_data->access_token;
+        }
+      }
+    }
+
+    if (!empty($this->access_token_data)) {
+      $expirey = $this->access_token_data->expiration;
+      if($this->debug) {
+        echo "Token expiration: " . date("Y-m-d h:i:s", $expirey) . "\n";
+        echo "Expires in " . ($expirey - $now) . " seconds..\n";
+        echo "\n\n";  
+      }
+      return $this->access_token;    
+    }
+    else {
+      die("\ngetAccessToken() - failed to retrieve access token! X_X\n\n");
+    }
+  }
+
+  /**
+   * Fetch a new access token via the API and return it
+   */
+  protected function fetchAccessToken() {
+    // get an access token
+    $bearer_auth_plain = $this->consumer_key . ":" . $this->consumer_secret;
+    if ($this->debug) echo "fetchAccessToken() - Tokens: " . $bearer_auth_plain . "\n";
+    $bearer_auth = base64_encode($bearer_auth_plain);
+    if ($this->debug) echo "fetchAccessToken() - Bearer auth: $bearer_auth\n";
+
+    $access_token_url = $this->base_url . "token?grant_type=client_credentials";
+    if ($this->debug) echo "fetchAccessToken() - Access token URL: $access_token_url\n";
+    $access_token_headers = [
+      "Accept: application/json",
+      "Authorization: Basic $bearer_auth",
+    ];
+
+    $ch = curl_init();
+    curl_setopt($ch, CURLOPT_URL, $access_token_url);
+
+    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
+    curl_setopt($ch, CURLOPT_POST, true);
+    // curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
+    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+    curl_setopt($ch, CURLOPT_HTTPHEADER, $access_token_headers);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
+
+    if(isset($this->resolve_host)) {
+      if ($this->debug) echo "fetchAccessToken() - Resolve host set: " . print_r($this->resolve_host, true) . "\n";
+      curl_setopt($ch, CURLOPT_RESOLVE, $this->resolve_host);
+      curl_setopt($ch, CURLOPT_PORT, constant($this->soip_constant_name));  
+    }
+
+    if($this->debug > 1) {
+      curl_setopt($ch, CURLOPT_VERBOSE, true);
+    }
+
+    $access_token_start = microtime(true);
+    $access_token_result = curl_exec($ch);
+    $access_token_error = curl_error($ch);
+    curl_close($ch);
+    $access_token_finish = microtime(true);
+    $access_token_seconds = $access_token_finish - $access_token_start;
+    if ($this->debug) echo "fetchAccessToken() - CURL error: " . print_r($access_token_error, true) . "\n";
+
+    if(!empty($access_token_error) || empty($access_token_result)) {
+      echo "\n$access_token_error\n";
+      die("fetchAccessToken() - Failed to fetch access token from the API!!  X_X\n\n");
+    }
+    else {
+      $now = time();
+      $access_token_data = json_decode($access_token_result);
+      $access_token_data->expiration = (time() + $access_token_data->expires_in);
+      $access_token_data->fetched = $now;
+  
+      if ($this->debug) {
+        echo "fetchAccessToken() - Access token result: \n";
+        print_r($access_token_result);
+        echo "\n";
+        echo "fetchAccessToken() - Access token in $access_token_seconds seconds\n";  
+        echo "fetchAccessToken() - Now: " . $now . " - " . date("Y-m-d h:i:s", $now) . "\n";
+        print_r($access_token_data);
+      }
+  
+      // Save access token data to config
+      $env_access_token_setting = 'asc_courses.' . $this->environment . '_access_token';
+      if($this->debug) echo "fetchAccessToken() - env_access_token_setting: $env_access_token_setting\n";
+      $config = \Drupal::service('config.factory')->getEditable('asc_courses.settings');
+      $config->set("asc_courses." . $this->environment . "_access_token", serialize($access_token_data));
+      $config->save();
+  
+      return $access_token_data;
+    }
+  }
+
+  /**
+   * Fetch and store courses for a single D-Org
+   */
+  public function fetchSubjectCourses($dorg) {
+    $access_token = $this->getAccessToken();
+    $course_data_url = $this->base_url . "crseinfo/1.0.0/getCatalogInfo?campus=COL&acad_org=$dorg";
+
+    if($this->debug) echo "course data url: $course_data_url\n";
+    $course_data_headers = [
+      "Accept: application/json",
+      "Authorization: Bearer $access_token"
+    ];
+
+
+
+    $ch = curl_init();
+    curl_setopt($ch, CURLOPT_URL, $course_data_url);
+
+    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
+    // curl_setopt($ch, CURLOPT_POST, true);
+    // curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
+    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+    curl_setopt($ch, CURLOPT_HTTPHEADER, $course_data_headers);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
+
+    if(isset($this->resolve_host)) {
+      if ($this->debug) echo "fetchAccessToken() - Resolve host set: " . $this->resolve_host . "\n";
+      // error_log("fetchAccessToken() - Resolve host set: " . $this->resolve_host);
+      curl_setopt($ch, CURLOPT_RESOLVE, $this->resolve_host);
+      curl_setopt($ch, CURLOPT_PORT, constant($this->soip_constant_name));  
+    }
+
+    if($this->debug > 1) {
+      curl_setopt($ch, CURLOPT_VERBOSE, true);
+    }
+
+    $curl_start = microtime(true);
+    $course_data_result = curl_exec($ch);
+    $curl_error = curl_error($ch);
+    curl_close($ch);
+    $curl_finish = microtime(true);
+    $curl_seconds = $curl_finish - $curl_start;
+    if ($this->debug) echo "fetchSubjectCourses() - CURL error: " . print_r($curl_error, true) . "\n";
+
+    if(!empty($curl_error) || empty($course_data_result)) {
+      echo "\n$curl_error\n";
+      die("fetchSubjectCourses() - Failed to fetch [$dorg] course information from the API!!  X_X\n\n");
+    }
+    else {
+      if ($this->debug) echo "Course data length:" . strlen($course_data_result) . "\n";
+      if ($this->debug) echo "Course data in $curl_seconds seconds\n\n";
+  
+  
+      $connection = \Drupal::database();
+      $row = [
+        'date' => time(),
+        'dept_org' => $dorg,
+        'raw_json' => $course_data_result
+      ];
+      $connection->insert('asc_course_data')->fields($row)->execute();
+  
+      $course_data = json_decode($course_data_result);      
+      return $course_data;
+    }
+
+
+  }
+
+}
diff --git a/web/modules/asc_courses/src/AscCoursesImporter.php b/web/modules/asc_courses/src/AscCoursesImporter.php
new file mode 100644
index 0000000000000000000000000000000000000000..3e798236ff1ed093554eb2d5f3201f8b7c5c6f7c
--- /dev/null
+++ b/web/modules/asc_courses/src/AscCoursesImporter.php
@@ -0,0 +1,213 @@
+<?php
+namespace Drupal\asc_courses;
+
+// use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
+// use Drush\Commands\DrushCommands;
+use \Drupal\Core\Database\Database;
+use \Drupal\Core\Config;
+use \Drupal\node\Entity\Node;
+
+class AscCoursesImporter {
+  public $debug = 0;
+  public $config;
+  public $api;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct($config = null) {
+    // Get settings
+    if (!empty($config)) {
+      $this->config = $config;
+    }
+    else {
+      $this->config = $config = \Drupal::service('config.factory')->get('asc_courses.settings');
+    }
+  }
+
+  public function importCourseNodes($courses_data) {
+    if($this->debug) echo "importCourseNodes()\n";
+    $i = 0;
+    foreach($courses_data->getCourseCatalogResponse->catalog->course as $course_data) {
+      $node_storage = \Drupal::entityTypeManager()->getStorage('node');
+      // if($this->debug) echo "$i catalog-nbr: " . $course_data->{'catalog-nbr'} . "\n";
+      if($i < 13) {
+        // if($this->debug > 1) echo "importCourseNodes() - " . print_r($course_data, true);
+
+        // look up existing node
+        $node_query = \Drupal::entityQuery('node')
+          ->condition('field_eip_id', $course_data->{'crse-id'}, '=');
+        $existing_node = $node_query->execute();
+
+        if(empty($existing_node)) {
+          $this->createCourseNode($node_storage, $course_data);
+        }
+        else {
+          // update existing node
+          $existing_nid = array_shift($existing_node);
+          $this->updateCourseNode($node_storage, $existing_nid, $course_data);
+          // if($this->debug) echo "\n";
+        }
+        // $i++;
+      }
+    }
+  }
+
+  public function createCourseNode($node_storage, $course_data) {
+    if($this->debug) echo "createCourseNode() - create new node\n";
+    // $course_node = Node::create([
+    $course_node = $node_storage->create(array(
+      'type'                        => 'course',
+      'title'                       => $course_data->{'course-title-long'},
+      'field_course_description'    => [$course_data->{'descrlong'}],
+      'field_course_number'         => [$course_data->{'catalog-nbr'}],
+      'field_credit_hours'          => [$course_data->{'acad-prog'}],
+      'field_subject_abbreviation'  => [$course_data->{'subject'}],
+      'field_eip_id'                => [$course_data->{'crse-id'}],
+    ));
+    $course_node->save();
+    // $new_course_nid = $course_node->get('nid')->getValue()[0]['value'];
+    $new_course_nid = $course_node->id();
+    // if($this->debug) echo "createCourseNode() - $new_course_nid\n";
+    if($this->debug) echo "createCourseNode() - New course nid[$new_course_nid] catalog[" . $course_data->{'catalog-nbr'} . "]\n";
+  }
+
+  public function updateCourseNode($node_storage, $nid, $course_data) {
+    if($this->debug) echo "updateCourseNode() - Existing nid: $nid\n";
+
+    $course_node = $node_storage->load($nid);
+
+    // Compare values and update if necessary
+    $content_changed = false;
+
+    // $title = $course_node->get('title')->getValue()[0]['value'];
+    // echo "title: " . print_r($title, true);
+    $title = $course_node->title->value;
+    if ($title != $course_data->{'course-title-long'}) {
+      if($this->debug) {
+        echo "### Title changed..\n";
+        echo "Node title: $title\n";
+        echo "API title: " . $course_data->{'course-title-long'} . "\n";  
+      }
+      $content_changed = true;
+      // $course_node->setTitle($course_data->{'course-title-long'});
+      $course_node->title->value = $course_data->{'course-title-long'};
+    }
+
+    $descrption = $course_node->field_description->value;
+    // echo "descrption: $descrption\n";
+    if ($descrption != $course_data->{'descrlong'}) {
+      if($this->debug) {
+        echo "### descrption changed..\n";
+        echo "Node descrption: $descrption\n";
+        echo "API descrption: " . $course_data->{'descrlong'} . "\n";  
+      }
+      $content_changed = true;
+      $course_node->descrption->value = $course_data->{'descrlong'};
+    }
+
+    // $course_number = $course_node->get('field_course_number')->getValue()[0]['value'];
+    // echo "course_number: " . print_r($course_number, true);
+    $course_number = $course_node->field_course_number->value;
+    if ($course_number != $course_data->{'catalog-nbr'}) {
+      if($this->debug) {
+        echo "### course_number changed..\n";
+        echo "Node course_number: $course_number\n";
+        echo "API course_number: " . $course_data->{'catalog-nbr'} . "\n";  
+      }
+      $content_changed = true;
+      // $course_node->set('field_course_number', $course_data->{'catalog-nbr'});
+      $course_node->field_course_number->value = $course_data->{'catalog-nbr'};
+    }
+
+    $credit_hours = $course_node->field_credit_hours->value;
+    if ($credit_hours != $course_data->{'acad-prog'}) {
+      if($this->debug) {
+        echo "### credit_hours changed..\n";
+        echo "Node credit_hours: $credit_hours\n";
+        echo "API credit_hours: " . $course_data->{'acad-prog'} . "\n";  
+      }
+      $content_changed = true;
+      $course_node->field_credit_hours->value = $course_data->{'acad-prog'};
+    }
+
+    $subj_abbrev = $course_node->field_subject_abbreviation->value;
+    if ($subj_abbrev != $course_data->{'subject'}) {
+      if($this->debug) {
+        echo "### subj_abbrev changed..\n";
+        echo "Node subj_abbrev: $subj_abbrev\n";
+        echo "API subj_abbrev: " . $course_data->{'subject'} . "\n";  
+      }
+      $content_changed = true;
+      $course_node->field_subject_abbreviation->value = $course_data->{'subject'};
+    }
+
+    if($content_changed) {
+      if($this->debug) echo "\nContent has changed.. save updated node.\n\n";
+      $course_node->save();
+    }
+  }
+
+
+  public function fetchAndImportAll() {
+    // $this->config = \Drupal::service('config.factory')->get('asc_courses.settings');
+    if(!isset($this->api)) $this->api = new AscCoursesApi($this->config);
+
+    $config_dorgs = $this->config->get('asc_courses.dept_org');
+
+    $dorgs = explode(',', trim($config_dorgs));
+
+    foreach($dorgs as $dorg) {
+      $dorg = trim($dorg);
+      if ($this->debug) echo "D-org: $dorg\n";
+      $db_data = $this->loadSubjectDataFromDatabase($dorg);
+      if(empty($db_data)) {
+        $courses_data = $this->api->fetchSubjectCourses($dorg);
+      }
+      else {
+        $courses_data = $db_data;
+      }
+      
+      $this->importCourseNodes($courses_data);
+    }
+  }
+
+  public function loadSubjectDataFromDatabase($dorg) {
+    $connection = \Drupal::database();
+    $course_info_query = $connection->select('asc_course_data', 'acd')
+      // ->fields('acd', array('id', 'date', 'dept_org'))
+      ->fields('acd')
+      ->condition('dept_org', $dorg, "=")
+      ->orderBy('date', 'DESC')
+      ->range(0, 1);
+    $course_info_result = $course_info_query->execute();
+    // $result_count = $course_info_result->rowCount();
+    // echo "Rows: $result_count\n";
+    // $rows = $course_info_result->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+    if($row = $course_info_result->fetchAssoc()) {
+      // print_r($row);
+      $data_date = $row['date'];
+      $data_age = time() - $data_date;
+      if($this->debug) echo "Data date: $data_date - " . date("Y-m-d h:i:s", $data_date) . " - $data_age seconds old\n";
+
+      if($data_age < 3600) {
+        if($this->debug) echo "Data is less than an hour old.. reusing data from database.\n";
+        $courses_json = $row['raw_json'];
+        // $len = strlen($courses_json);
+        if($this->debug) echo "JSON Length:" . strlen($courses_json) . "\n";
+        // echo substr($courses_json, 0, 1024) . "\n\n";
+        $json_data = json_decode($courses_json);
+        // print_r($json_data);
+        return $json_data;
+      }
+      else {
+        return false;
+      }
+    }
+    else {
+      return false;
+    }
+  }
+
+
+}
diff --git a/web/modules/asc_courses/src/Commands/AscCoursesCommands.php b/web/modules/asc_courses/src/Commands/AscCoursesCommands.php
new file mode 100644
index 0000000000000000000000000000000000000000..57ea6a862c3f3939b6bed742f40aed6249d701a4
--- /dev/null
+++ b/web/modules/asc_courses/src/Commands/AscCoursesCommands.php
@@ -0,0 +1,394 @@
+<?php
+
+namespace Drupal\asc_courses\Commands;
+
+use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
+use Drush\Commands\DrushCommands;
+use \Drupal\node\Entity\Node;
+
+use \Drupal\asc_courses\AscCoursesApi;
+use \Drupal\asc_courses\AscCoursesImporter;
+
+
+/**
+ * A Drush commandfile.
+ *
+ * In addition to this file, you need a drush.services.yml
+ * in root of your module, and a composer.json file that provides the name
+ * of the services file to use.
+ *
+ * See these files for an example of injecting Drupal services:
+ *   - http://cgit.drupalcode.org/devel/tree/src/Commands/DevelCommands.php
+ *   - http://cgit.drupalcode.org/devel/tree/drush.services.yml
+ */
+class AscCoursesCommands extends DrushCommands {
+
+  public $soip_constant_name = "PANTHEON_SOIP_EIP";
+  // public $soip_constant_name = "PANTHEON_SOIP_EIP_PROD";
+  
+  /**
+   * Command description here.
+   *
+   * @param $arg1
+   *   Argument description.
+   * @param array $options
+   *   An associative array of options whose values come from cli, aliases, config, etc.
+   * @option option-name
+   *   Description
+   * @usage asc_courses-commandName foo
+   *   Usage description
+   *
+   * @command asc_courses:commandName
+   * @aliases foo
+   */
+  public function commandName($arg1, $options = ['option-name' => 'default']) {
+    //$this->logger()->success(dt('Achievement unlocked.'));
+
+    // $api = new AscCoursesApi();
+    // print_r($api->fetchSubjectCourses('D0506'));
+
+    // $api->getAccessToken();
+    // $dorg = $api->config->get('asc_courses.dept_org');
+    // echo "D-org: $dorg\n";
+    // $courses = $api->fetchSubjectCourses($dorg);
+
+
+    $importer = new AscCoursesImporter();
+    $importer->fetchAndImportAll();
+    // print_r($importer->loadSubjectDataFromDatabase('D0506'));
+    
+  }
+
+  /**
+   * Command description here.
+   *
+   * @param array $options
+   *   An associative array of options whose values come from cli, aliases, config, etc.
+   * @option option-name
+   *   Description
+   * @usage asc_courses-class-import
+   *   Usage description
+   *
+   * @command asc_courses:class-import
+   * @aliases asc-class-import
+   */
+  public function classImport($options = ['option-name' => 'default']) {
+    // $this->logger()->success(dt('Achievement unlocked.'));
+
+    // Bittrex example
+    // $response = \Drupal::httpClient()
+    // ->get('https://api.bittrex.com/api/v1.1/public/getmarkets');
+    // $json_string = (string) $response->getBody();
+    // $json_data = json_decode($json_string);
+    // print_r($json_data);
+
+    // return true;
+
+    echo $this->soip_constant_name . ": " . constant($this->soip_constant_name) . "\n";
+    
+
+    $config = \Drupal::service('config.factory')->get('asc_courses.settings');
+    $consumer_key = $config->get('asc_courses.qa_consumer_key');
+    $consumer_secret = $config->get('asc_courses.qa_consumer_secret');
+    $dept_org = $config->get('asc_courses.dept_org');
+    $bearer_auth = base64_encode("$consumer_key:$consumer_secret");
+    echo "consumer key: $consumer_key\n";
+    echo "consumer secret: $consumer_secret\n";
+    echo "dept_org: $dept_org\n";
+    echo "bearer_auth: $bearer_auth\n";
+
+
+
+    /*
+      // get access token
+      $token_response = \Drupal::httpClient()
+        ->post($access_token_url, 
+            [
+              'headers' => [
+                'Accept' => 'application/json',
+                'Authorization' => "Basic $bearer_auth"
+              ],
+              'form_params' => [
+                'grant_type' => 'client_credentials'
+              ]
+            ]
+      );
+      $json_string = (string) $token_response->getBody();
+      $json_data = json_decode($json_string);
+      print_r($json_data);
+      $access_token = json_decode($token_response->getBody())->access_token;
+      // echo "access_token: $access_token\n";
+    */
+
+    // QA URL
+    $access_token_url = 'https://apig-qa.eip.osu.edu/token?grant_type=client_credentials';
+
+    // Production URL
+    // $access_token_url = 'https://apig.eip.osu.edu/token?grant_type=client_credentials';
+
+    $access_token_headers = [
+      "Accept: application/json",
+      "Authorization: Basic $bearer_auth",
+    ];
+
+    // Create a "resolve_host" that will point to localhost and resolve externally
+    $host = parse_url($access_token_url, PHP_URL_HOST);
+    $localhost = "127.0.0.1";
+    $resolve_host = array(sprintf("%s:%d:%s", $host, constant($this->soip_constant_name), $localhost));
+    echo "\n resolve_host : " . implode($resolve_host) . "\n";
+
+    // $post_data = ["grant_type" => "client_credentials"];
+
+    $ch = curl_init();
+    curl_setopt($ch, CURLOPT_URL, $access_token_url);
+    curl_setopt($ch, CURLOPT_RESOLVE, $resolve_host);
+    curl_setopt($ch, CURLOPT_PORT, constant($this->soip_constant_name));
+    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
+    curl_setopt($ch, CURLOPT_POST, true);
+    // curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
+    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+    curl_setopt($ch, CURLOPT_HTTPHEADER, $access_token_headers);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
+    curl_setopt($ch, CURLOPT_VERBOSE, true);
+    $access_token_start = microtime(true);
+    $access_token_result = curl_exec($ch);
+    $access_token_error = curl_error($ch);
+    print_r($access_token_error);
+    curl_close($ch);
+    $access_token_finish = microtime(true);
+    $access_token_seconds = $access_token_finish - $access_token_start;
+
+    echo "\n================\nAccess token result: \n";
+    print_r($access_token_result);
+    echo "\n";
+    echo "Access token in $access_token_seconds seconds\n";
+
+
+    $json_data = json_decode($access_token_result);
+    // print_r($json_data);
+    $access_token = $json_data->access_token;
+    echo "access_token: $access_token\n\n";
+
+
+    // QA URL
+    $course_data_url = "https://apig-qa.eip.osu.edu/crseinfo/1.0.0/getCatalogInfo?campus=COL&acad_org=$dept_org";
+
+    // Production URL
+    // $course_data_url = "https://apig.eip.osu.edu/crseinfo/1.0.0/getCatalogInfo?campus=COL&acad_org=$dept_org";
+    
+    echo "course data url: $course_data_url\n";
+    $course_data_headers = [
+      "Accept: application/json",
+      "Authorization: Bearer $access_token"
+    ];
+
+
+    /*
+      // get course data  (httpClient)
+      $response = \Drupal::httpClient()
+      ->get("https://apig-qa.eip.osu.edu/crseinfo/1.0.0/getCatalogInfo?campus=COL&acad_org=$dept_org", 
+              [
+                // 'auth' => ['username', 'password'],
+                'headers' => [
+                  'Accept' => 'application/json',
+                  'Authorization' => "Bearer $access_token"
+                ]
+              ]
+      );
+    
+      $course_json = (string) $response->getBody();
+      echo "json length: " . strlen($course_json) . "\n\n";
+    */
+
+    $ch = curl_init();
+    curl_setopt($ch, CURLOPT_URL, $course_data_url);
+    curl_setopt($ch, CURLOPT_RESOLVE, $resolve_host);
+    curl_setopt($ch, CURLOPT_PORT, constant($this->soip_constant_name));
+    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
+    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+    curl_setopt($ch, CURLOPT_HTTPHEADER, $course_data_headers);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
+    // curl_setopt($ch, CURLOPT_VERBOSE, true);
+    $course_data_start = microtime(true);
+    $course_data_result = curl_exec($ch);
+    // print_r($course_data_result);
+    $course_data_error = curl_error($ch);
+    curl_close($ch);
+    $course_data_finish = microtime(true);
+    $course_data_seconds = $course_data_finish - $course_data_start;
+
+    echo "Course data length:" . strlen($course_data_result) . "\n";
+    echo "Course data in $course_data_seconds seconds\n\n";
+
+
+    $connection = \Drupal::database();
+    $row = [
+      'date' => time(),
+      'dept_org' => $dept_org,
+      'raw_json' => $course_data_result
+    ];
+    $connection->insert('asc_course_data')->fields($row)->execute();
+
+
+    $json_data = json_decode($course_data_result);
+
+    // print_r($json_data);
+  }
+
+  /**
+   * Command description here.
+   *
+   * @param array $options
+   *   An associative array of options whose values come from cli, aliases, config, etc.
+   * @option option-name
+   *   Description
+   * @usage asc_courses-create-courses
+   *   Usage description
+   *
+   * @command asc_courses:create-courses
+   * @aliases asc-create-courses
+   */
+  public function createCourses($options = ['option-name' => 'default']) {
+    $connection = \Drupal::database();
+    $course_info_query = $connection->select('asc_course_data', 'acd')
+      // ->fields('acd', array('id', 'date', 'dept_org'))
+      ->fields('acd')
+      ->orderBy('date', 'DESC')
+      ->range(0, 1);
+    $course_info_result = $course_info_query->execute();
+    // $result_count = $course_info_result->rowCount();
+    // echo "Rows: $result_count\n";
+    // $rows = $course_info_result->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+    $row = $course_info_result->fetchAssoc();
+    // print_r($row);
+    $courses_json = $row['raw_json'];
+    // $len = strlen($courses_json);
+    echo "JSON Length:" . strlen($courses_json) . "\n";
+    // echo substr($courses_json, 0, 1024) . "\n\n";
+    $json_data = json_decode($courses_json);
+    // print_r($json_data);
+    
+    foreach($json_data->getCourseCatalogResponse->catalog->course as $json_course) {
+      if($i < 1) {
+        print_r($json_course);
+
+        // look up existing node
+        $node_query = \Drupal::entityQuery('node')
+          ->condition('field_eip_id', $json_course->{'crse-id'}, '=');
+        $existing_node = $node_query->execute();
+
+        if(empty($existing_node)) {
+          // $course_node = Node::create([
+          $course_node = \Drupal::entityTypeManager()->getStorage('node')->create(array(
+            'type'        => 'course',
+            'title'       => $json_course->{'course-title-long'},
+            // 'field_course_description'  => $json_course->{'descrlong'},
+            'field_course_number' => [$json_course->{'catalog-nbr'}],
+            'field_credit_hours' => [$json_course->{'acad-prog'}],
+            'field_subject_abbreviation' => [$json_course->{'subject'}],
+            'field_eip_id'                => [$json_course->{'crse-id'}],
+          ));
+          $course_node->save();
+          echo "New course [" . $json_course->{'catalog-nbr'} . "] ";
+        }
+        else {
+          // update existing node
+          echo "Update existing node!\n\n";
+          print_r($existing_node);
+        }
+
+        // $i++;
+      }
+    }
+  }
+
+  /**
+   * An example of the table output format.
+   *
+   * @param $nid
+   *   ID of the node to be printed.
+   * @param array $options An associative array of options whose values come from cli, aliases, config, etc.
+   *
+   * @field-labels
+   *   group: Group
+   *   token: Token
+   *   name: Name
+   * @default-fields group,token,name
+   *
+   * @command asc_courses:print-node
+   * @aliases asc-print-node
+   *
+   * @filter-default-field name
+   * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
+   */
+  public function printNode($nid, $options = ['option-name' => 'default']) {
+    $asdf_node = Node::load($nid);
+    // print_r($asdf_node);
+
+    $asdf_body = $asdf_node->get('body')->getValue();
+    echo "body: " . print_r($asdf_body, true) . "\n";
+
+    $asdf_desc = $asdf_node->get('field_course_description')->getValue();
+    echo "description: " . print_r($asdf_desc, true) . "\n";
+
+    $asdf_course_number = $asdf_node->get('field_course_number')->getValue();
+    echo "course_number: " . print_r($asdf_course_number, true) . "\n";
+
+    $asdf_credit_hours = $asdf_node->get('field_credit_hours')->getValue();
+    echo "credit_hours: " . print_r($asdf_credit_hours, true) . "\n";
+
+    // $asdf_eip_id = $asdf_node->get('field_eip_id')->getValue();
+    // echo "eip_id: " . print_r($asdf_eip_id, true) . "\n";
+
+    // $asdf_offered_autumn = $asdf_node->get('field_offered_autumn')->getValue();
+    // echo "offered_autumn: " . print_r($asdf_offered_autumn, true) . "\n";
+
+    // $asdf_offered_spring = $asdf_node->get('field_offered_spring')->getValue();
+    // echo "offered_spring: " . print_r($asdf_offered_spring, true) . "\n";
+
+    // $asdf_offered_summer = $asdf_node->get('field_offered_summer')->getValue();
+    // echo "offered_summer: " . print_r($asdf_offered_summer, true) . "\n";
+
+    // $asdf_subject_abbreviation = $asdf_node->get('field_subject_abbreviation'->getValue();
+    // echo "subject_abbreviation: " . print_r($asdf_subject_abbreviation, true) . "\n";
+
+    // $node_entity_type = \Drupal::entityTypeManager()->getDefinition('node');
+    // $bundle_key = $node_entity_type = \Drupal::entityTypeManager()->getDefinition('node');
+    // echo "bundle key: " . $bundle_key->getKey() . "\n";
+
+  }
+
+  /**
+   * An example of the table output format.
+   *
+   * @param array $options An associative array of options whose values come from cli, aliases, config, etc.
+   *
+   * @field-labels
+   *   group: Group
+   *   token: Token
+   *   name: Name
+   * @default-fields group,token,name
+   *
+   * @command asc_courses:token
+   * @aliases token
+   *
+   * @filter-default-field name
+   * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
+   */
+  public function token($options = ['format' => 'table']) {
+    $all = \Drupal::token()->getInfo();
+    foreach ($all['tokens'] as $group => $tokens) {
+      foreach ($tokens as $key => $token) {
+        $rows[] = [
+          'group' => $group,
+          'token' => $key,
+          'name' => $token['name'],
+        ];
+      }
+    }
+    return new RowsOfFields($rows);
+  }
+
+}
diff --git a/web/modules/asc_courses/src/Controller/CoursesController.php b/web/modules/asc_courses/src/Controller/CoursesController.php
new file mode 100644
index 0000000000000000000000000000000000000000..ec0890d4ef71a8248be4fae523cb723bdcee8e93
--- /dev/null
+++ b/web/modules/asc_courses/src/Controller/CoursesController.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\asc_courses\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+
+/**
+ * Defines CoursesController class.
+ */
+class CoursesController extends ControllerBase {
+
+  /**
+   * Display the markup.
+   *
+   * @return array
+   *   Return markup array.
+   */
+  public function content() {
+    return [
+      '#type' => 'markup',
+      '#markup' => $this->t('Hello, World!'),
+    ];
+  }
+
+}
\ No newline at end of file
diff --git a/web/modules/asc_courses/src/Form/AdminForm.php b/web/modules/asc_courses/src/Form/AdminForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..138a25e356ee6e41576dc964ca811a1dd2f8f56f
--- /dev/null
+++ b/web/modules/asc_courses/src/Form/AdminForm.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Drupal\asc_courses\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+use \Drupal\asc_courses\AscCoursesApi;
+use \Drupal\asc_courses\AscCoursesImporter;
+
+class AdminForm extends ConfigFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'asc_courses_config';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    // Form constructor
+    $form = parent::buildForm($form, $form_state);
+
+    $config = $this->config('asc_courses.settings');
+
+    // QA Consumer key field
+    $form['qa_consumer_key'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('QA Consumer Key'),
+      '#default_value' => $config->get('asc_courses.qa_consumer_key'),
+      '#description' => $this->t('Consumer key from QA EIP API dashboard'),
+    );
+
+    // QA Consumer secret field
+    $form['qa_consumer_secret'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('QA Consumer Secret'),
+      '#default_value' => $config->get('asc_courses.qa_consumer_secret'),
+      '#description' => $this->t('Consumer secret from QA EIP API dashboard.'),
+    );
+
+    // Prod Consumer key field
+    $form['prod_consumer_key'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('Production Consumer Key'), 
+      '#default_value' => $config->get('asc_courses.prod_consumer_key'),
+      '#description' => $this->t('Consumer key from Production EIP API dashboard'),
+    );
+
+    // Prod Consumer secret field
+    $form['prod_consumer_secret'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('Production Consumer Secret'),
+      '#default_value' => $config->get('asc_courses.prod_consumer_secret'),
+      '#description' => $this->t('Consumer secret from Production EIP API dashboard.'),
+    );
+    
+    // EIP environment
+    $form['eip_environment'] = array(
+      '#type' => 'radios',
+      '#title' => $this->t('EIP Environment'),
+      '#default_value' => $config->get('asc_courses.eip_environment'),
+      '#options' => array(
+        'qa' => $this->t('QA'),
+        'prod' => $this->t('Production')
+      )
+    );
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $config = $this->config('asc_courses.settings');
+    $config->set('asc_courses.qa_consumer_key', $form_state->getValue('qa_consumer_key'));
+    $config->set('asc_courses.qa_consumer_secret', $form_state->getValue('qa_consumer_secret'));
+    $config->set('asc_courses.prod_consumer_key', $form_state->getValue('prod_consumer_key'));
+    $config->set('asc_courses.prod_consumer_secret', $form_state->getValue('prod_consumer_secret'));
+    $config->set('asc_courses.eip_environment', $form_state->getValue('eip_environment'));
+    $config->save();
+
+    $importer = new AscCoursesImporter();
+    $importer->fetchAndImportAll();
+    
+    return parent::submitForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return [
+      'asc_courses.settings',
+    ];
+  }
+}
+
diff --git a/web/modules/asc_courses/src/Form/ApiSettingsForm.php b/web/modules/asc_courses/src/Form/ApiSettingsForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..775c702228c2b2805f7e28d48af2528d26890dd0
--- /dev/null
+++ b/web/modules/asc_courses/src/Form/ApiSettingsForm.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Drupal\asc_courses\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+use \Drupal\asc_courses\AscCoursesApi;
+use \Drupal\asc_courses\AscCoursesImporter;
+
+class ApiSettingsForm extends ConfigFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'asc_courses_config';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    // Form constructor
+    $form = parent::buildForm($form, $form_state);
+
+    $config = $this->config('asc_courses.settings');
+
+    // QA Consumer key field
+    $form['qa_consumer_key'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('QA Consumer Key'),
+      '#default_value' => $config->get('asc_courses.qa_consumer_key'),
+      '#description' => $this->t('Consumer key from QA EIP API dashboard'),
+    );
+
+    // QA Consumer secret field
+    $form['qa_consumer_secret'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('QA Consumer Secret'),
+      '#default_value' => $config->get('asc_courses.qa_consumer_secret'),
+      '#description' => $this->t('Consumer secret from QA EIP API dashboard.'),
+    );
+
+    // Prod Consumer key field
+    $form['prod_consumer_key'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('Production Consumer Key'), 
+      '#default_value' => $config->get('asc_courses.prod_consumer_key'),
+      '#description' => $this->t('Consumer key from Production EIP API dashboard'),
+    );
+
+    // Prod Consumer secret field
+    $form['prod_consumer_secret'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('Production Consumer Secret'),
+      '#default_value' => $config->get('asc_courses.prod_consumer_secret'),
+      '#description' => $this->t('Consumer secret from Production EIP API dashboard.'),
+    );
+    
+    // EIP environment
+    $form['eip_environment'] = array(
+      '#type' => 'radios',
+      '#title' => $this->t('EIP Environment'),
+      '#default_value' => $config->get('asc_courses.eip_environment'),
+      '#options' => array(
+        'qa' => $this->t('QA'),
+        'prod' => $this->t('Production')
+      )
+    );
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $config = $this->config('asc_courses.settings');
+    $config->set('asc_courses.qa_consumer_key', $form_state->getValue('qa_consumer_key'));
+    $config->set('asc_courses.qa_consumer_secret', $form_state->getValue('qa_consumer_secret'));
+    $config->set('asc_courses.prod_consumer_key', $form_state->getValue('prod_consumer_key'));
+    $config->set('asc_courses.prod_consumer_secret', $form_state->getValue('prod_consumer_secret'));
+    $config->set('asc_courses.eip_environment', $form_state->getValue('eip_environment'));
+    $config->save();
+
+    $importer = new AscCoursesImporter();
+    $importer->fetchAndImportAll();
+    
+    return parent::submitForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return [
+      'asc_courses.settings',
+    ];
+  }
+}
+
diff --git a/web/modules/asc_courses/src/Form/SettingsForm.php b/web/modules/asc_courses/src/Form/SettingsForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..b38b0e16cfeb9f96d7d2e65bbafacb5c7947c034
--- /dev/null
+++ b/web/modules/asc_courses/src/Form/SettingsForm.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\asc_courses\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+use \Drupal\asc_courses\AscCoursesApi;
+use \Drupal\asc_courses\AscCoursesImporter;
+
+class SettingsForm extends ConfigFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'asc_courses_config';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    // Form constructor
+    $form = parent::buildForm($form, $form_state);
+
+    $config = $this->config('asc_courses.settings');
+
+    // D-Org number 
+    $form['dept_org'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('D-Org number(s)'),
+      '#default_value' => $config->get('asc_courses.dept_org'),
+      '#description' => $this->t('D-Org number (i.e. D1435). Multiple numbers may be entered separated by commas'),
+    );
+
+    // Import now?
+    $form['import_now'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Import now!'),
+      // '#default_value' => $config->get('asc_courses.dept_org'),
+      '#description' => $this->t('Checking this box will create new and update existing courses.'),
+    );
+    
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $config = $this->config('asc_courses.settings');
+    $config->set('asc_courses.dept_org', $form_state->getValue('dept_org'));
+    $config->save();
+
+    $import = $form_state->getValue('import_now');
+    
+
+    if($import) {
+      \Drupal::logger('asc_courses')->notice("Running courses import.");
+      $importer = new AscCoursesImporter();
+      $importer->fetchAndImportAll();
+    }
+
+    
+    return parent::submitForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return [
+      'asc_courses.settings',
+    ];
+  }
+}
+
diff --git a/web/modules/asc_courses/src/Plugin/migrate/CourseDataCached.php b/web/modules/asc_courses/src/Plugin/migrate/CourseDataCached.php
new file mode 100644
index 0000000000000000000000000000000000000000..620ac3c70ce38bc4c937fdc7ee223982ac1c04d2
--- /dev/null
+++ b/web/modules/asc_courses/src/Plugin/migrate/CourseDataCached.php
@@ -0,0 +1,205 @@
+<?php
+namespace Drupal\asc_courses\Plugin\migrate\source;
+
+use Drupal\Core\Database\Query\Condition;
+use Drupal\migrate\Row;
+// use Drupal\migrate\MigrateSkipRowException;
+// use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
+use Drupal\migrate\Plugin\migrate\source\SqlBase;
+
+/**
+ * Drupal 7 file_entity source from database.
+ *
+ * @MigrateSource(
+ *   id = "asc_person",
+ *   source_provider = "user"
+ * )
+ */
+class CourseDataCached extends SqlBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query() {
+    //echo "AscPerson::query()\n";
+
+    /*
+      -- Users who also have people nodes (where clearly identifiable)
+      select
+        u.uid,
+        u.mail,
+        substring(u.mail, 1, locate('@osu.edu', u.mail)-1) as umail,
+        u.name,
+        substring(u.name, 1, locate('@osu.edu', u.name)-1) as uname,
+        fdfape.entity_id,
+        fdfapp.entity_id,
+        fdfape.field_asc_people_email_email,
+        fdfapp.field_asc_people_picture_opic
+      from
+        users u
+          left outer join field_data_field_asc_people_email fdfape
+            on u.mail = fdfape.field_asc_people_email_email
+          left outer join field_data_field_asc_people_picture fdfapp
+            on fdfape.entity_id = fdfapp.entity_id
+      ;
+
+
+      select
+        substring(u.mail, 1, locate('@osu.edu', u.mail)-1) as umail
+      from users u
+      union
+      select field_asc_people_picture_opic
+      from field_data_field_asc_people_picture fdfapp
+      ;
+
+      select
+        u.mail as umail
+      from users u
+      union
+      select field_asc_people_picture_opic
+      from field_data_field_asc_people_picture;
+
+      select uid, name, mail, created, access, login, status from users limit 20;
+
+        select
+
+        from
+          users u
+    */
+
+    $course_data_query = $this->select('asc_course_data', 'acd');
+    $course_data_query->fields('acd', ['id', 'date', 'dept_org', 'raw_json']);
+    // $course_data_query->
+
+
+
+    $people_query = $this->select('node', 'n');
+    $people_query->addExpression('n.nid', 'asc_people_id');
+    $people_query->fields('n', array('title', 'created', 'changed', 'status'));
+    $people_query->leftJoin('field_data_field_asc_people_picture', 'fdfapp', 'n.nid = fdfapp.entity_id');
+    $people_query->leftJoin('field_data_field_asc_people_email', 'fdfape', 'n.nid = fdfape.entity_id');
+    $people_query->addExpression('fdfapp.field_asc_people_picture_opic', 'osu_name_num');
+    $people_query->addExpression('fdfape.field_asc_people_email_email', 'name');
+    $people_query->addExpression('fdfape.field_asc_people_email_email', 'mail');
+    $people_query->addExpression('0', 'login');
+    $people_query->addExpression('0', 'access');
+    $people_query->condition('n.status', 0, '>');
+    $people_query->condition('n.type', 'asc_people', '=');
+    // echo "people query: $people_query\n";
+
+    // $pictures_query = $this->select('field_data_field_asc_people_picture', 'fdfapp');
+    // $pictures_query->addExpression("fdfapp.entity_id", 'asc_people_id');
+    // $pictures_query->addExpression("'node'", "entity_type");
+    // $pictures_query->addExpression("fdfapp.entity_id", "entity_id");
+    // $pictures_query->addExpression("concat(fdfapp.field_asc_people_picture_opic, '@osu.edu')", "name");
+    // $pictures_query->addExpression("concat(fdfapp.field_asc_people_picture_opic, '@osu.edu')", "mail");
+    // $pictures_query->addExpression("fdfapp.field_asc_people_picture_opic", "osu_name_num");
+
+    // $pictures_query->addExpression("1", "status");
+    // $pictures_query->addExpression("unix_timestamp()", "created");
+    // $pictures_query->range(1,1);
+    // echo "pictures query: $pictures_query\n";
+
+    return $people_query;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function count($refresh = false) {
+    //echo "AscPerson::count()\n";
+
+    $peoplecount_query = $this->select('node');
+    $peoplecount_query->condition('type', 'asc_people', '=');
+    $peoplecount_query->condition('status', 0, '>');
+    $peoplecount_query->addExpression('count(*)', 'count');
+    $peoplecount = $peoplecount_query->execute()->fetchField(0);
+
+    return $peoplecount;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepareRow(Row $row) {
+    // print_r($row);
+    $mail = $row->getSourceProperty('mail');
+    if(empty($mail)) {
+      $osu_name_num = $row->getSourceProperty('osu_name_num');
+      $mail = $osu_name_num . "@osu.edu";
+      $row->setSourceProperty('mail', $mail);
+      echo "Update email using OSU name.#: $mail\n";
+    }
+
+    foreach (array_keys($this->getFields('node', 'asc_people')) as $field) {
+      $nid = $row->getSourceProperty('asc_people_id');
+      $row->setSourceProperty($field, $this->getFieldValues('node', $field, $nid));
+    }
+
+    $row->setSourceProperty('roles', ['department_faculty_staff']);
+
+    // $node_query = $this->select('node', 'n')
+    //   ->fields('n', array('created', 'changed'))
+    //   ->condition('n.nid', $nid);
+    // $node_query->addExpression('0', 'login');
+    // $node_query->addExpression('0', 'access');
+    // $node_row = $node_query->execute()->fetch();
+
+    // foreach($node_row as $field_name => $field_value) {
+    //   $row->setSourceProperty($field_name, $field_value);
+    // }
+
+    return parent::prepareRow($row);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fields() {
+    $fields = [
+      'uid' => $this->t('User ID'),
+      'name' => $this->t('Username'),
+      //'pass' => $this->t('Password'),
+      'mail' => $this->t('Email address'),
+      //'signature' => $this->t('Signature'),
+      //'signature_format' => $this->t('Signature format'),
+      'created' => $this->t('Registered timestamp'),
+      'access' => $this->t('Last access timestamp'),
+      'login' => $this->t('Last login timestamp'),
+      'changed' => $this->t('Last time user was updated'),
+      'status' => $this->t('Status'),
+      'timezone' => $this->t('Timezone'),
+      'language' => $this->t('Language'),
+      'picture' => $this->t('Picture'),
+      'init' => $this->t('Init'),
+      'data' => $this->t('User data'),
+      'roles' => $this->t('Roles'),
+    ];
+
+    return $fields;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIds() {
+    /*
+    $ids['fid']['type'] = 'integer';
+    return $ids;
+    return [
+      'uid' => [
+        'type' => 'integer',
+        'alias' => 'u',
+      ],
+    ];
+    */
+    $ids = [
+      'asc_people_id' => [
+        'type' => 'string'
+      ]
+    ];
+
+    return $ids;
+  }
+}
+