diff --git a/src/app/core/models/planned-term.ts b/src/app/core/models/planned-term.ts
index bc5bad39a3890113d9da8ea0234a7bc0038b73cd..f5b732ce90466412cd5122f1ce0250567d5a02ab 100644
--- a/src/app/core/models/planned-term.ts
+++ b/src/app/core/models/planned-term.ts
@@ -1,7 +1,5 @@
 import { Course } from '@app/core/models/course';
-import { TermCode } from '@app/core/models/termcode';
-
-export type PlannedTermEra = 'past' | 'active' | 'future';
+import { TermCode } from '@app/degree-planner/shared/term-codes/termcode';
 
 export type PlannedTermNote =
   | { isLoaded: true; text: string; id: number }
@@ -10,7 +8,6 @@ export type PlannedTermNote =
 export interface PlannedTerm {
   roadmapId: number;
   termCode: TermCode;
-  era: PlannedTermEra;
   note?: PlannedTermNote;
   plannedCourses: ReadonlyArray<Course>;
   enrolledCourses: ReadonlyArray<Course>;
diff --git a/src/app/core/models/year.ts b/src/app/core/models/year.ts
index c8dcef8dcc2d366381735ee73721a7516ed72bb5..8b7271292a4b55ad94fcc5aea793ed2dff090687 100644
--- a/src/app/core/models/year.ts
+++ b/src/app/core/models/year.ts
@@ -1,5 +1,5 @@
 import { PlannedTerm } from '@app/core/models/planned-term';
-import { YearCode } from '@app/core/models/termcode';
+import { YearCode } from '@app/degree-planner/shared/term-codes/yearcode';
 
 export interface Year {
   yearCode: YearCode;
diff --git a/src/app/degree-planner/course-search/course-search.component.html b/src/app/degree-planner/course-search/course-search.component.html
index 54c88a335cc131ee3ba4c3f310a5f4063b7d4cb0..293bc81d42f3e61fd18430858630468fe2dc14ff 100644
--- a/src/app/degree-planner/course-search/course-search.component.html
+++ b/src/app/degree-planner/course-search/course-search.component.html
@@ -10,7 +10,7 @@
     <mat-select placeholder="Term" aria-label="Term" [disableOptionCentering]="true" formControlName="term">
       <mat-option value="0000">All courses</mat-option>
       <mat-option
-        *ngFor="let termCode of constants.activeTermCodes()"
+        *ngFor="let termCode of termCodeFactory.active"
         [value]="termCode.toString()"
         >{{ termCode | getTermDescription }}</mat-option>
     </mat-select>
diff --git a/src/app/degree-planner/course-search/course-search.component.ts b/src/app/degree-planner/course-search/course-search.component.ts
index f5bff94e85b7b57f6f6e1537748aca7beede5cf1..7151bca0a7aedf4f60b3aba9c9128c758ae8655b 100644
--- a/src/app/degree-planner/course-search/course-search.component.ts
+++ b/src/app/degree-planner/course-search/course-search.component.ts
@@ -21,9 +21,10 @@ import {
   SubjectCodesTo,
   SubjectDescription,
 } from '@app/core/models/course';
-import { TermCode } from '@app/core/models/termcode';
+import { TermCode } from '@app/degree-planner/shared/term-codes/termcode';
 import { MediaMatcher } from '@angular/cdk/layout';
 import { ConstantsService } from '../services/constants.service';
+import { TermCodeFactory } from '../services/termcode.factory';
 
 @Component({
   selector: 'cse-course-search',
@@ -41,7 +42,7 @@ export class CourseSearchComponent implements OnInit, OnDestroy {
 
   // Observable used for drag and drop and for populating term select
   public dropZoneIds$: Observable<string[]>;
-  public activeTerms$: Observable<ReadonlyArray<TermCode>>;
+  public activeTerms: ReadonlyArray<TermCode>;
   public activeSelectedSearchTerm$: Observable<TermCode | undefined>;
   public isCourseSearchOpen$: Observable<boolean>;
 
@@ -61,6 +62,7 @@ export class CourseSearchComponent implements OnInit, OnDestroy {
     private fb: FormBuilder,
     private api: DegreePlannerApiService,
     private snackBar: MatSnackBar,
+    public termCodeFactory: TermCodeFactory,
     mediaMatcher: MediaMatcher,
     public constants: ConstantsService,
   ) {
@@ -74,6 +76,8 @@ export class CourseSearchComponent implements OnInit, OnDestroy {
     this.hasResults = false;
     this.isLoading = false;
 
+    this.activeTerms = this.termCodeFactory.active;
+
     // Get active term drop zones
     this.dropZoneIds$ = this.store.pipe(
       select(selectors.selectAllVisibleYears),
diff --git a/src/app/degree-planner/degree-planner.component.ts b/src/app/degree-planner/degree-planner.component.ts
index 2bfea522b49cf96da4ce7216bb29840676848ccb..cdf7fdd6dd267d990cb71c05bd2a3f4882b6fb23 100644
--- a/src/app/degree-planner/degree-planner.component.ts
+++ b/src/app/degree-planner/degree-planner.component.ts
@@ -35,10 +35,11 @@ import {
   ExpandAcademicYear,
   CollapseAcademicYear,
 } from './store/actions/ui.actions';
-import { YearCode } from '@app/core/models/termcode';
+import { YearCode } from '@app/degree-planner/shared/term-codes/yearcode';
 import { ConstantsService } from './services/constants.service';
 import { AddAcademicYearRequest } from './store/actions/addAcademicYear.actions';
 import { UserPreferences } from '@app/core/models/user-preferences';
+import { TermCodeFactory } from './services/termcode.factory';
 
 @Component({
   selector: 'cse-degree-planner',
@@ -62,9 +63,11 @@ export class DegreePlannerComponent implements OnInit {
   constructor(
     private store: Store<GlobalState>,
     private constants: ConstantsService,
+    private termCodeFactory: TermCodeFactory,
     public mediaMatcher: MediaMatcher,
     public dialog: MatDialog,
     private snackBar: MatSnackBar,
+    private termCodeService: TermCodeFactory,
   ) {
     this.mobileView = mediaMatcher.matchMedia('(max-width: 959px)');
     this.version = constants.getVersion();
@@ -105,7 +108,7 @@ export class DegreePlannerComponent implements OnInit {
       select(selectors.selectAllVisibleYears),
       map(years => Object.keys(years)),
       distinctUntilChanged(utils.compareStringArrays),
-      map(yearCodes => yearCodes.map(YearCode.fromString)),
+      map(ycs => ycs.map(yc => this.termCodeFactory.fromRawYearCode(yc))),
     );
   }
 
@@ -180,7 +183,7 @@ export class DegreePlannerComponent implements OnInit {
     const text =
       `This will change your primary plan and replace the current ` +
       `courses in your cart with the courses in this plan's ` +
-      `${this.constants.firstActiveTermCode().description} term.`;
+      `${this.termCodeService.first().description} term.`;
 
     this.dialog
       .open(ConfirmDialogComponent, {
diff --git a/src/app/degree-planner/dialogs/credit-overload-dialog/credit-overload-dialog.component.ts b/src/app/degree-planner/dialogs/credit-overload-dialog/credit-overload-dialog.component.ts
index f39441acc5dfd6f8f7810f897304bc36b61ad0a4..f8070e1b68e7279c6359c3b36f39eb3f2f34d7d3 100644
--- a/src/app/degree-planner/dialogs/credit-overload-dialog/credit-overload-dialog.component.ts
+++ b/src/app/degree-planner/dialogs/credit-overload-dialog/credit-overload-dialog.component.ts
@@ -1,5 +1,5 @@
 import { Component, Input, Inject } from '@angular/core';
-import { TermCode } from '@app/core/models/termcode';
+import { TermCode } from '@app/degree-planner/shared/term-codes/termcode';
 import { MAT_DIALOG_DATA } from '@angular/material';
 
 @Component({
diff --git a/src/app/degree-planner/dialogs/notes-dialog/notes-dialog.component.ts b/src/app/degree-planner/dialogs/notes-dialog/notes-dialog.component.ts
index c045f30225c82e478bf98f12b6837a175ff28c15..f11adefca7e7df9be821a091b5f24fc981eda6de 100644
--- a/src/app/degree-planner/dialogs/notes-dialog/notes-dialog.component.ts
+++ b/src/app/degree-planner/dialogs/notes-dialog/notes-dialog.component.ts
@@ -10,7 +10,7 @@ import {
   WriteNote,
   DeleteNote,
 } from '@app/degree-planner/store/actions/note.actions';
-import { TermCode } from '@app/core/models/termcode';
+import { TermCode } from '@app/degree-planner/shared/term-codes/termcode';
 
 export type NotesDialogData =
   | {
diff --git a/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts b/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts
index e2e2d3bda2f24c9ce6c41ed15e16e02c3e584b19..a26cd0e462fd45e8a93e0a4043f5245b0c4bb8f0 100644
--- a/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts
+++ b/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts
@@ -8,7 +8,7 @@ import { DegreePlannerState } from '@app/degree-planner/store/state';
 import { Store } from '@ngrx/store';
 
 import { RemoveCourse } from '@app/degree-planner/store/actions/course.actions';
-import { TermCode } from '@app/core/models/termcode';
+import { TermCodeFactory } from '@app/degree-planner/services/termcode.factory';
 
 @Component({
   selector: 'cse-remove-course-confirm-dialog',
@@ -23,6 +23,7 @@ export class RemoveCourseConfirmDialogComponent implements OnInit {
 
   constructor(
     private dialogRef: MatDialogRef<RemoveCourseConfirmDialogComponent>,
+    private termCodeService: TermCodeFactory,
     private store: Store<{ degreePlanner: DegreePlannerState }>,
     @Inject(MAT_DIALOG_DATA) data: any,
   ) {
@@ -43,7 +44,9 @@ export class RemoveCourseConfirmDialogComponent implements OnInit {
         console.log(this.course);
         const id = this.course.id;
         if (typeof id === 'number') {
-          const fromTermCode = new TermCode(this.course.termCode);
+          const fromTermCode = this.termCodeService.fromString(
+            this.course.termCode,
+          );
           this.store.dispatch(new RemoveCourse({ fromTermCode, recordId: id }));
         } else {
           throw new Error('cannot remove a course that does not have an ID');
diff --git a/src/app/degree-planner/saved-for-later-container/saved-for-later-container.component.ts b/src/app/degree-planner/saved-for-later-container/saved-for-later-container.component.ts
index 1963d3736c36f8956d1e0b070ac6f9acd1367cdf..13dc6110e7d7f9329df2c27eda58c04f652d374a 100644
--- a/src/app/degree-planner/saved-for-later-container/saved-for-later-container.component.ts
+++ b/src/app/degree-planner/saved-for-later-container/saved-for-later-container.component.ts
@@ -15,8 +15,9 @@ import {
 } from '@app/degree-planner/store/actions/course.actions';
 import * as selectors from '@app/degree-planner/store/selectors';
 import { distinctUntilChanged } from 'rxjs/operators';
-import { TermCode } from '@app/core/models/termcode';
+import { TermCode } from '@app/degree-planner/shared/term-codes/termcode';
 import { MediaMatcher } from '@angular/cdk/layout';
+import { TermCodeFactory } from '../services/termcode.factory';
 
 @Component({
   selector: 'cse-saved-for-later-container',
@@ -31,6 +32,7 @@ export class SavedForLaterContainerComponent implements OnInit {
 
   constructor(
     private store: Store<{ degreePlanner: DegreePlannerState }>,
+    private termCodeService: TermCodeFactory,
     mediaMatcher: MediaMatcher,
   ) {
     this.mobileView = mediaMatcher.matchMedia('(max-width: 900px)');
@@ -68,7 +70,7 @@ export class SavedForLaterContainerComponent implements OnInit {
 
       this.store.dispatch(
         new RemoveCourse({
-          fromTermCode: new TermCode(course.termCode),
+          fromTermCode: this.termCodeService.fromString(course.termCode),
           recordId: course.id as number,
         }),
       );
diff --git a/src/app/degree-planner/services/api.service.ts b/src/app/degree-planner/services/api.service.ts
index e9b6823fc18e67247b1e6e2c2122a6ac2e1ba278..20f9b7dc253fdcc7055f1a6d2639ff95a8400d90 100644
--- a/src/app/degree-planner/services/api.service.ts
+++ b/src/app/degree-planner/services/api.service.ts
@@ -13,8 +13,9 @@ import { DegreePlan } from '@app/core/models/degree-plan';
 import { Profile } from '@app/core/models/profile';
 import { SavedForLaterCourseBase } from '@app/core/models/saved-for-later-course';
 import { SearchResults } from '@app/core/models/search-results';
-import { TermCode } from '@app/core/models/termcode';
+import { TermCode } from '../shared/term-codes/termcode';
 import { StudentInfo } from '@app/core/models/student-info';
+import { RawTermCode } from '../shared/term-codes/without-era';
 
 const HTTP_OPTIONS = {
   headers: new HttpHeaders({
@@ -111,7 +112,7 @@ export class DegreePlannerApiService {
     const url = `/api/search/v1/terms`;
     return this.http
       .get<Term[]>(url, HTTP_OPTIONS)
-      .pipe(map(terms => terms.map(TermCode.fromTerm)));
+      .pipe(map(terms => terms.map(term => new RawTermCode(term.termCode))));
   }
 
   public getAllNotes(roadmapId: number): Observable<Note[]> {
diff --git a/src/app/degree-planner/services/constants.service.ts b/src/app/degree-planner/services/constants.service.ts
index 7adef7968e7631dbe6ee58f014633a033fadc9e3..be64957e69687602cd848a7d3d7ac430f937770f 100644
--- a/src/app/degree-planner/services/constants.service.ts
+++ b/src/app/degree-planner/services/constants.service.ts
@@ -4,12 +4,10 @@ import { environment } from './../../../environments/environment';
 import { forkJoinWithKeys } from '@app/degree-planner/shared/utils';
 import { DegreePlannerApiService } from '@app/degree-planner/services/api.service';
 import { map, tap } from 'rxjs/operators';
-import { TermCode } from '@app/core/models/termcode';
 import { SubjectDescription, SubjectCodesTo } from '@app/core/models/course';
 import { StudentInfo } from '@app/core/models/student-info';
 
 export interface ConstantData {
-  activeTermCodes: TermCode[];
   studentInfo: Partial<StudentInfo>;
   subjectDescriptions: SubjectCodesTo<SubjectDescription>;
 }
@@ -18,25 +16,12 @@ export interface ConstantData {
 export class ConstantsService implements Resolve<ConstantData> {
   private version = environment.version;
   private constants: ConstantData = {
-    activeTermCodes: [],
     studentInfo: {},
     subjectDescriptions: {},
   };
 
   constructor(private api: DegreePlannerApiService) {}
 
-  public firstActiveTermCode(): TermCode {
-    if (this.constants.activeTermCodes.length === 0) {
-      throw new Error(`tried to use the active term before it was loaded`);
-    }
-
-    return this.constants.activeTermCodes[0];
-  }
-
-  public activeTermCodes(): ReadonlyArray<TermCode> {
-    return this.constants.activeTermCodes;
-  }
-
   public getStudentInfo() {
     return this.constants.studentInfo;
   }
@@ -69,8 +54,6 @@ export class ConstantsService implements Resolve<ConstantData> {
   }
 
   public resolve() {
-    const activeTermCodes = this.api.getActiveTermCodes();
-
     const studentInfo = this.api.getStudentInfo();
 
     const subjectDescriptions = forkJoinWithKeys({
@@ -101,7 +84,6 @@ export class ConstantsService implements Resolve<ConstantData> {
     );
 
     return forkJoinWithKeys({
-      activeTermCodes,
       studentInfo,
       subjectDescriptions,
     }).pipe(tap(constants => (this.constants = constants)));
diff --git a/src/app/degree-planner/services/termcode.factory.ts b/src/app/degree-planner/services/termcode.factory.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bcdef14eab0148161b5659e60e783e000587ff36
--- /dev/null
+++ b/src/app/degree-planner/services/termcode.factory.ts
@@ -0,0 +1,74 @@
+import { Injectable } from '@angular/core';
+import { RawYearCode, RawTermCode } from '../shared/term-codes/without-era';
+import { YearCode } from '../shared/term-codes/yearcode';
+import { TermCode, Era } from '../shared/term-codes/termcode';
+
+@Injectable({ providedIn: 'root' })
+export class TermCodeFactory {
+  private state: 'uninitialized' | 'initialized' = 'uninitialized';
+  public active: ReadonlyArray<TermCode> = [];
+
+  private static pickEra(code: RawTermCode, active: ReadonlyArray<TermCode>) {
+    const isActive = active.some(t => t.equals(code));
+    if (isActive) {
+      return Era.Active;
+    }
+
+    const beforeActive = active.every(t => t.comesAfter(code));
+    if (beforeActive) {
+      return Era.Past;
+    } else {
+      return Era.Future;
+    }
+  }
+
+  private requireInitialization() {
+    if (this.state !== 'initialized') {
+      throw new Error('cannot use TermCodeFactory without active terms');
+    }
+  }
+
+  public setActiveTermCodes(active: RawTermCode[]) {
+    if (this.state !== 'uninitialized') {
+      throw new Error('the TermCodeFactory was not uninitialized');
+    } else if (active.length === 0) {
+      throw new Error('app cannot have 0 active terms, must have at least 1');
+    }
+
+    this.state = 'initialized';
+    this.active = active
+      .sort(RawTermCode.sort)
+      .map(t => new TermCode(t, Era.Active, this.fromRawYearCode(t.yearCode)));
+  }
+
+  public first() {
+    return this.active[0];
+  }
+
+  public fromString(str: string) {
+    this.requireInitialization();
+
+    const raw = new RawTermCode(str);
+    const era = TermCodeFactory.pickEra(raw, this.active);
+    return new TermCode(raw, era, this.fromRawYearCode(raw.yearCode));
+  }
+
+  public fromRawYearCode(raw: RawYearCode | string): YearCode {
+    if (typeof raw === 'string') {
+      raw = new RawYearCode(raw);
+    }
+
+    const fall = TermCodeFactory.pickEra(raw.fall(), this.active);
+    const spring = TermCodeFactory.pickEra(raw.spring(), this.active);
+    const summer = TermCodeFactory.pickEra(raw.summer(), this.active);
+    return new YearCode(raw.toString(), fall, spring, summer);
+  }
+
+  public fromYear(year: YearCode) {
+    return {
+      fall: this.fromString(`${year.toString()}2`),
+      spring: this.fromString(`${year.toString()}4`),
+      summer: this.fromString(`${year.toString()}6`),
+    };
+  }
+}
diff --git a/src/app/degree-planner/shared/course-item/course-item.component.ts b/src/app/degree-planner/shared/course-item/course-item.component.ts
index a464f25c9a0d69b85a0a9310fde1ad6205cc104c..b3e2b251c69885b5855de300c7c9fd8790a6af0e 100644
--- a/src/app/degree-planner/shared/course-item/course-item.component.ts
+++ b/src/app/degree-planner/shared/course-item/course-item.component.ts
@@ -19,9 +19,10 @@ import { DegreePlannerApiService } from '@app/degree-planner/services/api.servic
 import { ConfirmDialogComponent } from '@app/shared/dialogs/confirm-dialog/confirm-dialog.component';
 import { CourseDetailsDialogComponent } from '@app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component';
 import { distinctUntilChanged, filter } from 'rxjs/operators';
-import { TermCode } from '@app/core/models/termcode';
+import { TermCode, Era } from '@app/degree-planner/shared/term-codes/termcode';
 import { PlannedTerm } from '@app/core/models/planned-term';
 import { ConstantsService } from '@app/degree-planner/services/constants.service';
+import { TermCodeFactory } from '@app/degree-planner/services/termcode.factory';
 
 const isntUndefined = <T>(thing: T | undefined): thing is T => {
   return thing !== undefined;
@@ -38,7 +39,7 @@ export class CourseItemComponent implements OnInit {
   @Input() isPastTerm: boolean;
   @Input() disabled: boolean;
   @Input() type: 'saved' | 'course' | 'search';
-  @Input() era?: unknown;
+  @Input() era?: Era;
   visibleTerms: any;
   activeTerm: any;
   public status:
@@ -48,8 +49,7 @@ export class CourseItemComponent implements OnInit {
     | 'NotOfferedInTerm'
     | 'DoesNotExist'
     | 'Normal';
-  public visibleTermCodes$: Observable<string[]>;
-  public droppableTermCodes$: Observable<string[]>;
+  public droppableTermCodes$: Observable<TermCode[]>;
   public term$: Observable<PlannedTerm>;
   public plannedCourses: ReadonlyArray<Course>;
   public toActiveTerm: boolean;
@@ -63,13 +63,14 @@ export class CourseItemComponent implements OnInit {
     private constants: ConstantsService,
     private snackBar: MatSnackBar,
     public mediaMatcher: MediaMatcher,
+    private termCodeFactory: TermCodeFactory,
   ) {
     this.mobileView = mediaMatcher.matchMedia('(max-width: 959px)');
   }
 
   ngOnInit() {
-    const isActive = this.era === 'active';
-    const isPast = this.era === 'past';
+    const isActive = this.era === Era.Active;
+    const isPast = this.era === Era.Past;
     const isNotOffered = this.course.studentEnrollmentStatus === 'NOTOFFERED';
     const doesNotExist = this.course.studentEnrollmentStatus === 'DOESNOTEXIST';
 
@@ -102,7 +103,7 @@ export class CourseItemComponent implements OnInit {
     this.droppableTermCodes$ = this.store.pipe(
       select(selectors.selectAllVisibleYears),
       utils.yearsToDroppableTermCodes(),
-      distinctUntilChanged(utils.compareStringArrays),
+      distinctUntilChanged(utils.compareArrays((a, b) => a.equals(b))),
     );
   }
 
@@ -123,8 +124,7 @@ export class CourseItemComponent implements OnInit {
    *  Handle moving a course to different terms based on course type
    *
    */
-  onMove(termCode: string) {
-    const toTermCode = new TermCode(termCode);
+  onMove(toTermCode: TermCode) {
     this.term$ = this.store.pipe(
       select(selectors.selectVisibleTerm, { termCode: toTermCode }),
       filter(isntUndefined),
@@ -133,7 +133,7 @@ export class CourseItemComponent implements OnInit {
 
     this.term$.subscribe(term => {
       this.plannedCourses = term.plannedCourses;
-      this.toActiveTerm = term.era === 'active';
+      this.toActiveTerm = term.termCode.era === Era.Active;
     });
 
     const isCourseInPlannedCourses = this.plannedCourses.some(
@@ -158,7 +158,7 @@ export class CourseItemComponent implements OnInit {
       case 'course': {
         const id = this.course.id as number;
         const { subjectCode, courseId } = this.course;
-        const from = new TermCode(this.course.termCode);
+        const from = this.termCodeFactory.fromString(this.course.termCode);
         this.store.dispatch(
           new MoveCourseBetweenTerms({
             to: toTermCode,
@@ -220,7 +220,7 @@ export class CourseItemComponent implements OnInit {
       case 'course':
         this.store.dispatch(
           new RemoveCourse({
-            fromTermCode: new TermCode(termCode),
+            fromTermCode: this.termCodeFactory.fromString(termCode),
             recordId: this.course.id as number,
           }),
         );
@@ -255,7 +255,7 @@ export class CourseItemComponent implements OnInit {
         break;
 
       default:
-        if (this.era === 'future') {
+        if (this.era === Era.Future) {
           dialogOptions.text = `This will remove "${
             this.course.title
           }" from your degree plan.`;
@@ -284,7 +284,9 @@ export class CourseItemComponent implements OnInit {
             case 'course':
               this.store.dispatch(
                 new RemoveCourse({
-                  fromTermCode: new TermCode(this.course.termCode),
+                  fromTermCode: this.termCodeFactory.fromString(
+                    this.course.termCode,
+                  ),
                   recordId: this.course.id as number,
                 }),
               );
@@ -320,7 +322,6 @@ export class CourseItemComponent implements OnInit {
       this.snackBar.open(`'${short} ${catalogNumber}' no longer offered`);
       return;
     }
-    console.log('mobile view', this.mobileView);
 
     this.api
       .getCourseDetails(subjectCode, courseId)
diff --git a/src/app/degree-planner/shared/term-codes/termcode.ts b/src/app/degree-planner/shared/term-codes/termcode.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0cfe906f5641b0fb19fcaad81758c99e91ea787d
--- /dev/null
+++ b/src/app/degree-planner/shared/term-codes/termcode.ts
@@ -0,0 +1,31 @@
+import { RawTermCode } from './without-era';
+import { YearCode } from './yearcode';
+
+export enum Era {
+  Past,
+  Active,
+  Future,
+}
+
+export class TermCode extends RawTermCode {
+  public era: Era;
+  public yearCode: YearCode;
+
+  constructor(from: RawTermCode | string, era: Era, yearCode: YearCode) {
+    super(from.toString());
+    this.yearCode = yearCode;
+    this.era = era;
+  }
+
+  public isPast() {
+    return this.era === Era.Past;
+  }
+
+  public isActive() {
+    return this.era === Era.Active;
+  }
+
+  public isFuture() {
+    return this.era === Era.Future;
+  }
+}
diff --git a/src/app/core/models/termcode.ts b/src/app/degree-planner/shared/term-codes/without-era.ts
similarity index 60%
rename from src/app/core/models/termcode.ts
rename to src/app/degree-planner/shared/term-codes/without-era.ts
index 6f51c5a48c676d6e4ff34b3f755987ea33fd665e..05be64cdd4542c8d5f07915f21bb46aaf10b22a2 100644
--- a/src/app/core/models/termcode.ts
+++ b/src/app/degree-planner/shared/term-codes/without-era.ts
@@ -1,52 +1,74 @@
-import { Term } from './term';
-
 const YEARCODE_PATTERN = /^[01]\d{2}$/i;
 const TERMCODE_PATTERN = /^[01]\d{2}[2346]$/i;
 
-export class YearCode {
+export class RawYearCode {
   public readonly centuryOffset: '0' | '1';
   public readonly yearOffset: string;
 
+  public static isValid(str: string): boolean {
+    return YEARCODE_PATTERN.test(str);
+  }
+
+  public static sort(a: RawYearCode, b: RawYearCode): -1 | 0 | 1 {
+    if (a.comesBefore(b)) {
+      return -1;
+    }
+
+    if (a.comesAfter(b)) {
+      return 1;
+    }
+
+    return 0;
+  }
+
+  public static fromString(str: string): RawYearCode {
+    return new RawYearCode(str);
+  }
+
   public get fromYear(): number {
     const century = 1900 + parseInt(this.centuryOffset, 10) * 100;
     const year = century + parseInt(this.yearOffset, 10) - 1;
     return year;
   }
 
-  public get fall(): TermCode {
-    return new TermCode(`${this.toString()}2`);
+  public get toYear(): number {
+    return this.fromYear + 1;
   }
 
-  public get spring(): TermCode {
-    return new TermCode(`${this.toString()}4`);
+  public fall() {
+    return new RawTermCode(`${this.toString()}2`);
   }
 
-  public get summer(): TermCode {
-    return new TermCode(`${this.toString()}6`);
+  public spring() {
+    return new RawTermCode(`${this.toString()}4`);
   }
 
-  public get toYear(): number {
-    return this.fromYear + 1;
+  public summer() {
+    return new RawTermCode(`${this.toString()}6`);
   }
 
   constructor(str: string) {
-    if (YearCode.isValid(str) === false) {
+    if (typeof str !== 'string') {
+      throw new Error(`constructor expected string, got ${typeof str}`);
+    }
+
+    if (RawYearCode.isValid(str) === false) {
       throw new Error(`'${str}' is not a valid year code`);
     }
 
-    this.centuryOffset = str.substr(0, 1) as YearCode['centuryOffset'];
-    this.yearOffset = str.substr(1, 2) as YearCode['yearOffset'];
+    this.centuryOffset = str.substr(0, 1) as RawYearCode['centuryOffset'];
+    this.yearOffset = str.substr(1, 2) as RawYearCode['yearOffset'];
   }
 
-  public equals(other: YearCode): boolean {
+  public equals(other: RawYearCode): boolean {
     return this.toString() === other.toString();
   }
 
-  public comesBefore(other: YearCode): boolean {
+  public comesBefore(other: RawYearCode): boolean {
     return this.toString() < other.toString();
   }
 
-  public comesAfter(other: YearCode): boolean {
+  public comesAfter(other: RawYearCode): boolean {
     return this.toString() > other.toString();
   }
 
@@ -60,12 +82,21 @@ export class YearCode {
   public toString() {
     return `${this.centuryOffset}${this.yearOffset}`;
   }
+}
+
+export class RawTermCode {
+  public readonly yearCode: RawYearCode;
+  public readonly termId: '2' | '3' | '4' | '6';
+
+  public static describe(str: string): string {
+    return new RawTermCode(str).description;
+  }
 
   public static isValid(str: string): boolean {
-    return YEARCODE_PATTERN.test(str);
+    return TERMCODE_PATTERN.test(str);
   }
 
-  public static sort(a: YearCode, b: YearCode): -1 | 0 | 1 {
+  public static sort(a: RawTermCode, b: RawTermCode): -1 | 0 | 1 {
     if (a.comesBefore(b)) {
       return -1;
     }
@@ -77,15 +108,6 @@ export class YearCode {
     return 0;
   }
 
-  public static fromString(str: string): YearCode {
-    return new YearCode(str);
-  }
-}
-
-export class TermCode {
-  public readonly yearCode: YearCode;
-  public readonly termId: '2' | '3' | '4' | '6';
-
   public get termName(): 'fall' | 'spring' | 'summer' {
     switch (this.termId) {
       case '2':
@@ -111,23 +133,31 @@ export class TermCode {
   }
 
   constructor(str: string) {
-    if (TermCode.isValid(str) === false) {
+    if (typeof str !== 'string') {
+      throw new Error(`constructor expected string, got ${typeof str}`);
+    }
+
+    if (RawTermCode.isValid(str) === false) {
       throw new Error(`'${str}' is not a valid term code`);
     }
 
-    this.yearCode = new YearCode(str.substr(0, 3));
-    this.termId = str.substr(3, 1) as TermCode['termId'];
+    if (typeof str !== 'string') {
+      console.log({ str });
+    }
+
+    this.yearCode = new RawYearCode(str.substr(0, 3));
+    this.termId = str.substr(3, 1) as RawTermCode['termId'];
   }
 
-  public equals(other: TermCode): boolean {
+  public equals(other: RawTermCode): boolean {
     return this.toString() === other.toString();
   }
 
-  public comesBefore(other: TermCode): boolean {
+  public comesBefore(other: RawTermCode): boolean {
     return this.toString() < other.toString();
   }
 
-  public comesAfter(other: TermCode): boolean {
+  public comesAfter(other: RawTermCode): boolean {
     return this.toString() > other.toString();
   }
 
@@ -141,28 +171,4 @@ export class TermCode {
   public toString(): string {
     return `${this.yearCode}${this.termId}`;
   }
-
-  public static isValid(str: string): boolean {
-    return TERMCODE_PATTERN.test(str);
-  }
-
-  public static sort(a: TermCode, b: TermCode): -1 | 0 | 1 {
-    if (a.comesBefore(b)) {
-      return -1;
-    }
-
-    if (a.comesAfter(b)) {
-      return 1;
-    }
-
-    return 0;
-  }
-
-  public static fromString(str: string): TermCode {
-    return new TermCode(str);
-  }
-
-  public static fromTerm(term: Term): TermCode {
-    return new TermCode(term.termCode);
-  }
 }
diff --git a/src/app/degree-planner/shared/term-codes/yearcode.ts b/src/app/degree-planner/shared/term-codes/yearcode.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c5c865ca599dd7a1455c383f9e5e879552c2d135
--- /dev/null
+++ b/src/app/degree-planner/shared/term-codes/yearcode.ts
@@ -0,0 +1,23 @@
+import { RawYearCode } from './without-era';
+import { Era, TermCode } from './termcode';
+
+export class YearCode extends RawYearCode {
+  private eras: { fall: Era; spring: Era; summer: Era };
+
+  constructor(str: string, fall: Era, spring: Era, summer: Era) {
+    super(str);
+    this.eras = { fall, spring, summer };
+  }
+
+  public fall() {
+    return new TermCode(super.fall(), this.eras.fall, this);
+  }
+
+  public spring() {
+    return new TermCode(super.spring(), this.eras.spring, this);
+  }
+
+  public summer() {
+    return new TermCode(super.summer(), this.eras.summer, this);
+  }
+}
diff --git a/src/app/degree-planner/shared/utils.ts b/src/app/degree-planner/shared/utils.ts
index f2be7be3a9ff38243a23095bd41bfdcb46630360..190c2eafcc4465ab88573e2db72851c2e6da184a 100644
--- a/src/app/degree-planner/shared/utils.ts
+++ b/src/app/degree-planner/shared/utils.ts
@@ -1,78 +1,22 @@
 import { Observable, forkJoin } from 'rxjs';
 import { map } from 'rxjs/operators';
-import { PlannedTermEra, PlannedTerm } from '@app/core/models/planned-term';
 import { YearMapping } from '@app/core/models/year';
-import { YearCode, TermCode } from '@app/core/models/termcode';
-
-export const isValidTermCode = (anything: any): anything is string => {
-  return /^\d{4}$/.test(anything);
-};
-
-export const pickTermName = (termOffset: string) => {
-  switch (termOffset) {
-    case '2':
-    case '3':
-      return 'fall';
-    case '4':
-      return 'spring';
-    case '6':
-      return 'summer';
-    default:
-      throw new Error(`'${termOffset}' is not a valid term offset`);
-  }
-};
-
-export const pickTermEra = (
-  termCode: TermCode,
-  activeTermCodes: ReadonlyArray<TermCode>,
-): PlannedTermEra => {
-  const noActiveTermCodes = activeTermCodes.length === 0;
-  const isActiveTermCode = activeTermCodes.some(tc => tc.equals(termCode));
-  const beforeAllActiveTermCodes = activeTermCodes.every(tc =>
-    tc.comesAfter(termCode),
-  );
-  if (noActiveTermCodes || isActiveTermCode) {
-    return 'active';
-  } else if (beforeAllActiveTermCodes) {
-    return 'past';
-  } else {
-    return 'future';
-  }
-};
-
-export const pickYearEra = (
-  yearCode: YearCode,
-  activeTermCodes: ReadonlyArray<TermCode>,
-): PlannedTermEra => {
-  const activeYearCodes = activeTermCodes.map(tc => tc.yearCode);
-  const noActiveYearCodes = activeYearCodes.length === 0;
-  const isActiveYearCode = activeYearCodes.some(yc => yc.equals(yearCode));
-  const beforeAllActiveYearCodes = activeYearCodes.every(yc =>
-    yc.comesAfter(yearCode),
-  );
-  if (noActiveYearCodes || isActiveYearCode) {
-    return 'active';
-  } else if (beforeAllActiveYearCodes) {
-    return 'past';
-  } else {
-    return 'future';
-  }
-};
+import { TermCode, Era } from './term-codes/termcode';
 
 export const yearsToDropZoneIds = () => (years$: Observable<YearMapping>) => {
   return years$.pipe(
     map(years => {
       const yearCodes = Object.keys(years);
       const termCodes = yearCodes.reduce<string[]>((acc, yearCode) => {
-        if (years[yearCode].fall.era !== 'past') {
+        if (years[yearCode].fall.termCode.era !== Era.Past) {
           acc = acc.concat(years[yearCode].fall.termCode.toString());
         }
 
-        if (years[yearCode].spring.era !== 'past') {
+        if (years[yearCode].spring.termCode.era !== Era.Past) {
           acc = acc.concat(years[yearCode].spring.termCode.toString());
         }
 
-        if (years[yearCode].summer.era !== 'past') {
+        if (years[yearCode].summer.termCode.era !== Era.Past) {
           acc = acc.concat(years[yearCode].summer.termCode.toString());
         }
 
@@ -100,33 +44,23 @@ export const compareArrays = <T>(cmp: (a: T, b: T) => boolean) => {
 
 export const compareStringArrays = compareArrays<string>((a, b) => a === b);
 
-export const yearsToYearCodes = () => (years$: Observable<YearMapping>) => {
-  return years$.pipe(
-    map(years => {
-      const yearCodes = Object.keys(years);
-      const sortedYearCodes = yearCodes.sort();
-      return sortedYearCodes;
-    }),
-  );
-};
-
 export const yearsToDroppableTermCodes = () => (
   years$: Observable<YearMapping>,
 ) => {
   return years$.pipe(
     map(years => {
       const yearCodes = Object.keys(years);
-      return yearCodes.reduce<string[]>((acc, yearCode) => {
-        if (years[yearCode].fall.era !== 'past') {
-          acc = acc.concat(years[yearCode].fall.termCode.toString());
+      return yearCodes.reduce<TermCode[]>((acc, yearCode) => {
+        if (years[yearCode].fall.termCode.era !== Era.Past) {
+          acc = acc.concat(years[yearCode].fall.termCode);
         }
 
-        if (years[yearCode].spring.era !== 'past') {
-          acc = acc.concat(years[yearCode].spring.termCode.toString());
+        if (years[yearCode].spring.termCode.era !== Era.Past) {
+          acc = acc.concat(years[yearCode].spring.termCode);
         }
 
-        if (years[yearCode].summer.era !== 'past') {
-          acc = acc.concat(years[yearCode].summer.termCode.toString());
+        if (years[yearCode].summer.termCode.era !== Era.Past) {
+          acc = acc.concat(years[yearCode].summer.termCode);
         }
 
         return acc;
diff --git a/src/app/degree-planner/store/actions/course.actions.ts b/src/app/degree-planner/store/actions/course.actions.ts
index 16bf00a03b60aeb5476697b7d0ff37fc3807eeab..df0a07f269bd3c8ede05300546c8e7768fbc31a9 100644
--- a/src/app/degree-planner/store/actions/course.actions.ts
+++ b/src/app/degree-planner/store/actions/course.actions.ts
@@ -1,6 +1,6 @@
 import { Action } from '@ngrx/store';
 import { Course } from '@app/core/models/course';
-import { TermCode } from '@app/core/models/termcode';
+import { TermCode } from '@app/degree-planner/shared/term-codes/termcode';
 
 export enum CourseActionTypes {
   AddCourse = '[Course] Add',
@@ -80,7 +80,9 @@ export class AddCourse implements Action {
 
 export class AddCourseSuccess implements Action {
   public readonly type = CourseActionTypes.AddCourseSuccess;
-  constructor(public payload: { course: Course; newIndex?: number }) {}
+  constructor(
+    public payload: { termCode: TermCode; course: Course; newIndex?: number },
+  ) {}
 }
 
 export class RemoveCourse implements Action {
diff --git a/src/app/degree-planner/store/actions/note.actions.ts b/src/app/degree-planner/store/actions/note.actions.ts
index f1fed10160b85118aea2eb76d6aa452695697097..be7299f2b70740ec764d91429dca7dafe286421d 100644
--- a/src/app/degree-planner/store/actions/note.actions.ts
+++ b/src/app/degree-planner/store/actions/note.actions.ts
@@ -1,6 +1,6 @@
 import { Action } from '@ngrx/store';
 import { Note } from '@app/core/models/note';
-import { TermCode } from '@app/core/models/termcode';
+import { TermCode } from '@app/degree-planner/shared/term-codes/termcode';
 
 export enum NoteActionTypes {
   WriteNote = '[Note] Write',
@@ -19,7 +19,7 @@ export class WriteNote implements Action {
 
 export class WriteNoteSuccess implements Action {
   public readonly type = NoteActionTypes.WriteNoteSuccess;
-  constructor(public payload: { updatedNote: Note }) {}
+  constructor(public payload: { termCode: TermCode; updatedNote: Note }) {}
 }
 
 export class DeleteNote implements Action {
diff --git a/src/app/degree-planner/store/actions/ui.actions.ts b/src/app/degree-planner/store/actions/ui.actions.ts
index 76b57e7ed454ad7a2938903174db0a96d1f237c5..90fbe3013e6593dbd8c68be4f63a1a4b0c414229 100644
--- a/src/app/degree-planner/store/actions/ui.actions.ts
+++ b/src/app/degree-planner/store/actions/ui.actions.ts
@@ -1,6 +1,7 @@
 import { Action } from '@ngrx/store';
-import { YearCode, TermCode } from '@app/core/models/termcode';
 import { UserPreferences } from '@app/core/models/user-preferences';
+import { YearCode } from '@app/degree-planner/shared/term-codes/yearcode';
+import { TermCode } from '@app/degree-planner/shared/term-codes/termcode';
 
 export enum UIActionTypes {
   ToggleAcademicYear = '[UI] Toggle Academic Year',
diff --git a/src/app/degree-planner/store/effects/course.effects.ts b/src/app/degree-planner/store/effects/course.effects.ts
index ab36ab3e22134ed40efda860c708727b202dd477..bb54b17451c7bbb68bb927f767a4ffbf99106933 100644
--- a/src/app/degree-planner/store/effects/course.effects.ts
+++ b/src/app/degree-planner/store/effects/course.effects.ts
@@ -31,7 +31,7 @@ import {
 } from '@app/degree-planner/store/actions/course.actions';
 import { DegreePlan } from '@app/core/models/degree-plan';
 import { Course, CourseBase } from '@app/core/models/course';
-import { TermCode } from '@app/core/models/termcode';
+import { TermCode } from '@app/degree-planner/shared/term-codes/termcode';
 import { ConstantsService } from '@app/degree-planner/services/constants.service';
 
 @Injectable()
@@ -56,7 +56,6 @@ export class CourseEffects {
     // Get term data for the degree plan specified by the roadmap ID.
     flatMap(([action, degreePlan]) => {
       const roadmapId = degreePlan.roadmapId;
-      const activeTerms = this.constants.activeTermCodes();
       const {
         id: recordId,
         to: toTermCode,
@@ -64,15 +63,13 @@ export class CourseEffects {
         courseId,
       } = action.payload;
 
-      const toActiveTerm = activeTerms.some(t => t.equals(toTermCode));
-
       const moveCourse = this.api.updateCourseTerm(
         roadmapId,
         recordId,
         toTermCode,
       );
 
-      if (toActiveTerm) {
+      if (toTermCode.isActive()) {
         /**
          * The `updateCourseTerm` API won't force cart validation which we want
          * if we're adding a course to the cart. Calling the `addCourseToCart`
@@ -119,13 +116,11 @@ export class CourseEffects {
     flatMap(([action, visibleDegreePlan]) => {
       // TODO error handle the API calls
       const roadmapId = (visibleDegreePlan as DegreePlan).roadmapId;
-      const activeTerms = this.constants.activeTermCodes();
       const { subjectCode, termCode, courseId, newIndex } = action.payload;
 
-      const isActiveTerm = activeTerms.some(term => term.equals(termCode));
       const isPrimaryPlan = (visibleDegreePlan as DegreePlan).primary;
       const addCourse$ =
-        isActiveTerm && isPrimaryPlan
+        termCode.isActive() && isPrimaryPlan
           ? this.api.addCourseToCart(subjectCode, courseId, termCode)
           : this.api.addCourse(roadmapId, subjectCode, courseId, termCode);
 
@@ -137,7 +132,7 @@ export class CourseEffects {
       );
 
       const toSuccessAction$ = courseBaseToCourse$.pipe(
-        map(course => new AddCourseSuccess({ course, newIndex })),
+        map(course => new AddCourseSuccess({ termCode, course, newIndex })),
       );
 
       return toSuccessAction$;
@@ -145,7 +140,7 @@ export class CourseEffects {
 
     tap(state => {
       const touchedCourse = state.payload.course;
-      const touchedTerm = new TermCode(touchedCourse.termCode).description;
+      const touchedTerm = TermCode.describe(touchedCourse.termCode);
       const subject = this.constants.subjectDescription(
         touchedCourse.subjectCode,
       ).short;
diff --git a/src/app/degree-planner/store/effects/note.effects.ts b/src/app/degree-planner/store/effects/note.effects.ts
index 30de10d1f67b21d53830be0c5d6bc3b87cd0f801..ebb3a95a246dd330639d42459d24af410ef14d43 100644
--- a/src/app/degree-planner/store/effects/note.effects.ts
+++ b/src/app/degree-planner/store/effects/note.effects.ts
@@ -31,7 +31,7 @@ import {
 } from '@app/degree-planner/store/actions/note.actions';
 import * as selectors from '@app/degree-planner/store/selectors';
 import { GlobalState } from '@app/core/state';
-import { TermCode } from '@app/core/models/termcode';
+import { TermCode } from '@app/degree-planner/shared/term-codes/termcode';
 
 @Injectable()
 export class NoteEffects {
@@ -66,17 +66,21 @@ export class NoteEffects {
       if (existingNote !== undefined && existingNote.isLoaded) {
         // Since the term DOES have a note, update the existing note
         const noteId = existingNote.id;
-        return this.api.updateNote(planId, termCode, noteText, noteId);
+        return this.api
+          .updateNote(planId, termCode, noteText, noteId)
+          .pipe(
+            map(updatedNote => new WriteNoteSuccess({ termCode, updatedNote })),
+          );
       } else {
         // Since the term DOES NOT have a note, create a new note
-        return this.api.createNote(planId, termCode, noteText);
+        return this.api
+          .createNote(planId, termCode, noteText)
+          .pipe(
+            map(updatedNote => new WriteNoteSuccess({ termCode, updatedNote })),
+          );
       }
     }),
 
-    // Dispatch an `WriteNoteSuccess` action so that the State
-    // object can be updated with the new Note data.
-    map(updatedNote => new WriteNoteSuccess({ updatedNote })),
-
     tap(() => {
       const message = 'Note has been saved';
       this.snackBar.open(message, undefined, {});
diff --git a/src/app/degree-planner/store/effects/plan.effects.ts b/src/app/degree-planner/store/effects/plan.effects.ts
index 33fc2352a55bf40bfa5fbaeb7d18322eecbd3a0f..16722c5bb555d2320b2e149f68a46c03af6fce3b 100644
--- a/src/app/degree-planner/store/effects/plan.effects.ts
+++ b/src/app/degree-planner/store/effects/plan.effects.ts
@@ -33,18 +33,16 @@ import {
   DeletePlanSuccess,
   ChangeGradeVisibility,
 } from '@app/degree-planner/store/actions/plan.actions';
-import * as utils from '@app/degree-planner/shared/utils';
 import { DegreePlan } from '@app/core/models/degree-plan';
 import { PlannedTerm, PlannedTermNote } from '@app/core/models/planned-term';
 import { INITIAL_DEGREE_PLANNER_STATE } from '@app/degree-planner/store/state';
 import { YearMapping, MutableYearMapping } from '@app/core/models/year';
 import { Note } from '@app/core/models/note';
 import { CourseBase, Course } from '@app/core/models/course';
-import { pickTermEra } from '@app/degree-planner/shared/utils';
-import { TermCode, YearCode } from '@app/core/models/termcode';
-import { ConstantsService } from '@app/degree-planner/services/constants.service';
+import { TermCode } from '@app/degree-planner/shared/term-codes/termcode';
 import { Alert } from '@app/core/models/alert';
 import { UpdateUserPreferences } from '../actions/userPreferences.actions';
+import { TermCodeFactory } from '@app/degree-planner/services/termcode.factory';
 
 @Injectable()
 export class DegreePlanEffects {
@@ -53,7 +51,7 @@ export class DegreePlanEffects {
     private api: DegreePlannerApiService,
     private store$: Store<GlobalState>,
     private snackBar: MatSnackBar,
-    private constants: ConstantsService,
+    private termCodeService: TermCodeFactory,
   ) {}
 
   @Effect()
@@ -64,11 +62,13 @@ export class DegreePlanEffects {
       console.log('loading all degree plans');
       return forkJoinWithKeys({
         allDegreePlans: this.api.getAllDegreePlans(),
+        activeTermCodes: this.api.getActiveTermCodes(),
         userPreferences: this.api.getUserPreferences(),
       });
     }),
-    // Load data specific to the primary degree plan.
-    flatMap(({ allDegreePlans, userPreferences }) => {
+
+    flatMap(({ allDegreePlans, activeTermCodes, userPreferences }) => {
+      this.termCodeService.setActiveTermCodes(activeTermCodes);
       const savedForLaterCourses = this.api.getSavedForLaterCourses();
       const visibleDegreePlan = userPreferences.degreePlannerSelectedPlan
         ? pickDegreePlanById(
@@ -79,7 +79,7 @@ export class DegreePlanEffects {
       const visibleYears = loadPlanYears(
         this.api,
         visibleDegreePlan.roadmapId,
-        this.constants,
+        this.termCodeService,
       );
 
       const alerts: Alert[] = [];
@@ -120,7 +120,6 @@ export class DegreePlanEffects {
       return new InitialLoadSuccess({
         ...INITIAL_DEGREE_PLANNER_STATE,
         ...payload,
-        activeTermCodes: this.constants.activeTermCodes(),
         isLoadingPlan: false,
       });
     }),
@@ -147,7 +146,7 @@ export class DegreePlanEffects {
       const visibleYears = loadPlanYears(
         this.api,
         visibleDegreePlan.roadmapId,
-        this.constants,
+        this.termCodeService,
       );
 
       return forkJoinWithKeys({
@@ -271,7 +270,7 @@ export class DegreePlanEffects {
           const newYears = loadPlanYears(
             this.api,
             newPlan.roadmapId,
-            this.constants,
+            this.termCodeService,
           );
 
           return forkJoinWithKeys({
@@ -356,10 +355,6 @@ const matchesTermCode = (termCode: TermCode) => (thing: {
   return thing.termCode === termCode.toString();
 };
 
-const toYearCode = (termCode: string) => {
-  return termCode.substr(0, 3);
-};
-
 const buildTerm = (
   roadmapId: number,
   termCode: TermCode,
@@ -368,7 +363,6 @@ const buildTerm = (
     termCode: string;
     courses: ReadonlyArray<CourseBase>;
   }>,
-  constants: ConstantsService,
 ): PlannedTerm => {
   const baseNote = notes.find(matchesTermCode(termCode));
   const note: PlannedTermNote | undefined = baseNote
@@ -390,11 +384,9 @@ const buildTerm = (
     plannedCourses.push(course);
   });
 
-  const era = pickTermEra(termCode, constants.activeTermCodes());
   return {
     roadmapId,
     termCode,
-    era,
     note,
     plannedCourses,
     enrolledCourses,
@@ -404,7 +396,7 @@ const buildTerm = (
 const loadPlanYears = (
   api: DegreePlannerApiService,
   roadmapId: number,
-  constants: ConstantsService,
+  termCodeService: TermCodeFactory,
 ): Observable<YearMapping> => {
   const notesAndCourses$ = forkJoinWithKeys({
     notes: api.getAllNotes(roadmapId),
@@ -418,11 +410,11 @@ const loadPlanYears = (
       const allTermCodes = [
         ...noteTermCodes,
         ...courseTermCodes,
-        ...constants.activeTermCodes().map(tc => tc.toString()),
-      ].map(TermCode.fromString);
+        ...termCodeService.active.map(t => t.toString()),
+      ].map(t => termCodeService.fromString(t));
       const uniqueYearCodes = unique(
         allTermCodes.map(tc => tc.yearCode.toString()),
-      ).map(yearCodeStr => new YearCode(yearCodeStr));
+      ).map(yearCodeStr => termCodeService.fromRawYearCode(yearCodeStr));
       return {
         uniqueYearCodes,
         notes,
@@ -435,25 +427,13 @@ const loadPlanYears = (
     map(({ uniqueYearCodes, notes, courses }) => {
       const mapping: MutableYearMapping = {};
       uniqueYearCodes.forEach(yearCode => {
+        const { fall, spring, summer } = termCodeService.fromYear(yearCode);
         mapping[yearCode.toString()] = {
           yearCode,
-          isExpanded:
-            utils.pickYearEra(yearCode, constants.activeTermCodes()) !== 'past',
-          fall: buildTerm(roadmapId, yearCode.fall, notes, courses, constants),
-          spring: buildTerm(
-            roadmapId,
-            yearCode.spring,
-            notes,
-            courses,
-            constants,
-          ),
-          summer: buildTerm(
-            roadmapId,
-            yearCode.summer,
-            notes,
-            courses,
-            constants,
-          ),
+          isExpanded: !(fall.isPast() && spring.isPast() && summer.isPast()),
+          fall: buildTerm(roadmapId, fall, notes, courses),
+          spring: buildTerm(roadmapId, spring, notes, courses),
+          summer: buildTerm(roadmapId, summer, notes, courses),
         };
       });
 
diff --git a/src/app/degree-planner/store/reducer.ts b/src/app/degree-planner/store/reducer.ts
index a0ed618a3a860e390700df3a00ee9298ef9e274a..22ccb8202c366106649e733f829d2005f7aba444 100644
--- a/src/app/degree-planner/store/reducer.ts
+++ b/src/app/degree-planner/store/reducer.ts
@@ -1,4 +1,3 @@
-import { pickTermEra } from '@app/degree-planner/shared/utils';
 import { UIActionTypes, CloseSidenav, OpenSidenav } from './actions/ui.actions';
 
 import {
@@ -59,13 +58,9 @@ import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course';
 import { DegreePlan } from '@app/core/models/degree-plan';
 import { Year, YearMapping } from '@app/core/models/year';
 import { Course } from '@app/core/models/course';
-import {
-  PlannedTerm,
-  PlannedTermEra,
-  PlannedTermNote,
-} from '@app/core/models/planned-term';
-import * as utils from '@app/degree-planner/shared/utils';
-import { TermCode, YearCode } from '@app/core/models/termcode';
+import { PlannedTerm, PlannedTermNote } from '@app/core/models/planned-term';
+import { YearCode } from '@app/degree-planner/shared/term-codes/yearcode';
+import { TermCode, Era } from '@app/degree-planner/shared/term-codes/termcode';
 
 type SupportedActions =
   | PlanError
@@ -150,11 +145,15 @@ export function degreePlannerReducer(
           return parseInt(yearCode, 10);
         }),
       );
-      const nextYearCode = new YearCode(`${largestYearCode + 1}`);
+      const nextYearCode = new YearCode(
+        `${largestYearCode + 1}`,
+        Era.Future,
+        Era.Future,
+        Era.Future,
+      );
       const nextYear = emptyYear(
         (state.visibleDegreePlan as DegreePlan).roadmapId,
         nextYearCode,
-        state.activeTermCodes,
       );
       const visibleYears: YearMapping = {
         ...state.visibleYears,
@@ -304,7 +303,6 @@ export function degreePlannerReducer(
           (state.visibleDegreePlan as DegreePlan).roadmapId,
           termCode,
           newNote,
-          state.activeTermCodes,
           state.visibleYears[yearCode.toString()],
         ),
       };
@@ -322,8 +320,7 @@ export function degreePlannerReducer(
      *  - *OR* adds a new term with the given note if no term exists with the note's termCode.
      */
     case NoteActionTypes.WriteNoteSuccess: {
-      const { updatedNote } = action.payload;
-      const termCode = new TermCode(updatedNote.termCode);
+      const { termCode, updatedNote } = action.payload;
       const { yearCode } = termCode;
 
       const visibleYears: YearMapping = {
@@ -332,7 +329,6 @@ export function degreePlannerReducer(
           (state.visibleDegreePlan as DegreePlan).roadmapId,
           termCode,
           { isLoaded: true, text: updatedNote.note, id: updatedNote.id },
-          state.activeTermCodes,
           state.visibleYears[yearCode.toString()],
         ),
       };
@@ -354,7 +350,6 @@ export function degreePlannerReducer(
         [yearCode.toString()]: createYearWithoutNote(
           (state.visibleDegreePlan as DegreePlan).roadmapId,
           termCode,
-          state.activeTermCodes,
           state.visibleYears[yearCode.toString()],
         ),
       };
@@ -426,7 +421,6 @@ export function degreePlannerReducer(
           (state.visibleDegreePlan as DegreePlan).roadmapId,
           fromTermCode,
           course.id,
-          state.activeTermCodes,
           state.visibleYears[fromYearCode.toString()],
         );
 
@@ -434,7 +428,6 @@ export function degreePlannerReducer(
           (state.visibleDegreePlan as DegreePlan).roadmapId,
           toTermCode,
           { ...course, termCode: toTermCode.toString() },
-          state.activeTermCodes,
           fromYearCode.equals(toYearCode)
             ? fromYear
             : state.visibleYears[toYearCode.toString()],
@@ -466,7 +459,6 @@ export function degreePlannerReducer(
         (state.visibleDegreePlan as DegreePlan).roadmapId,
         termCode,
         course,
-        state.activeTermCodes,
         state.visibleYears[termCode.yearCode.toString()],
         newIndex,
       );
@@ -480,15 +472,13 @@ export function degreePlannerReducer(
     }
 
     case CourseActionTypes.AddCourseSuccess: {
-      const { course, newIndex } = action.payload;
-      const termCode = new TermCode(course.termCode);
+      const { termCode, course, newIndex } = action.payload;
       const { yearCode } = termCode;
 
       const year: Year = createYearWithCourse(
         (state.visibleDegreePlan as DegreePlan).roadmapId,
         termCode,
         course,
-        state.activeTermCodes,
         state.visibleYears[yearCode.toString()],
         newIndex,
       );
@@ -509,7 +499,6 @@ export function degreePlannerReducer(
         (state.visibleDegreePlan as DegreePlan).roadmapId,
         fromTermCode,
         recordId,
-        state.activeTermCodes,
         state.visibleYears[yearCode.toString()],
       );
 
@@ -656,54 +645,36 @@ export function degreePlannerReducer(
   }
 }
 
-const emptyTerm = (
-  roadmapId: number,
-  termCode: TermCode,
-  era: PlannedTermEra,
-): PlannedTerm => {
-  return { roadmapId, termCode, era, plannedCourses: [], enrolledCourses: [] };
+const emptyTerm = (roadmapId: number, termCode: TermCode): PlannedTerm => {
+  return { roadmapId, termCode, plannedCourses: [], enrolledCourses: [] };
 };
 
-const emptyYear = (
-  roadmapId: number,
-  yearCode: YearCode,
-  activeTermCodes: ReadonlyArray<TermCode>,
-): Year => {
+const emptyYear = (roadmapId: number, yearCode: YearCode): Year => {
   return {
     yearCode,
-    isExpanded: utils.pickYearEra(yearCode, activeTermCodes) !== 'past',
-    fall: emptyTerm(
-      roadmapId,
-      yearCode.fall,
-      pickTermEra(yearCode.fall, activeTermCodes),
-    ),
-    spring: emptyTerm(
-      roadmapId,
-      yearCode.spring,
-      pickTermEra(yearCode.spring, activeTermCodes),
-    ),
-    summer: emptyTerm(
-      roadmapId,
-      yearCode.summer,
-      pickTermEra(yearCode.summer, activeTermCodes),
+    isExpanded: !(
+      yearCode.fall().isPast() &&
+      yearCode.spring().isPast() &&
+      yearCode.summer().isPast()
     ),
+    fall: emptyTerm(roadmapId, yearCode.fall()),
+    spring: emptyTerm(roadmapId, yearCode.spring()),
+    summer: emptyTerm(roadmapId, yearCode.summer()),
   };
 };
 
 const generateYearForTermCode = (
   roadmapId: number,
   termCode: TermCode,
-  activeTermCodes: ReadonlyArray<TermCode>,
 ): Year => {
-  return emptyYear(roadmapId, termCode.yearCode, activeTermCodes);
+  return emptyYear(roadmapId, termCode.yearCode);
 };
 
 const createYearWithNote = (
   roadmapId: number,
   termCode: TermCode,
   note: PlannedTermNote | undefined,
-  activeTermCodes: ReadonlyArray<TermCode>,
-  year = generateYearForTermCode(roadmapId, termCode, activeTermCodes),
+  year = generateYearForTermCode(roadmapId, termCode),
 ): Year => {
   const term = year[termCode.termName];
   return { ...year, [termCode.termName]: { ...term, note } };
@@ -712,16 +683,9 @@ const createYearWithNote = (
 const createYearWithoutNote = (
   roadmapId: number,
   termCode: TermCode,
-  activeTermCodes: ReadonlyArray<TermCode>,
   year?: Year,
 ) => {
-  return createYearWithNote(
-    roadmapId,
-    termCode,
-    undefined,
-    activeTermCodes,
-    year,
-  );
+  return createYearWithNote(roadmapId, termCode, undefined, year);
 };
 
 const findCourse = (
@@ -740,8 +704,7 @@ const createYearWithCourse = (
   roadmapId: number,
   termCode: TermCode,
   course: Course,
-  activeTermCodes: ReadonlyArray<TermCode>,
-  year = generateYearForTermCode(roadmapId, termCode, activeTermCodes),
+  year = generateYearForTermCode(roadmapId, termCode),
   newIndex?: number,
 ): Year => {
   const term = year[termCode.termName];
@@ -757,8 +720,7 @@ const createYearWithoutCourse = (
   roadmapId: number,
   termCode: TermCode,
   recordId: number,
-  activeTermCodes: ReadonlyArray<TermCode>,
-  year = generateYearForTermCode(roadmapId, termCode, activeTermCodes),
+  year = generateYearForTermCode(roadmapId, termCode),
 ): Year => {
   const term = year[termCode.termName];
   const courses = term.plannedCourses.filter(course => course.id !== recordId);
diff --git a/src/app/degree-planner/store/selectors.ts b/src/app/degree-planner/store/selectors.ts
index 3d44099450e88d52a8a2e630b8ce5d9cd4b4a934..444768c5c6ac52ff63a354a3562061a0a52f8bbb 100644
--- a/src/app/degree-planner/store/selectors.ts
+++ b/src/app/degree-planner/store/selectors.ts
@@ -1,7 +1,8 @@
 import { createSelector } from '@ngrx/store';
 import { GlobalState } from '@app/core/state';
 import { DegreePlannerState } from './state';
-import { TermCode, YearCode } from '@app/core/models/termcode';
+import { TermCode } from '@app/degree-planner/shared/term-codes/termcode';
+import { YearCode } from '@app/degree-planner/shared/term-codes/yearcode';
 import { YearMapping, Year } from '@app/core/models/year';
 import { UserPreferences } from '@app/core/models/user-preferences';
 
@@ -95,12 +96,8 @@ export const getSelectedSearchTerm = createSelector(
 export const getActiveSelectedSearchTerm = createSelector(
   getDegreePlannerState,
   (state: DegreePlannerState) => {
-    const { selectedTerm } = state.search;
-    if (
-      selectedTerm &&
-      state.activeTermCodes.some(tc => tc.equals(selectedTerm))
-    ) {
-      return selectedTerm;
+    if (state.search.selectedTerm && state.search.selectedTerm.isActive()) {
+      return state.search.selectedTerm;
     } else {
       return undefined;
     }
diff --git a/src/app/degree-planner/store/state.ts b/src/app/degree-planner/store/state.ts
index 1697917e23c1786c9deae8b1827c2a197ae46415..0976a2a4fd72643f5c44229a7416a4c355fb332f 100644
--- a/src/app/degree-planner/store/state.ts
+++ b/src/app/degree-planner/store/state.ts
@@ -2,7 +2,7 @@ import { YearMapping } from '@app/core/models/year';
 import { DegreePlan } from '@app/core/models/degree-plan';
 import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course';
 import { SubjectCodesTo, SubjectDescription } from '@app/core/models/course';
-import { TermCode } from '@app/core/models/termcode';
+import { TermCode } from '@app/degree-planner/shared/term-codes/termcode';
 import { Alert } from '@app/core/models/alert';
 import { UserPreferences } from '@app/core/models/user-preferences';
 
@@ -10,7 +10,6 @@ export interface DegreePlannerState {
   visibleDegreePlan: DegreePlan | undefined;
   visibleYears: YearMapping;
   savedForLaterCourses: ReadonlyArray<SavedForLaterCourse>;
-  activeTermCodes: ReadonlyArray<TermCode>;
   allDegreePlans: ReadonlyArray<DegreePlan>;
   subjectDescriptions: SubjectCodesTo<SubjectDescription>;
   search: { visible: boolean; selectedTerm?: TermCode };
@@ -25,7 +24,6 @@ export const INITIAL_DEGREE_PLANNER_STATE: DegreePlannerState = {
   visibleDegreePlan: undefined,
   visibleYears: {},
   savedForLaterCourses: [],
-  activeTermCodes: [],
   allDegreePlans: [],
   subjectDescriptions: {},
   search: { visible: false },
diff --git a/src/app/degree-planner/term-container/term-container.component.html b/src/app/degree-planner/term-container/term-container.component.html
index 343124bb86bf95ec47ec9a5fa39f1c0d10668721..2ca5f0a89dd15284eedd34e6a993a64ef55efc47 100644
--- a/src/app/degree-planner/term-container/term-container.component.html
+++ b/src/app/degree-planner/term-container/term-container.component.html
@@ -3,10 +3,10 @@
     <div class="course-list">
       <div class="course-list-inner">
         <ng-container *ngIf="enrolledCourses.length === 0">
-          <p *ngIf="(term$ | async).era === 'past'" class="no-courses">
+          <p *ngIf="(term$ | async).termCode.isPast()" class="no-courses">
             No courses taken
           </p>
-          <p *ngIf="(term$ | async).era === 'active'" class="no-courses">
+          <p *ngIf="(term$ | async).termCode.isActive()" class="no-courses">
             Not enrolled in any courses
           </p>
         </ng-container>
@@ -39,13 +39,13 @@
       <div class="course-list-inner term-body">
         <ng-container
           *ngIf="plannedCourses.length === 0 && !hasItemDraggedOver">
-          <p *ngIf="(term$ | async).era === 'active'" class="no-courses">
+          <p *ngIf="(term$ | async).termCode.isActive()" class="no-courses">
             No Courses in cart
           </p>
-          <p *ngIf="(term$ | async).era === 'future'" class="no-courses">
+          <p *ngIf="(term$ | async).termCode.isFuture()" class="no-courses">
             No courses planned
           </p>
-          <p *ngIf="(term$ | async).era === 'past'" class="no-courses">
+          <p *ngIf="(term$ | async).termCode.isPast()" class="no-courses">
             No courses planned
           </p>
         </ng-container>
@@ -70,7 +70,7 @@
     </div>
 
     <!-- Add course -->
-    <div class="add-new-wrapper" *ngIf="(term$ | async).era !== 'past'">
+    <div class="add-new-wrapper" *ngIf="(term$ | async).termCode.isPast() == false">
       <button
         mat-raised-button
         attr.aria-label="Add course to {{ termCode | getTermDescription }}"
@@ -163,7 +163,7 @@
   </div>
 
   <!-- If this term is an active term -->
-  <ng-container *ngIf="(term$ | async).era === 'active'">
+  <ng-container *ngIf="(term$ | async).termCode.isActive()">
     <mat-tab-group (selectedTabChange)="changeVisibleCredits($event)" [selectedIndex]="(enrolledCourses.length > 0) ? 0 : 1">
       <mat-tab [label]="'In Progress (' + enrolledCourses.length + ')'" aria-label="In progress courses">
         <ng-container cdkFocusinitial *ngTemplateOutlet="enrolled"></ng-container>
@@ -175,7 +175,7 @@
   </ng-container>
 
   <!-- If this term is a past term -->
-  <ng-container *ngIf="(term$ | async).era === 'past'">
+  <ng-container *ngIf="(term$ | async).termCode.isPast()">
     <mat-tab-group (selectedTabChange)="changeVisibleCredits($event)" [selectedIndex]="0">
       <mat-tab [label]="'Completed (' + enrolledCourses.length + ')'" aria-label="Completed courses">
         <ng-container cdkFocusinitial *ngTemplateOutlet="enrolled"></ng-container>
@@ -187,7 +187,7 @@
   </ng-container>
 
   <!-- If this term is a future term -->
-  <ng-container *ngIf="(term$ | async).era === 'future'">
+  <ng-container *ngIf="(term$ | async).termCode.isFuture()">
     <ng-container cdkFocusinitial *ngTemplateOutlet="planned"></ng-container>
   </ng-container>
 </mat-card>
diff --git a/src/app/degree-planner/term-container/term-container.component.ts b/src/app/degree-planner/term-container/term-container.component.ts
index 84eea72148f61a5487455582c55f1f4df02c5ffd..b21a889f0bc4ec54861dd5c42baa76bf7b27d4e5 100644
--- a/src/app/degree-planner/term-container/term-container.component.ts
+++ b/src/app/degree-planner/term-container/term-container.component.ts
@@ -16,7 +16,7 @@ import {
   NotesDialogData,
 } from '@app/degree-planner/dialogs/notes-dialog/notes-dialog.component';
 import * as utils from '@app/degree-planner/shared/utils';
-import { TermCode } from '@app/core/models/termcode';
+import { TermCode, Era } from '@app/degree-planner/shared/term-codes/termcode';
 import { ConfirmDialogComponent } from '@app/shared/dialogs/confirm-dialog/confirm-dialog.component';
 import { MediaMatcher } from '@angular/cdk/layout';
 import { ConstantsService } from '../services/constants.service';
@@ -72,7 +72,6 @@ export class TermContainerComponent implements OnInit, OnDestroy {
   // List of courses pulled for the Observable
   public plannedCourses: ReadonlyArray<Course>;
   public enrolledCourses: ReadonlyArray<Course>;
-  public era: 'past' | 'active' | 'future';
   public hasItemDraggedOver: boolean;
   public plannedCredits: string;
   public enrolledCredits: number;
@@ -156,19 +155,14 @@ export class TermContainerComponent implements OnInit, OnDestroy {
     }
 
     this.termSubscription = this.term$.subscribe(term => {
-      // const {plannedCourses, enrolledCourses} = term;
       this.plannedCourses = term.plannedCourses;
       this.plannedCredits = this.sumPlannedCredits(term.plannedCourses);
 
       this.enrolledCourses = term.enrolledCourses;
       this.enrolledCredits = this.sumEnrolledCredits(term.enrolledCourses);
 
-      this.era = term.era;
-      const activeTermEnrollmentStatus = this.plannedCourses.forEach(
-        course => course.studentEnrollmentStatus,
-      );
-
-      this.visibleCredits = term.era === 'past' ? 'enrolled' : 'planned';
+      this.visibleCredits =
+        term.termCode.era === Era.Past ? 'enrolled' : 'planned';
     });
 
     this.note$ = this.term$.pipe(
@@ -247,11 +241,12 @@ export class TermContainerComponent implements OnInit, OnDestroy {
 
     if (newContainer === previousContainer) {
       const newIndex = event.currentIndex;
-      const { id: recordId, termCode } = event.item.data as Course;
+      const termCode = event.container.data;
+      const { id: recordId } = event.item.data as Course;
 
       if (recordId !== null) {
         const action = new actions.MoveCourseInsideTerm({
-          termCode: new TermCode(termCode),
+          termCode,
           recordId,
           newIndex,
         });
diff --git a/src/app/degree-planner/year-container/year-container.component.html b/src/app/degree-planner/year-container/year-container.component.html
index ab9ed4a8913de9e3d0c7efbdd597cd84f19328f7..f6854171e5636ac8c75ce2d3dc06087c9b4cc06a 100644
--- a/src/app/degree-planner/year-container/year-container.component.html
+++ b/src/app/degree-planner/year-container/year-container.component.html
@@ -14,11 +14,11 @@
     fxLayoutGap="20px"
     fxLayoutAlign="start stretch"
     class="term-container-wrapper">
-    <cse-term-container fxFlex="33%" [termCode]="yearCode.fall">
+    <cse-term-container fxFlex="33%" [termCode]="yearCode.fall()">
     </cse-term-container>
-    <cse-term-container fxFlex="33%" [termCode]="yearCode.spring">
+    <cse-term-container fxFlex="33%" [termCode]="yearCode.spring()">
     </cse-term-container>
-    <cse-term-container fxFlex="33%" [termCode]="yearCode.summer">
+    <cse-term-container fxFlex="33%" [termCode]="yearCode.summer()">
     </cse-term-container>
   </div>
 </mat-expansion-panel>
diff --git a/src/app/degree-planner/year-container/year-container.component.ts b/src/app/degree-planner/year-container/year-container.component.ts
index 3c3064a3d81bdf7927057dd868e2ea07279f86b3..be7ab44185f71675e3dff7c551c0293934cca842 100644
--- a/src/app/degree-planner/year-container/year-container.component.ts
+++ b/src/app/degree-planner/year-container/year-container.component.ts
@@ -7,7 +7,7 @@ import {
   ExpandAcademicYear,
   CollapseAcademicYear,
 } from '@app/degree-planner/store/actions/ui.actions';
-import { YearCode } from '@app/core/models/termcode';
+import { YearCode } from '../shared/term-codes/yearcode';
 
 @Component({
   selector: 'cse-year-container',
diff --git a/src/app/shared/components/course-details/course-details.component.html b/src/app/shared/components/course-details/course-details.component.html
index c5b9bdd1ae66be89a3c716b6d9a24e6846e8c157..9cff49b386f4b56d394bab73ff336ec813074d3e 100644
--- a/src/app/shared/components/course-details/course-details.component.html
+++ b/src/app/shared/components/course-details/course-details.component.html
@@ -34,6 +34,7 @@
         <form [formGroup]="termSelector" (ngSubmit)="addCourseToPlan($event)">
           <mat-form-field style="margin-right:20px;">
             <mat-select
+              [compareWith]="sameTermCodes"
               placeholder="Term"
               aria-label="Term"
               matInput
diff --git a/src/app/shared/components/course-details/course-details.component.ts b/src/app/shared/components/course-details/course-details.component.ts
index e6e31cd146dcd4aa88e67fb6ae66ebf65dc9fc11..ef6d419efb4c1cd90a66fedbc051b7a688e11995 100644
--- a/src/app/shared/components/course-details/course-details.component.ts
+++ b/src/app/shared/components/course-details/course-details.component.ts
@@ -3,14 +3,14 @@ import { Component, OnInit, OnDestroy, Inject, Input } from '@angular/core';
 import { MAT_DIALOG_DATA } from '@angular/material';
 import { FormBuilder, FormGroup } from '@angular/forms';
 import { Observable, Subscription } from 'rxjs';
-import { distinctUntilChanged, map, filter } from 'rxjs/operators';
+import { distinctUntilChanged, filter, take } from 'rxjs/operators';
 import { MatDialog } from '@angular/material';
 import { Store, select } from '@ngrx/store';
 
 import { CourseDetails } from '@app/core/models/course-details';
 import * as selectors from '@app/degree-planner/store/selectors';
 import * as utils from '@app/degree-planner/shared/utils';
-import { TermCode } from '@app/core/models/termcode';
+import { TermCode } from '@app/degree-planner/shared/term-codes/termcode';
 import { GlobalState } from '@app/core/state';
 import { Course } from '@app/core/models/course';
 import { PlannedTerm } from '@app/core/models/planned-term';
@@ -34,13 +34,13 @@ export class CourseDetailsComponent implements OnInit, OnDestroy {
   @Input() termCode: TermCode;
   public courseDetails: CourseDetails;
   public type: 'course' | 'search' | 'saved';
-  public selectedSearchTerm: string;
+  public selectedSearchTerm: TermCode | undefined;
   public term$: Observable<PlannedTerm>;
   public termSelector: FormGroup;
   public mobileView: MediaQueryList;
 
-  public selectedSearchTerm$: Observable<string>;
-  public droppableTermCodes$: Observable<string[]>;
+  public selectedSearchTerm$: Observable<TermCode | undefined>;
+  public droppableTermCodes$: Observable<TermCode[]>;
   public searchTermSubscription: Subscription;
   public termSubscription: Subscription;
   public plannedCourses: ReadonlyArray<Course>;
@@ -58,26 +58,16 @@ export class CourseDetailsComponent implements OnInit, OnDestroy {
   }
 
   ngOnInit() {
-    this.selectedSearchTerm$ = this.store.pipe(
-      select(selectors.getSelectedSearchTerm),
-      map(termCode => (termCode ? termCode.toString() : '0000')),
-    );
+    this.searchTermSubscription = this.store
+      .select(selectors.getSelectedSearchTerm)
+      .pipe(take(1))
+      .subscribe(term => (this.termSelector = this.fb.group({ term })));
 
     this.droppableTermCodes$ = this.store.pipe(
       select(selectors.selectAllVisibleYears),
       utils.yearsToDroppableTermCodes(),
-      distinctUntilChanged(utils.compareStringArrays),
-    );
-
-    this.searchTermSubscription = this.selectedSearchTerm$.subscribe(
-      termCode => {
-        this.selectedSearchTerm = termCode;
-      },
+      distinctUntilChanged(utils.compareArrays((a, b) => a.equals(b))),
     );
-
-    this.termSelector = this.fb.group({
-      term: this.selectedSearchTerm,
-    });
   }
 
   ngOnDestroy() {
@@ -87,9 +77,14 @@ export class CourseDetailsComponent implements OnInit, OnDestroy {
   addCourseToPlan($event) {
     $event.preventDefault();
 
-    const termCode = new TermCode(this.termSelector.value.term);
+    const termCode: TermCode | undefined = this.termSelector.value.term;
     const subjectCode = this.courseDetails.subject.subjectCode;
     const courseId = this.courseDetails.courseId;
+
+    if (termCode === undefined) {
+      return;
+    }
+
     const payload = {
       courseId,
       termCode,
@@ -100,7 +95,7 @@ export class CourseDetailsComponent implements OnInit, OnDestroy {
 
     this.term$ = this.store.pipe(
       select(selectors.selectVisibleTerm, {
-        termCode: new TermCode(this.termSelector.value.term),
+        termCode,
       }),
       filter(isntUndefined),
       distinctUntilChanged(),
@@ -117,7 +112,7 @@ export class CourseDetailsComponent implements OnInit, OnDestroy {
       this.dialog
         .open(ConfirmDialogComponent, {
           data: {
-            title: "Can't add course to term",
+            title: `Can't add course to term`,
             confirmText: 'OK',
             dialogClass: 'alertDialog',
             text: `This course already exists in selected term`,
@@ -139,4 +134,14 @@ export class CourseDetailsComponent implements OnInit, OnDestroy {
       }
     }
   }
+
+  public sameTermCodes(a: TermCode | undefined, b: TermCode | undefined) {
+    if (a === undefined && b === undefined) {
+      return true;
+    } else if (!a || !b) {
+      return false;
+    } else {
+      return a.equals(b);
+    }
+  }
 }
diff --git a/src/app/shared/pipes/academic-year-state.pipe.ts b/src/app/shared/pipes/academic-year-state.pipe.ts
index a4a132872ecfaa9c09efbf6538e7685cdbcbcc8c..6e22ae665cfcc38d922f6b609d39fdd7b56e4253 100644
--- a/src/app/shared/pipes/academic-year-state.pipe.ts
+++ b/src/app/shared/pipes/academic-year-state.pipe.ts
@@ -1,25 +1,17 @@
 import { Pipe, PipeTransform } from '@angular/core';
-import * as utils from '@app/degree-planner/shared/utils';
-import { YearCode } from '@app/core/models/termcode';
-import { ConstantsService } from '@app/degree-planner/services/constants.service';
+import { YearCode } from '@app/degree-planner/shared/term-codes/yearcode';
 
 @Pipe({ name: 'academicYearState' })
 export class AcademicYearStatePipe implements PipeTransform {
-  constructor(private constants: ConstantsService) {}
-
-  transform(yearCode: string | YearCode): string {
-    if (typeof yearCode === 'string') {
-      yearCode = new YearCode(yearCode);
+  transform(yearCode: YearCode): string {
+    if (yearCode.summer().isPast()) {
+      return `Past year: ${yearCode.fromYear}-${yearCode.toYear}`;
     }
 
-    const era = utils.pickYearEra(yearCode, this.constants.activeTermCodes());
-    switch (era) {
-      case 'past':
-        return `Past year: ${yearCode.fromYear}-${yearCode.toYear}`;
-      case 'future':
-        return `Future year: ${yearCode.fromYear}-${yearCode.toYear}`;
-      default:
-        return `Active year: ${yearCode.fromYear}-${yearCode.toYear}`;
+    if (yearCode.fall().isFuture()) {
+      return `Future year: ${yearCode.fromYear}-${yearCode.toYear}`;
     }
+
+    return `Active year: ${yearCode.fromYear}-${yearCode.toYear}`;
   }
 }
diff --git a/src/app/shared/pipes/get-term-description.pipe.ts b/src/app/shared/pipes/get-term-description.pipe.ts
index 917f1bb6bfda95c587d761a61cc63852f15d4ff1..21d62baf574c916eaa37db1d0a47c86eb32aee23 100644
--- a/src/app/shared/pipes/get-term-description.pipe.ts
+++ b/src/app/shared/pipes/get-term-description.pipe.ts
@@ -1,13 +1,11 @@
 import { Pipe, PipeTransform } from '@angular/core';
-import { TermCode } from '@app/core/models/termcode';
+import { RawTermCode } from '@app/degree-planner/shared/term-codes/without-era';
 
-@Pipe({
-  name: 'getTermDescription',
-})
+@Pipe({ name: 'getTermDescription' })
 export class GetTermDescriptionPipe implements PipeTransform {
-  transform(termCode: string | TermCode): string {
+  transform(termCode: RawTermCode | string): string {
     if (typeof termCode === 'string') {
-      termCode = new TermCode(termCode);
+      termCode = new RawTermCode(termCode);
     }
 
     return termCode.description;