From 6125a1279ccceeb9665fe40cf0da02f4b67c6cb0 Mon Sep 17 00:00:00 2001
From: tgs266 <siegel.tucker@gmail.com>
Date: Tue, 21 Sep 2021 18:44:46 -0400
Subject: [PATCH] major update, includes testing, context wrapper for logger,
 seed data for corn, and begin process of enforcing the passing of context to
 downstream functions for logging purposes

---
 .gitignore                              |   4 +-
 README.md                               |   4 +
 config/routes.go                        |   3 +
 context/context.go                      |  23 ++++
 controllers/data_download_controller.go |   4 +-
 controllers/gdd_controller.go           |  10 +-
 controllers/misc_controller.go          |   4 +-
 controllers/nomads_controller.go        |  22 ++-
 controllers/seed_controllers.go         |  47 +++++++
 docs/docs.go                            | 170 +++++++++++++++++++++---
 docs/swagger.json                       | 156 ++++++++++++++++++++--
 docs/swagger.yaml                       | 105 ++++++++++++++-
 errors/dawn_errors.go                   |  41 +++---
 go.mod                                  |  10 +-
 go.sum                                  |  17 +++
 logger/logger.go                        |   6 +-
 main.go                                 |  20 +--
 models/csv.go                           |   6 +-
 models/enums/product_type.go            |   4 +-
 models/freezing_dates.go                |   4 +-
 models/gdd.go                           |  14 +-
 models/seeds.go                         |  66 +++++++++
 persistence/entities/seed.go            |  10 ++
 persistence/mongodb.go                  |  66 +++++++--
 services/data_download_service.go       |  19 +--
 services/freezing_date_service.go       |   4 +-
 services/gdd_service.go                 |  24 +++-
 services/seed_service.go                |  72 ++++++++++
 utils/clip_test.go                      |  37 ++++++
 utils/date_converter.go                 |  38 ++++++
 utils/gdd_calculations.go               |  27 +---
 utils/gdd_calculations_test.go          |  97 ++++++++++++++
 32 files changed, 1006 insertions(+), 128 deletions(-)
 create mode 100644 context/context.go
 create mode 100644 controllers/seed_controllers.go
 create mode 100644 models/seeds.go
 create mode 100644 persistence/entities/seed.go
 create mode 100644 services/seed_service.go
 create mode 100644 utils/clip_test.go
 create mode 100644 utils/date_converter.go
 create mode 100644 utils/gdd_calculations_test.go

diff --git a/.gitignore b/.gitignore
index 965dfc0..b861460 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
 *.exe
 .idea
-docs/
\ No newline at end of file
+docs/
+
+coverage.out
\ No newline at end of file
diff --git a/README.md b/README.md
index 9662a36..c490275 100644
--- a/README.md
+++ b/README.md
@@ -11,5 +11,9 @@ To run: `go run main.go`
 Build to executable: `go build`
 To rebuild swagger: `swag init`
 
+## Testing
+
+Call `go test -covermode=count -coverprofile=coverage.out ./...`
+To generate code coverage html page, call `go tool cover -html=coverage.out`
 
 Swagger [Here](http://localhost:8080/api/weather/gdd-swagger/index.html#/)
\ No newline at end of file
diff --git a/config/routes.go b/config/routes.go
index 7a89f6e..3fdcfac 100644
--- a/config/routes.go
+++ b/config/routes.go
@@ -17,4 +17,7 @@ func GddRoutes(route fiber.Router) {
 	route.Get("freezing-dates", controllers.GetFreezingDates)
 
 	route.Get("gdd/csv", controllers.GetCSVFile)
+
+	route.Get("gdd/seeds/corn", controllers.GetCornSeedMaturityDate)
+	route.Get("gdd/seeds", controllers.GetSeedList)
 }
diff --git a/context/context.go b/context/context.go
new file mode 100644
index 0000000..d56374c
--- /dev/null
+++ b/context/context.go
@@ -0,0 +1,23 @@
+package context
+
+import (
+	"dawn-weather/logger"
+
+	"github.com/gofiber/fiber/v2"
+)
+
+type DawnCtx struct {
+	FiberCtx *fiber.Ctx
+}
+
+func (ctx DawnCtx) INFO(message string) {
+	logger.INFO(ctx.FiberCtx, message)
+}
+
+func (ctx DawnCtx) DEBUG(message string) {
+	logger.DEBUG(ctx.FiberCtx, message)
+}
+
+func (ctx DawnCtx) TRACE(message string) {
+	logger.TRACE(ctx.FiberCtx, message)
+}
diff --git a/controllers/data_download_controller.go b/controllers/data_download_controller.go
index c1a289b..46a58f1 100644
--- a/controllers/data_download_controller.go
+++ b/controllers/data_download_controller.go
@@ -1,6 +1,7 @@
 package controllers
 
 import (
+	"dawn-weather/context"
 	"dawn-weather/models"
 	"dawn-weather/services"
 
@@ -36,7 +37,8 @@ import (
 // @Router /api/weather/gdd/csv [get]
 func GetCSVFile(c *fiber.Ctx) error {
 	request := models.CSVRequest{}.Build(c)
-	fileText := services.GetDataDownload(c, request)
+	ctx := context.DawnCtx{FiberCtx: c}
+	fileText := services.GetDataDownload(ctx, request)
 	e := c.Status(fiber.StatusOK).SendString(fileText)
 	return e
 }
diff --git a/controllers/gdd_controller.go b/controllers/gdd_controller.go
index 3d40f8a..67818ab 100644
--- a/controllers/gdd_controller.go
+++ b/controllers/gdd_controller.go
@@ -1,12 +1,15 @@
 package controllers
 
 import (
+	"dawn-weather/context"
 	"dawn-weather/models"
 	"dawn-weather/services"
 
 	"github.com/gofiber/fiber/v2"
 )
 
+var GetGddValues = services.GetGddValues
+
 // GetDailyGdd godoc
 // @Summary Get gdd values
 // @Tags Gdd
@@ -22,8 +25,9 @@ import (
 // @Param accumulate query boolean true "Accumulate gdd values"
 // @Router /api/weather/gdd/daily [get]
 func GetDailyGdd(c *fiber.Ctx) error {
-	request := models.GddRequest{}.Build(c)
-	return c.Status(fiber.StatusOK).JSON(services.GetGddValues(c, request))
+	ctx := context.DawnCtx{FiberCtx: c}
+	request := models.BuildGddRequest(ctx.FiberCtx)
+	return c.Status(fiber.StatusOK).JSON(GetGddValues(ctx, request))
 }
 
 // GetNormalGdd godoc
@@ -40,6 +44,6 @@ func GetDailyGdd(c *fiber.Ctx) error {
 // @Param accumulate query boolean true "Accumulate gdd values"
 // @Router /api/weather/gdd/normals [get]
 func GetNormalGdd(c *fiber.Ctx) error {
-	request := models.GddRequest{}.Build(c)
+	request := models.BuildGddRequest(c)
 	return c.Status(fiber.StatusOK).JSON(services.GetNormalValues(request))
 }
diff --git a/controllers/misc_controller.go b/controllers/misc_controller.go
index b8b3e9b..b93933d 100644
--- a/controllers/misc_controller.go
+++ b/controllers/misc_controller.go
@@ -1,7 +1,7 @@
 package controllers
 
 import (
-	"dawn-weather/errors"
+	DawnErrors "dawn-weather/errors"
 	"dawn-weather/models"
 	"dawn-weather/persistence"
 	"dawn-weather/persistence/entities"
@@ -27,7 +27,7 @@ func GetAnalogYear(c *fiber.Ctx) error {
 	lon, _ := strconv.ParseFloat(c.Query("longitude", "-10000.0"), 64)
 	err := models.ValidateAnalogRequest(lat, lon)
 	if err != nil {
-		panic(errors.BAD_REQUEST)
+		panic(DawnErrors.BAD_REQUEST)
 	}
 
 	location := entities.Location{Type: "Point", Coordinates: []float64{lon, lat}}
diff --git a/controllers/nomads_controller.go b/controllers/nomads_controller.go
index eb5cec1..3a4b1a1 100644
--- a/controllers/nomads_controller.go
+++ b/controllers/nomads_controller.go
@@ -21,7 +21,7 @@ import (
 // @Param accumulate query boolean true "Accumulate gdd values"
 // @Router /api/weather/gdd/gefs [get]
 func GetGefsGDD(c *fiber.Ctx) error {
-	request := models.GddRequest{}.Build(c)
+	request := models.BuildGddRequest(c)
 	return c.Status(fiber.StatusOK).JSON(services.GetGefsGddValues(request))
 }
 
@@ -39,6 +39,24 @@ func GetGefsGDD(c *fiber.Ctx) error {
 // @Param accumulate query boolean true "Accumulate gdd values"
 // @Router /api/weather/gdd/cfs [get]
 func GetCfsGDD(c *fiber.Ctx) error {
-	request := models.GddRequest{}.Build(c)
+	request := models.BuildGddRequest(c)
 	return c.Status(fiber.StatusOK).JSON(services.GetCfsGddValues(request))
 }
+
+// // GetCfsFreezingDate godoc
+// // @Summary Get predicted freezing date from CFS
+// // @Tags Gdd
+// // @Description Get GDD values calculated from CFS
+// // @Accept  json
+// // @Produce  json
+// // @Success 200 {object} models.CfsGddResponse
+// // @Failure 400 {object} errors.StandardError
+// // @Param product query string true "Crop to calculate gdd for" Enums(corn, soybean, sunflower, tomato, sugar_beet, peanut, cotton, potato, wheat, pea, oat, spring_wheat, rice, sorghum)
+// // @Param latitude query number true "Latitude to search for"
+// // @Param longitude query number true "Longitude to search for"
+// // @Param accumulate query boolean true "Accumulate gdd values"
+// // @Router /api/weather/gdd/cfs [get]
+// func GetCfsFreezingDate(c *fiber.Ctx) error {
+// 	request := models.GddRequest{}.Build(c)
+// 	return c.Status(fiber.StatusOK).JSON(services.GetCfsGddValues(request))
+// }
diff --git a/controllers/seed_controllers.go b/controllers/seed_controllers.go
new file mode 100644
index 0000000..583274e
--- /dev/null
+++ b/controllers/seed_controllers.go
@@ -0,0 +1,47 @@
+package controllers
+
+import (
+	"dawn-weather/models"
+	"dawn-weather/services"
+
+	"github.com/gofiber/fiber/v2"
+)
+
+// GetSeedList godoc
+// @Summary Get list of seeds in database
+// @Tags GDD Seed Data
+// @Description Get list of seeds in database
+// @Accept  json
+// @Produce  json
+// @Success 200 {object} models.SeedListResponse
+// @Failure 400 {object} errors.StandardError
+// @Param product query string true "Crop type to use" Enums(corn, soybean)
+// @Router /api/weather/gdd/seeds [get]
+func GetSeedList(c *fiber.Ctx) error {
+	request := models.SeedListRequest{Product: c.Query("product")}
+	return c.Status(fiber.StatusOK).JSON(
+		services.GetSeedList(c, request),
+	)
+}
+
+// GetCornSeedMaturityDate godoc
+// @Summary Get estimated maturity date from corn seed (uses CFS data if current GDUs are less than the crop maturity)
+// @Tags GDD Seed Data
+// @Description Get estimated maturity date from corn seed
+// @Accept  json
+// @Produce  json
+// @Success 200 {object} models.CornMaturityResponse
+// @Failure 400 {object} errors.StandardError
+// @Param latitude query number true "Latitude to search for"
+// @Param longitude query number true "Longitude to search for"
+// @Param seed query string true "Corn seed to use"
+// @Param month query number true "month planted"
+// @Param date query number true "date planted"
+// @Router /api/weather/gdd/seeds/corn [get]
+func GetCornSeedMaturityDate(c *fiber.Ctx) error {
+
+	request := models.BuildCornMaturityRequest(c)
+	return c.Status(fiber.StatusOK).JSON(
+		services.GetCornMaturityDate(c, request),
+	)
+}
diff --git a/docs/docs.go b/docs/docs.go
index 442c8e0..6adc5d3 100644
--- a/docs/docs.go
+++ b/docs/docs.go
@@ -1,14 +1,13 @@
-// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
+// Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
 // This file was generated by swaggo/swag
-
 package docs
 
 import (
 	"bytes"
 	"encoding/json"
 	"strings"
+	"text/template"
 
-	"github.com/alecthomas/template"
 	"github.com/swaggo/swag"
 )
 
@@ -16,7 +15,7 @@ var doc = `{
     "schemes": {{ marshal .Schemes }},
     "swagger": "2.0",
     "info": {
-        "description": "{{.Description}}",
+        "description": "{{escape .Description}}",
         "title": "{{.Title}}",
         "contact": {
             "name": "API Support",
@@ -656,29 +655,132 @@ var doc = `{
                     }
                 }
             }
-        }
-    },
-    "definitions": {
-        "errors.ErrorDetails": {
-            "type": "object",
-            "properties": {
-                "correlationId": {
-                    "type": "string"
+        },
+        "/api/weather/gdd/seeds": {
+            "get": {
+                "description": "Get list of seeds in database",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "GDD Seed Data"
+                ],
+                "summary": "Get list of seeds in database",
+                "parameters": [
+                    {
+                        "enum": [
+                            "corn",
+                            "soybean"
+                        ],
+                        "type": "string",
+                        "description": "Crop type to use",
+                        "name": "product",
+                        "in": "query",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/models.SeedListResponse"
+                        }
+                    },
+                    "400": {
+                        "description": "Bad Request",
+                        "schema": {
+                            "$ref": "#/definitions/errors.StandardError"
+                        }
+                    }
                 }
             }
         },
+        "/api/weather/gdd/seeds/corn": {
+            "get": {
+                "description": "Get estimated maturity date from corn seed",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "GDD Seed Data"
+                ],
+                "summary": "Get estimated maturity date from corn seed (uses CFS data if current GDUs are less than the crop maturity)",
+                "parameters": [
+                    {
+                        "type": "number",
+                        "description": "Latitude to search for",
+                        "name": "latitude",
+                        "in": "query",
+                        "required": true
+                    },
+                    {
+                        "type": "number",
+                        "description": "Longitude to search for",
+                        "name": "longitude",
+                        "in": "query",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "Corn seed to use",
+                        "name": "seed",
+                        "in": "query",
+                        "required": true
+                    },
+                    {
+                        "type": "number",
+                        "description": "month planted",
+                        "name": "month",
+                        "in": "query",
+                        "required": true
+                    },
+                    {
+                        "type": "number",
+                        "description": "date planted",
+                        "name": "date",
+                        "in": "query",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/models.CornMaturityResponse"
+                        }
+                    },
+                    "400": {
+                        "description": "Bad Request",
+                        "schema": {
+                            "$ref": "#/definitions/errors.StandardError"
+                        }
+                    }
+                }
+            }
+        }
+    },
+    "definitions": {
         "errors.StandardError": {
             "type": "object",
             "properties": {
                 "description": {
                     "type": "string"
                 },
+                "details": {
+                    "type": "object",
+                    "additionalProperties": {
+                        "type": "string"
+                    }
+                },
                 "errorCode": {
                     "type": "string"
                 },
-                "errorDetails": {
-                    "$ref": "#/definitions/errors.ErrorDetails"
-                },
                 "source": {
                     "type": "string"
                 }
@@ -756,6 +858,26 @@ var doc = `{
                 }
             }
         },
+        "models.CornMaturityResponse": {
+            "type": "object",
+            "properties": {
+                "closest_gdd": {
+                    "type": "number"
+                },
+                "closest_latitude": {
+                    "type": "number"
+                },
+                "closest_longitude": {
+                    "type": "number"
+                },
+                "date": {
+                    "type": "string"
+                },
+                "gdd": {
+                    "type": "number"
+                }
+            }
+        },
         "models.FreezingDateResponse": {
             "type": "object",
             "properties": {
@@ -839,6 +961,17 @@ var doc = `{
                     }
                 }
             }
+        },
+        "models.SeedListResponse": {
+            "type": "object",
+            "properties": {
+                "seeds": {
+                    "type": "array",
+                    "items": {
+                        "type": "string"
+                    }
+                }
+            }
         }
     }
 }`
@@ -873,6 +1006,13 @@ func (s *s) ReadDoc() string {
 			a, _ := json.Marshal(v)
 			return string(a)
 		},
+		"escape": func(v interface{}) string {
+			// escape tabs
+			str := strings.Replace(v.(string), "\t", "\\t", -1)
+			// replace " with \", and if that results in \\", replace that with \\\"
+			str = strings.Replace(str, "\"", "\\\"", -1)
+			return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1)
+		},
 	}).Parse(doc)
 	if err != nil {
 		return doc
diff --git a/docs/swagger.json b/docs/swagger.json
index d024431..77fea7a 100644
--- a/docs/swagger.json
+++ b/docs/swagger.json
@@ -641,29 +641,132 @@
                     }
                 }
             }
-        }
-    },
-    "definitions": {
-        "errors.ErrorDetails": {
-            "type": "object",
-            "properties": {
-                "correlationId": {
-                    "type": "string"
+        },
+        "/api/weather/gdd/seeds": {
+            "get": {
+                "description": "Get list of seeds in database",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "GDD Seed Data"
+                ],
+                "summary": "Get list of seeds in database",
+                "parameters": [
+                    {
+                        "enum": [
+                            "corn",
+                            "soybean"
+                        ],
+                        "type": "string",
+                        "description": "Crop type to use",
+                        "name": "product",
+                        "in": "query",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/models.SeedListResponse"
+                        }
+                    },
+                    "400": {
+                        "description": "Bad Request",
+                        "schema": {
+                            "$ref": "#/definitions/errors.StandardError"
+                        }
+                    }
                 }
             }
         },
+        "/api/weather/gdd/seeds/corn": {
+            "get": {
+                "description": "Get estimated maturity date from corn seed",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "GDD Seed Data"
+                ],
+                "summary": "Get estimated maturity date from corn seed (uses CFS data if current GDUs are less than the crop maturity)",
+                "parameters": [
+                    {
+                        "type": "number",
+                        "description": "Latitude to search for",
+                        "name": "latitude",
+                        "in": "query",
+                        "required": true
+                    },
+                    {
+                        "type": "number",
+                        "description": "Longitude to search for",
+                        "name": "longitude",
+                        "in": "query",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "Corn seed to use",
+                        "name": "seed",
+                        "in": "query",
+                        "required": true
+                    },
+                    {
+                        "type": "number",
+                        "description": "month planted",
+                        "name": "month",
+                        "in": "query",
+                        "required": true
+                    },
+                    {
+                        "type": "number",
+                        "description": "date planted",
+                        "name": "date",
+                        "in": "query",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/models.CornMaturityResponse"
+                        }
+                    },
+                    "400": {
+                        "description": "Bad Request",
+                        "schema": {
+                            "$ref": "#/definitions/errors.StandardError"
+                        }
+                    }
+                }
+            }
+        }
+    },
+    "definitions": {
         "errors.StandardError": {
             "type": "object",
             "properties": {
                 "description": {
                     "type": "string"
                 },
+                "details": {
+                    "type": "object",
+                    "additionalProperties": {
+                        "type": "string"
+                    }
+                },
                 "errorCode": {
                     "type": "string"
                 },
-                "errorDetails": {
-                    "$ref": "#/definitions/errors.ErrorDetails"
-                },
                 "source": {
                     "type": "string"
                 }
@@ -741,6 +844,26 @@
                 }
             }
         },
+        "models.CornMaturityResponse": {
+            "type": "object",
+            "properties": {
+                "closest_gdd": {
+                    "type": "number"
+                },
+                "closest_latitude": {
+                    "type": "number"
+                },
+                "closest_longitude": {
+                    "type": "number"
+                },
+                "date": {
+                    "type": "string"
+                },
+                "gdd": {
+                    "type": "number"
+                }
+            }
+        },
         "models.FreezingDateResponse": {
             "type": "object",
             "properties": {
@@ -824,6 +947,17 @@
                     }
                 }
             }
+        },
+        "models.SeedListResponse": {
+            "type": "object",
+            "properties": {
+                "seeds": {
+                    "type": "array",
+                    "items": {
+                        "type": "string"
+                    }
+                }
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/docs/swagger.yaml b/docs/swagger.yaml
index 6ed4fc3..9ad8465 100644
--- a/docs/swagger.yaml
+++ b/docs/swagger.yaml
@@ -1,18 +1,15 @@
 basePath: /
 definitions:
-  errors.ErrorDetails:
-    properties:
-      correlationId:
-        type: string
-    type: object
   errors.StandardError:
     properties:
       description:
         type: string
+      details:
+        additionalProperties:
+          type: string
+        type: object
       errorCode:
         type: string
-      errorDetails:
-        $ref: '#/definitions/errors.ErrorDetails'
       source:
         type: string
     type: object
@@ -63,6 +60,19 @@ definitions:
           type: number
         type: array
     type: object
+  models.CornMaturityResponse:
+    properties:
+      closest_gdd:
+        type: number
+      closest_latitude:
+        type: number
+      closest_longitude:
+        type: number
+      date:
+        type: string
+      gdd:
+        type: number
+    type: object
   models.FreezingDateResponse:
     properties:
       closest_latitude:
@@ -118,6 +128,13 @@ definitions:
           type: number
         type: array
     type: object
+  models.SeedListResponse:
+    properties:
+      seeds:
+        items:
+          type: string
+        type: array
+    type: object
 host: localhost:8080
 info:
   contact:
@@ -577,4 +594,78 @@ paths:
       summary: Get gdd normals
       tags:
       - Gdd
+  /api/weather/gdd/seeds:
+    get:
+      consumes:
+      - application/json
+      description: Get list of seeds in database
+      parameters:
+      - description: Crop type to use
+        enum:
+        - corn
+        - soybean
+        in: query
+        name: product
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.SeedListResponse'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/errors.StandardError'
+      summary: Get list of seeds in database
+      tags:
+      - GDD Seed Data
+  /api/weather/gdd/seeds/corn:
+    get:
+      consumes:
+      - application/json
+      description: Get estimated maturity date from corn seed
+      parameters:
+      - description: Latitude to search for
+        in: query
+        name: latitude
+        required: true
+        type: number
+      - description: Longitude to search for
+        in: query
+        name: longitude
+        required: true
+        type: number
+      - description: Corn seed to use
+        in: query
+        name: seed
+        required: true
+        type: string
+      - description: month planted
+        in: query
+        name: month
+        required: true
+        type: number
+      - description: date planted
+        in: query
+        name: date
+        required: true
+        type: number
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.CornMaturityResponse'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/errors.StandardError'
+      summary: Get estimated maturity date from corn seed (uses CFS data if current
+        GDUs are less than the crop maturity)
+      tags:
+      - GDD Seed Data
 swagger: "2.0"
diff --git a/errors/dawn_errors.go b/errors/dawn_errors.go
index adbb0c3..45289c9 100644
--- a/errors/dawn_errors.go
+++ b/errors/dawn_errors.go
@@ -15,21 +15,18 @@ type BaseError interface {
 }
 
 type DawnError struct {
-	Name        string `json:"name"`
-	Description string `json:"description"`
-	LogDetails  string `json:"log_details"`
-	Code        int    `json:"code"`
-}
-
-type ErrorDetails struct {
-	RequestId string `json:"correlationId"`
+	Name        string            `json:"name"`
+	Description string            `json:"description"`
+	LogDetails  string            `json:"log_details"`
+	Code        int               `json:"code"`
+	Details     map[string]string `json:"details"`
 }
 
 type StandardError struct {
-	Source      string       `json:"source"`
-	ErrorCode   string       `json:"errorCode"`
-	Description string       `json:"description"`
-	Details     ErrorDetails `json:"errorDetails"`
+	Source      string            `json:"source"`
+	ErrorCode   string            `json:"errorCode"`
+	Description string            `json:"description"`
+	Details     map[string]string `json:"details"`
 }
 
 func (err *DawnError) Error() string {
@@ -42,7 +39,10 @@ func (err *DawnError) Error() string {
 
 func (err *DawnError) BuildStandardError(ctx *fiber.Ctx) StandardError {
 	requestId := ctx.Locals("requestId")
-	details := ErrorDetails{RequestId: fmt.Sprintf("%s", requestId)}
+	details := map[string]string{"RequestId": fmt.Sprintf("%s", requestId)}
+	for k, v := range err.Details {
+		details[k] = v
+	}
 	return StandardError{Source: viper.GetString("app.name"), ErrorCode: err.Name, Description: err.Description, Details: details}
 }
 
@@ -51,6 +51,11 @@ func (err *DawnError) AddLogDetails(logDetails string) *DawnError {
 	return err
 }
 
+func (err *DawnError) PutDetail(key string, value string) *DawnError {
+	err.Details[key] = value
+	return err
+}
+
 func Build(err error) *DawnError {
 	return &DawnError{
 		Name:        "INTERNAL_SERVER_ERROR",
@@ -66,8 +71,6 @@ func (err *DawnError) LogJson(c *fiber.Ctx) {
 
 func (err *DawnError) LogString(c *fiber.Ctx) {
 	requestId := c.Locals("requestId")
-	// requestBody := c.Request()
-	// requestBodyStr, _ := json.Marshal(requestBody)
 	output := strconv.Itoa(os.Getpid()) + " " + fmt.Sprintf("%s", requestId) + " " + strconv.Itoa(err.Code) + " - " + c.Method() + " " + c.Route().Path + " - " + err.Error()
 	if err.LogDetails != "" {
 		output += " - " + err.LogDetails
@@ -89,6 +92,14 @@ var BAD_REQUEST = &DawnError{
 	Code:        400,
 }
 
+func SEED_TYPE_MISMATCH(passedSeed string, expectedSeed string) *DawnError {
+	return &DawnError{
+		Name:        "SEED_TYPE_MISMATCH",
+		Description: "Seed '" + passedSeed + "' was not of expected seed type '" + expectedSeed + "'",
+		Code:        400,
+	}
+}
+
 var NO_DATA_FOUND = &DawnError{
 	Name:        "NO_DATA_FOUND",
 	Description: "No data found for request",
diff --git a/go.mod b/go.mod
index 17e4177..1b382c5 100644
--- a/go.mod
+++ b/go.mod
@@ -25,14 +25,16 @@ require (
 	github.com/prometheus/common v0.30.0 // indirect
 	github.com/prometheus/procfs v0.7.2 // indirect
 	github.com/spf13/viper v1.8.1 // indirect
-	github.com/swaggo/swag v1.7.0 // indirect
+	github.com/stretchr/testify v1.7.0 // indirect
+	github.com/swaggo/swag v1.7.1 // indirect
 	github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 // indirect
 	github.com/valyala/fasthttp v1.28.0 // indirect
 	github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 // indirect
 	go.mongodb.org/mongo-driver v1.7.1 // indirect
 	go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
-	golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect
-	golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect
-	golang.org/x/tools v0.1.5 // indirect
+	golang.org/x/net v0.0.0-20210917221730-978cfadd31cf // indirect
+	golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 // indirect
+	golang.org/x/text v0.3.7 // indirect
+	golang.org/x/tools v0.1.6 // indirect
 	google.golang.org/protobuf v1.27.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 67138a7..8b10826 100644
--- a/go.sum
+++ b/go.sum
@@ -112,6 +112,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -423,6 +424,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
 github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -504,6 +506,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
@@ -511,10 +514,13 @@ github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01S
 github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
 github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E=
 github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo=
+github.com/swaggo/swag v1.7.1 h1:gY9ZakXlNWg/i/v5bQBic7VMZ4teq4m89lpiao74p/s=
+github.com/swaggo/swag v1.7.1/go.mod h1:gAiHxNTb9cIpNmA/VEGUP+CyZMCP/EW7mdtc8Bny+p8=
 github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
 github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
@@ -544,6 +550,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
 go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
@@ -669,6 +676,8 @@ golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210917221730-978cfadd31cf h1:R150MpwJIv1MpS0N/pc+NhTM8ajzvlmxlY5OYsrevXQ=
+golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -762,6 +771,9 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU=
 golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 h1:J27LZFQBFoihqXoegpscI10HpjZ7B5WQLLKL2FZXQKw=
+golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -772,6 +784,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -839,6 +853,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.6 h1:SIasE1FVIQOWz2GEAHFOmoW7xchJcqlucjSULTL0Ag4=
+golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -981,6 +997,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/logger/logger.go b/logger/logger.go
index 4a41cef..cbda320 100644
--- a/logger/logger.go
+++ b/logger/logger.go
@@ -36,6 +36,8 @@ type MessageLog struct {
 	Message   string
 }
 
+var LEVEL_FORMAT_STRING string = "%-5s"
+
 func buildMessageLog(c *fiber.Ctx, message string) MessageLog {
 	const layout = "2006-01-02 03:04:05"
 	requestId := c.Locals("requestId")
@@ -83,7 +85,7 @@ func LogRequest(message RequestLog) {
 		tempLogString, _ := json.MarshalIndent(message, "", "  ")
 		logString = string(tempLogString)
 	} else {
-		logString = fmt.Sprintf("[%s] %s %s %s %s - %s %s", message.Level, message.Date, message.PID, message.RequestId, message.StatusCode, message.Method, message.Path)
+		logString = fmt.Sprintf("[%s] %s %s %s %s - %s %s", fmt.Sprintf(LEVEL_FORMAT_STRING, message.Level), message.Date, message.PID, message.RequestId, message.StatusCode, message.Method, message.Path)
 		if message.Error != nil {
 			logString += " - " + message.Error.Error()
 		}
@@ -165,7 +167,7 @@ func _log(c *fiber.Ctx, level, message string) {
 		tempLogString, _ := json.MarshalIndent(lg, "", "  ")
 		logString = string(tempLogString)
 	} else {
-		logString = fmt.Sprintf("[%s] %s %s %s %s", lg.Level, lg.Date, lg.PID, lg.RequestId, lg.Message)
+		logString = fmt.Sprintf("[%s] %s %s %s %s", fmt.Sprintf(LEVEL_FORMAT_STRING, lg.Level), lg.Date, lg.PID, lg.RequestId, lg.Message)
 	}
 	fmt.Println(logString)
 }
diff --git a/main.go b/main.go
index 9da86f9..7903083 100644
--- a/main.go
+++ b/main.go
@@ -25,7 +25,7 @@ func registerRoutes(app *fiber.App) {
 }
 
 func registerSwagger(app *fiber.App) {
-	if viper.GetBool("swagger") {
+	if viper.GetBool("app.swagger") {
 		app.Get(viper.GetString("server.context-path")+"/gdd-swagger/*", swagger.Handler)
 		app.Get(viper.GetString("server.context-path")+"/gdd-swagger/*", swagger.New())
 	}
@@ -33,7 +33,7 @@ func registerSwagger(app *fiber.App) {
 
 func registerCors(app *fiber.App) {
 	app.Use(cors.New(cors.Config{
-		AllowOrigins: "http://localhost:3000, http://localhost:4200, http://localhost:5000",
+		AllowOrigins: "http://localhost:3000, http://localhost:4200, http://localhost:5000, http://localhost:12321",
 		AllowHeaders: "Origin, Content-Type, Accept",
 	}))
 }
@@ -63,7 +63,7 @@ func createFiberConfig() fiber.Config {
 
 			code := fiber.StatusInternalServerError
 			message := errors.StandardError{Source: viper.GetString("app.name"), ErrorCode: "INTERNAL_SERVER",
-				Description: "Internal Server Error Occurred", Details: errors.ErrorDetails{RequestId: ""}}
+				Description: "Internal Server Error Occurred", Details: map[string]string{"RequestId": ""}}
 
 			if e, ok := err.(*errors.DawnError); ok {
 				code = e.Code
@@ -89,9 +89,14 @@ func createFiberConfig() fiber.Config {
 	}
 }
 
-func createFiberApp() *fiber.App {
+func CreateFiberApp() *fiber.App {
 	app := fiber.New(createFiberConfig())
 	app.Use(recover.New())
+	registerCors(app)
+	registerSwagger(app)
+	registerPrometheus(app)
+	registerLogging(app)
+	registerRoutes(app)
 	return app
 }
 
@@ -105,12 +110,7 @@ func createFiberApp() *fiber.App {
 func main() {
 	config.GetConfig()
 	persistence.CreateDBSession()
-	app := createFiberApp()
-	registerCors(app)
-	registerSwagger(app)
-	registerPrometheus(app)
-	registerLogging(app)
-	registerRoutes(app)
+	app := CreateFiberApp()
 
 	err := app.Listen(":" + viper.GetString("server.port"))
 
diff --git a/models/csv.go b/models/csv.go
index 6ab7f10..5885c3e 100644
--- a/models/csv.go
+++ b/models/csv.go
@@ -1,7 +1,7 @@
 package models
 
 import (
-	"dawn-weather/errors"
+	DawnErrors "dawn-weather/errors"
 	"strconv"
 	"time"
 
@@ -85,8 +85,8 @@ func (r CSVRequest) Build(c *fiber.Ctx) CSVRequest {
 		Longitude:      longitude,
 	}
 
-	if newRequest.Validate() != nil {
-		panic(errors.BAD_REQUEST)
+	if e := newRequest.Validate(); e != nil {
+		panic(DawnErrors.BAD_REQUEST.AddLogDetails(e.Error()))
 	}
 
 	return newRequest
diff --git a/models/enums/product_type.go b/models/enums/product_type.go
index 9619f11..76bd38e 100644
--- a/models/enums/product_type.go
+++ b/models/enums/product_type.go
@@ -1,7 +1,7 @@
 package enums
 
 import (
-	"dawn-weather/errors"
+	DawnErrors "dawn-weather/errors"
 	"strings"
 )
 
@@ -89,5 +89,5 @@ func GetProductFromString(productString string) Product {
 	case strings.ToLower(ProductType.SORGHUM.Name):
 		return ProductType.SORGHUM
 	}
-	panic(errors.INVALID_PRODUCT(productString))
+	panic(DawnErrors.INVALID_PRODUCT(productString))
 }
diff --git a/models/freezing_dates.go b/models/freezing_dates.go
index 5d67b84..1458d30 100644
--- a/models/freezing_dates.go
+++ b/models/freezing_dates.go
@@ -1,7 +1,7 @@
 package models
 
 import (
-	"dawn-weather/errors"
+	DawnErrors "dawn-weather/errors"
 	"dawn-weather/persistence/entities"
 	"strconv"
 	"time"
@@ -48,7 +48,7 @@ func (r FreezingDateRequest) Build(c *fiber.Ctx) FreezingDateRequest {
 	}
 
 	if rNew.Validate() != nil {
-		panic(errors.BAD_REQUEST)
+		panic(DawnErrors.BAD_REQUEST)
 	}
 
 	return rNew
diff --git a/models/gdd.go b/models/gdd.go
index 5ff846f..8c818db 100644
--- a/models/gdd.go
+++ b/models/gdd.go
@@ -1,7 +1,7 @@
 package models
 
 import (
-	"dawn-weather/errors"
+	DawnErrors "dawn-weather/errors"
 	"dawn-weather/persistence/entities"
 	"strconv"
 	"time"
@@ -43,7 +43,7 @@ func (r GddRequest) ValidateNoYear() error {
 	)
 }
 
-func (r GddRequest) Build(c *fiber.Ctx) GddRequest {
+var BuildGddRequest = func(c *fiber.Ctx) GddRequest {
 	year, errYear := strconv.Atoi(c.Query("year", "0"))
 	product := c.Query("product")
 	latitude, errLat := strconv.ParseFloat(c.Query("latitude"), 64)
@@ -59,16 +59,16 @@ func (r GddRequest) Build(c *fiber.Ctx) GddRequest {
 	}
 
 	if errYear != nil || errLat != nil || errLon != nil || errBool != nil {
-		panic(errors.BAD_REQUEST)
+		panic(DawnErrors.BAD_REQUEST)
 	}
 
 	if rNew.Year != 0 {
-		if rNew.Validate() != nil {
-			panic(errors.BAD_REQUEST)
+		if e := rNew.Validate(); e != nil {
+			panic(DawnErrors.BAD_REQUEST.AddLogDetails(e.Error()))
 		}
 	} else {
-		if rNew.ValidateNoYear() != nil {
-			panic(errors.BAD_REQUEST)
+		if e := rNew.ValidateNoYear(); e != nil {
+			panic(DawnErrors.BAD_REQUEST.AddLogDetails(e.Error()))
 		}
 	}
 	return rNew
diff --git a/models/seeds.go b/models/seeds.go
new file mode 100644
index 0000000..f370bf0
--- /dev/null
+++ b/models/seeds.go
@@ -0,0 +1,66 @@
+package models
+
+import (
+	DawnErrors "dawn-weather/errors"
+	"strconv"
+	"time"
+
+	validation "github.com/go-ozzo/ozzo-validation"
+	"github.com/gofiber/fiber/v2"
+)
+
+type SeedListRequest struct {
+	Product string `json:"product"`
+}
+
+type SeedListResponse struct {
+	Seeds []string `json:"seeds"`
+}
+
+type CornMaturityRequest struct {
+	Latitude  float64 `json:"latitude"`
+	Longitude float64 `json:"longitude"`
+	Seed      string  `json:"seed"`
+	Month     int     `json:"month"`
+	Date      int     `json:"date"`
+}
+
+type CornMaturityResponse struct {
+	Date             time.Time `json:"date"`
+	GDD              float64   `json:"gdd"`
+	ClosestGDD       float64   `json:"closest_gdd"`
+	ClosestLatitude  float64   `json:"closest_latitude"`
+	ClosestLongitude float64   `json:"closest_longitude"`
+}
+
+func (r CornMaturityRequest) Validate() error {
+	return validation.ValidateStruct(&r,
+		validation.Field(&r.Latitude, validation.Required, validation.Min(-90.0), validation.Max(90.0)),
+		validation.Field(&r.Longitude, validation.Required, validation.Min(-180.0), validation.Max(180.0)),
+		validation.Field(&r.Month, validation.Required, validation.Min(1), validation.Max(12)),
+		validation.Field(&r.Date, validation.Required, validation.Min(1), validation.Max(31)),
+	)
+}
+
+func BuildCornMaturityRequest(c *fiber.Ctx) CornMaturityRequest {
+
+	lat, _ := strconv.ParseFloat(c.Query("latitude", "-10000.0"), 64)
+	lon, _ := strconv.ParseFloat(c.Query("longitude", "-10000.0"), 64)
+	month, _ := strconv.Atoi(c.Query("month", "0"))
+	date, _ := strconv.Atoi(c.Query("date", "0"))
+	seed := c.Query("seed", "-10000.0")
+
+	newRequest := CornMaturityRequest{
+		Latitude:  lat,
+		Longitude: lon,
+		Seed:      seed,
+		Month:     month,
+		Date:      date,
+	}
+
+	if e := newRequest.Validate(); e != nil {
+		panic(DawnErrors.BAD_REQUEST.AddLogDetails(e.Error()))
+	}
+
+	return newRequest
+}
diff --git a/persistence/entities/seed.go b/persistence/entities/seed.go
new file mode 100644
index 0000000..4b6e438
--- /dev/null
+++ b/persistence/entities/seed.go
@@ -0,0 +1,10 @@
+package entities
+
+type Seed struct {
+	Seed             string  `bson:"seed"`
+	Company          string  `bson:"company"`
+	SilkGdus         float64 `bson:"silk_gdus"`
+	BlackLayerGdus   float64 `bson:"black_layer_gdus"`
+	RelativeMaturity float64 `bson:"relative_maturity"`
+	Type             string  `bson:"type"`
+}
diff --git a/persistence/mongodb.go b/persistence/mongodb.go
index 45c5336..e91aad4 100644
--- a/persistence/mongodb.go
+++ b/persistence/mongodb.go
@@ -2,9 +2,10 @@ package persistence
 
 import (
 	"context"
-	"dawn-weather/errors"
+	DawnErrors "dawn-weather/errors"
 	"dawn-weather/models"
 	"dawn-weather/persistence/entities"
+	"strings"
 
 	"github.com/bradfitz/slice"
 	"github.com/spf13/viper"
@@ -67,7 +68,7 @@ func CurrentGddFindFirstByYearAndLocation(location entities.Location) entities.G
 	var g entities.Gdd
 	err := coll.FindOne(Ctx, filter).Decode(&g)
 	if err != nil {
-		panic(errors.NO_DATA_FOUND)
+		panic(DawnErrors.NO_DATA_FOUND)
 	}
 
 	return g
@@ -81,7 +82,7 @@ func GddFindFirstByYearAndLocation(year int, location entities.Location) entitie
 	var g entities.Gdd
 	err := coll.FindOne(Ctx, filter).Decode(&g)
 	if err != nil {
-		panic(errors.NO_DATA_FOUND)
+		panic(DawnErrors.NO_DATA_FOUND)
 	}
 
 	return g
@@ -95,7 +96,7 @@ func NormalsFindFirstByYearAndLocation(location entities.Location) entities.Norm
 	var n entities.Normal
 	err := coll.FindOne(Ctx, filter).Decode(&n)
 	if err != nil {
-		panic(errors.NO_DATA_FOUND)
+		panic(DawnErrors.NO_DATA_FOUND)
 	}
 
 	return n
@@ -115,7 +116,7 @@ func GefsFindAllByLocation(location entities.Location) []entities.GefsGdd {
 
 	cursor, err := coll.Find(Ctx, filter, options)
 	if err != nil {
-		panic(errors.NO_DATA_FOUND)
+		panic(DawnErrors.NO_DATA_FOUND)
 	}
 
 	for cursor.Next(context.TODO()) {
@@ -123,7 +124,7 @@ func GefsFindAllByLocation(location entities.Location) []entities.GefsGdd {
 		var elem entities.GefsGdd
 		err := cursor.Decode(&elem)
 		if err != nil {
-			panic(errors.NO_DATA_FOUND)
+			panic(DawnErrors.NO_DATA_FOUND)
 		}
 
 		results = append(results, elem)
@@ -144,7 +145,7 @@ func CfsFindAllByLocation(location entities.Location) entities.CfsGdd {
 	var g entities.CfsGdd
 	err := coll.FindOne(Ctx, filter).Decode(&g)
 	if err != nil {
-		panic(errors.NO_DATA_FOUND)
+		panic(DawnErrors.NO_DATA_FOUND)
 	}
 
 	return g
@@ -158,7 +159,7 @@ func FindAnalogYear(location entities.Location) models.AnalogResponse {
 	var g entities.Gdd
 	err := coll.FindOne(Ctx, filter).Decode(&g)
 	if err != nil {
-		panic(errors.NO_DATA_FOUND)
+		panic(DawnErrors.NO_DATA_FOUND)
 	}
 
 	results := models.AnalogResponse{
@@ -178,8 +179,55 @@ func FindFreezingDates(location entities.Location) entities.FreezingDates {
 	var g entities.FreezingDates
 	err := coll.FindOne(Ctx, filter).Decode(&g)
 	if err != nil {
-		panic(errors.NO_DATA_FOUND)
+		panic(DawnErrors.NO_DATA_FOUND)
 	}
 
 	return g
 }
+
+func FindSeed(seedName string) entities.Seed {
+	coll := Conn.Database(viper.GetString("db.database")).Collection("seeds")
+
+	filter := bson.D{
+		{Key: "seed", Value: seedName},
+	}
+
+	var g entities.Seed
+	err := coll.FindOne(Ctx, filter).Decode(&g)
+	if err != nil {
+		panic(DawnErrors.NO_DATA_FOUND)
+	}
+
+	return g
+}
+
+func FindSeeds(product string) []entities.Seed {
+
+	coll := Conn.Database(viper.GetString("db.database")).Collection("seeds")
+
+	filter := bson.D{
+		{Key: "type", Value: strings.ToLower(product)},
+	}
+
+	var results []entities.Seed
+
+	options := options.Find()
+
+	cursor, err := coll.Find(Ctx, filter, options)
+	if err != nil {
+		panic(DawnErrors.NO_DATA_FOUND)
+	}
+
+	for cursor.Next(context.TODO()) {
+
+		var elem entities.Seed
+		err := cursor.Decode(&elem)
+		if err != nil {
+			panic(DawnErrors.NO_DATA_FOUND)
+		}
+
+		results = append(results, elem)
+	}
+
+	return results
+}
diff --git a/services/data_download_service.go b/services/data_download_service.go
index 0bc4f0a..82b0de1 100644
--- a/services/data_download_service.go
+++ b/services/data_download_service.go
@@ -1,7 +1,8 @@
 package services
 
 import (
-	"dawn-weather/errors"
+	"dawn-weather/context"
+	DawnErrors "dawn-weather/errors"
 	"dawn-weather/models"
 	"dawn-weather/models/enums"
 	"dawn-weather/persistence"
@@ -92,7 +93,7 @@ func fillDates() []time.Time {
 	return dates
 }
 
-func getPrimary(c *fiber.Ctx, request models.CSVRequest, dates []time.Time) []float64 {
+func getPrimary(c context.DawnCtx, request models.CSVRequest, dates []time.Time) []float64 {
 	gddRequest := models.GddRequest{
 		Year:       request.Year,
 		Product:    request.Product,
@@ -152,7 +153,7 @@ func _parseDate(date string) time.Time {
 	const layout = "2006-Jan-02"
 	newDate, e := time.Parse(layout, date)
 	if e != nil {
-		panic(errors.DATE_PARSE_FAILURE.AddLogDetails(e.Error()))
+		panic(DawnErrors.DATE_PARSE_FAILURE.AddLogDetails(e.Error()))
 	}
 	return newDate
 }
@@ -187,7 +188,7 @@ func parseDate(date string) time.Time {
 	case "12":
 		return _parseDate("1981-Dev-" + day)
 	}
-	panic(errors.DATE_PARSE_FAILURE.AddLogDetails("Failed converting " + date + " to proper format."))
+	panic(DawnErrors.DATE_PARSE_FAILURE.AddLogDetails("Failed converting " + date + " to proper format."))
 }
 
 func getFreezingDates(request models.CSVRequest, dates []time.Time) [][]int {
@@ -230,7 +231,7 @@ func getFreezingDates(request models.CSVRequest, dates []time.Time) [][]int {
 	return [][]int{firstFreezingValues, lastFreezingValues}
 }
 
-func getComparisonYear(c *fiber.Ctx, request models.CSVRequest, dates []time.Time) []float64 {
+func getComparisonYear(c context.DawnCtx, request models.CSVRequest, dates []time.Time) []float64 {
 	gddRequest := models.GddRequest{
 		Year:       request.ComparisonYear,
 		Product:    request.Product,
@@ -346,13 +347,13 @@ func getGefsData(request models.CSVRequest, dates []time.Time) []float64 {
 	return fullGddValues
 }
 
-func pullData(c *fiber.Ctx, request models.CSVRequest) CSVData {
+func pullData(c context.DawnCtx, request models.CSVRequest) CSVData {
 	returnData := CSVData{}
 	dates := fillDates()
 	returnData.Date = dates
 
 	if request.Analog {
-		returnData.Analog = getAnalogYear(c, request, dates)
+		returnData.Analog = getAnalogYear(c.FiberCtx, request, dates)
 	}
 	if request.Cfs || request.CfsLower || request.CfsUpper {
 		t := getCfsData(request, dates)
@@ -424,14 +425,14 @@ func createRecords(keys []string, data CSVData) [][]string {
 	return records
 }
 
-func GetDataDownload(c *fiber.Ctx, request models.CSVRequest) string {
+func GetDataDownload(c context.DawnCtx, request models.CSVRequest) string {
 
 	fileId := uuid.New()
 
 	f, err := os.Create(fileId.String() + ".csv")
 
 	if err != nil {
-		panic(errors.FILE_CREATION_ERROR.AddLogDetails("Could not create file"))
+		panic(DawnErrors.FILE_CREATION_ERROR.AddLogDetails("Could not create file"))
 	}
 
 	w := csv.NewWriter(f)
diff --git a/services/freezing_date_service.go b/services/freezing_date_service.go
index 5bb6c54..14990b3 100644
--- a/services/freezing_date_service.go
+++ b/services/freezing_date_service.go
@@ -1,7 +1,7 @@
 package services
 
 import (
-	"dawn-weather/errors"
+	DawnErrors "dawn-weather/errors"
 	"dawn-weather/models"
 	"dawn-weather/persistence"
 	"dawn-weather/persistence/entities"
@@ -45,7 +45,7 @@ func GetFreezingDate(request models.FreezingDateRequest) models.FreezingDateResp
 	}
 
 	if tempIdx == -1 {
-		panic(errors.BAD_REQUEST)
+		panic(DawnErrors.BAD_REQUEST)
 	}
 
 	firstDates := freezingDates.FirstDates[tempIdx]
diff --git a/services/gdd_service.go b/services/gdd_service.go
index 4f70be9..670e171 100644
--- a/services/gdd_service.go
+++ b/services/gdd_service.go
@@ -1,6 +1,7 @@
 package services
 
 import (
+	"dawn-weather/context"
 	"dawn-weather/models"
 	"dawn-weather/models/enums"
 	"dawn-weather/persistence"
@@ -11,7 +12,28 @@ import (
 	"github.com/gofiber/fiber/v2"
 )
 
-func GetGddValues(c *fiber.Ctx, request models.GddRequest) models.GddResponse {
+func GetFullYearGddValues(c *fiber.Ctx, request models.GddRequest) models.GddResponse {
+	product := enums.GetProductFromString(request.Product)
+	var gdds entities.Gdd
+	if request.Year == time.Now().Year() {
+		gdds = persistence.CurrentGddFindFirstByYearAndLocation(request.BuildLocation())
+		gdds2 := persistence.CfsFindAllByLocation(request.BuildLocation())
+		gdds.MaxTemps = append(gdds.MaxTemps, gdds2.MaxTemps...)
+		gdds.MinTemps = append(gdds.MinTemps, gdds2.MinTemps...)
+	} else {
+		gdds = persistence.GddFindFirstByYearAndLocation(request.Year, request.BuildLocation())
+	}
+	returnGdds := models.GddResponse{
+		Product:          product.Name,
+		ClosestLatitude:  gdds.Location.Coordinates[1],
+		ClosestLongitude: gdds.Location.Coordinates[0],
+		GddValues:        utils.CalculateGddValues(gdds.MinTemps, gdds.MaxTemps, product, request.Accumulate),
+		LastDate:         gdds.LastDate.Time(),
+	}
+	return returnGdds
+}
+
+func GetGddValues(ctx context.DawnCtx, request models.GddRequest) models.GddResponse {
 	product := enums.GetProductFromString(request.Product)
 	var gdds entities.Gdd
 	if request.Year == time.Now().Year() {
diff --git a/services/seed_service.go b/services/seed_service.go
new file mode 100644
index 0000000..c64717e
--- /dev/null
+++ b/services/seed_service.go
@@ -0,0 +1,72 @@
+package services
+
+import (
+	DawnErrors "dawn-weather/errors"
+	"dawn-weather/models"
+	"dawn-weather/models/enums"
+	"dawn-weather/persistence"
+	"dawn-weather/utils"
+	"math"
+	"time"
+
+	"github.com/gofiber/fiber/v2"
+)
+
+func GetSeedList(c *fiber.Ctx, request models.SeedListRequest) models.SeedListResponse {
+	product := enums.GetProductFromString(request.Product)
+	if product.Name != enums.ProductType.CORN.Name && product.Name != enums.ProductType.SOYBEAN.Name {
+		panic(DawnErrors.INVALID_PRODUCT(request.Product))
+	}
+	seeds := persistence.FindSeeds(product.Name)
+	var results []string
+	for i := 0; i < len(seeds); i++ {
+		results = append(results, seeds[i].Seed)
+	}
+	return models.SeedListResponse{Seeds: results}
+}
+
+func GetCornMaturityDate(c *fiber.Ctx, request models.CornMaturityRequest) models.CornMaturityResponse {
+
+	seed := persistence.FindSeed(request.Seed)
+	if seed.Type != "corn" {
+		panic(DawnErrors.SEED_TYPE_MISMATCH(request.Seed, "corn"))
+	}
+
+	gddRequest := models.GddRequest{
+		Year:       time.Now().Year(),
+		Product:    "corn",
+		Latitude:   request.Latitude,
+		Longitude:  request.Longitude,
+		Accumulate: false,
+	}
+
+	gdds := GetFullYearGddValues(c, gddRequest)
+	closestValue := 0.0
+	closestIdx := 0
+	gdus := 0.0
+
+	startingDate := time.Date(time.Now().Year(), time.Month(request.Month), request.Date, 0, 0, 0, 0, time.UTC)
+	startingIdx := startingDate.Sub(time.Date(time.Now().Year(), time.January, 1, 0, 0, 0, 0, time.UTC))
+	startingIdxVal := int(startingIdx.Hours() / 24)
+	if isLeapYear(time.Now().Year()) {
+		startingIdxVal -= 1
+	}
+
+	for i := startingIdxVal; i < len(gdds.GddValues); i++ {
+		value := gdds.GddValues[i]
+		gdus += value
+		if math.Abs(gdus-seed.BlackLayerGdus) < math.Abs(closestValue-seed.BlackLayerGdus) {
+			closestValue = gdus
+			closestIdx = i
+		}
+	}
+	response := models.CornMaturityResponse{
+		Date:             utils.ConvertDateIdxToDate(closestIdx),
+		GDD:              seed.BlackLayerGdus,
+		ClosestGDD:       closestValue,
+		ClosestLatitude:  gdds.ClosestLatitude,
+		ClosestLongitude: gdds.ClosestLongitude,
+	}
+	return response
+
+}
diff --git a/utils/clip_test.go b/utils/clip_test.go
new file mode 100644
index 0000000..e6e6db4
--- /dev/null
+++ b/utils/clip_test.go
@@ -0,0 +1,37 @@
+package utils
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestClipInt(t *testing.T) {
+	value := 10
+	assert.Equal(t, value, ClipInt(value, 0, 11))
+}
+
+func TestClipIntLow(t *testing.T) {
+	value := 4
+	assert.Equal(t, 10, ClipInt(value, 10, 11))
+}
+
+func TestClipIntAbove(t *testing.T) {
+	value := 100
+	assert.Equal(t, 11, ClipInt(value, 10, 11))
+}
+
+func TestClipFloat(t *testing.T) {
+	value := 10.0
+	assert.Equal(t, value, ClipFloat(value, 0.0, 11.0))
+}
+
+func TestClipFloatLow(t *testing.T) {
+	value := 4.0
+	assert.Equal(t, 10.0, ClipFloat(value, 10.0, 11.0))
+}
+
+func TestClipFloatAbove(t *testing.T) {
+	value := 100.0
+	assert.Equal(t, 11.0, ClipFloat(value, 10.0, 11.0))
+}
diff --git a/utils/date_converter.go b/utils/date_converter.go
new file mode 100644
index 0000000..7e614cb
--- /dev/null
+++ b/utils/date_converter.go
@@ -0,0 +1,38 @@
+package utils
+
+import "time"
+
+func isLeapYear(year int) bool {
+	if (year % 4) == 0 {
+		if (year % 100) == 0 {
+			if (year % 400) == 0 {
+				return true
+			} else {
+				return false
+			}
+		} else {
+			return true
+		}
+	} else {
+		return false
+	}
+}
+
+func ConvertDateIdxToDate(idx int) time.Time {
+	idx -= 1
+	date := time.Date(time.Now().Year(), time.January, 1, 0, 0, 0, 0, time.UTC)
+	date = date.Add(time.Duration(24*idx) * time.Hour)
+	if isLeapYear(time.Now().Year()) {
+		date = date.Add(time.Duration(-24) * time.Hour)
+	}
+	return date
+}
+
+func ConvertDateIdxToDateWithPlantingDate(plantingDate time.Time, idx int) time.Time {
+	date := plantingDate
+	date = date.Add(time.Duration(24*idx) * time.Hour)
+	if isLeapYear(time.Now().Year()) && plantingDate.Before(time.Date(time.Now().Year(), time.March, 1, 0, 0, 0, 0, time.UTC)) {
+		date = date.Add(time.Duration(-24) * time.Hour)
+	}
+	return date
+}
diff --git a/utils/gdd_calculations.go b/utils/gdd_calculations.go
index c3de23f..3d068a9 100644
--- a/utils/gdd_calculations.go
+++ b/utils/gdd_calculations.go
@@ -1,31 +1,20 @@
 package utils
 
-import "dawn-weather/models/enums"
+import (
+	"dawn-weather/models/enums"
+)
 
 func CalculateSingleGdd(minTemp float64, maxTemp float64, product enums.Product) float64 {
 	if product.Name == "CORN" {
-		if minTemp < 50 {
-			minTemp = 50
-		}
-		if minTemp > 86 {
-			minTemp = 86
-		}
-
-		if maxTemp < 50 {
-			maxTemp = 50
-		}
-		if maxTemp > 86 {
-			maxTemp = 86
-		}
+		minTemp = ClipFloat(minTemp, 50, 86)
+		maxTemp = ClipFloat(maxTemp, 50, 86)
 	}
 	mean := (maxTemp + minTemp) / 2.0
 	value := mean - product.BaseTemp
 	if product.Name == "CORN" {
 		return value
 	}
-	if value < 0 {
-		value = 0
-	}
+	value = ClipMinFloat(value, 0)
 	return value
 }
 
@@ -33,9 +22,7 @@ func CalculateNormalGddValues(base []float64, product enums.Product, accumulate
 	var returnList []float64
 	for i := 0; i < len(base); i++ {
 		value := base[i] - product.BaseTemp
-		if value < 0 {
-			value = 0
-		}
+		value = ClipMinFloat(value, 0)
 		if accumulate && i > 0 {
 			value += returnList[len(returnList)-1]
 		}
diff --git a/utils/gdd_calculations_test.go b/utils/gdd_calculations_test.go
new file mode 100644
index 0000000..a3f5727
--- /dev/null
+++ b/utils/gdd_calculations_test.go
@@ -0,0 +1,97 @@
+package utils
+
+import (
+	"dawn-weather/models/enums"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var minTempsArr = [...]float64{55.5, 62.4, 22.5, 96.5}
+var maxTempsArr = [...]float64{78.4, 92.5, 55.8, 35.2}
+
+var normalBaseArr = [...]float64{50, 60, 70, 80}
+
+func Test_CalculateSingleGdd(t *testing.T) {
+	minTemp := minTempsArr[0]
+	maxTemp := maxTempsArr[0]
+
+	expectedGdd := (minTemp + maxTemp) / 2
+	expectedGdd -= enums.ProductType.SOYBEAN.BaseTemp
+
+	assert.Equal(t, expectedGdd, CalculateSingleGdd(minTemp, maxTemp, enums.ProductType.SOYBEAN))
+}
+
+func Test_CalculateSingleGdd_Corn(t *testing.T) {
+	minTemp := minTempsArr[0]
+	maxTemp := maxTempsArr[0]
+
+	expectedGdd := (minTemp + maxTemp) / 2
+	expectedGdd -= enums.ProductType.CORN.BaseTemp
+
+	assert.Equal(t, expectedGdd, CalculateSingleGdd(minTemp, maxTemp, enums.ProductType.CORN))
+}
+
+func Test_CalculateGddValues(t *testing.T) {
+	var expectedGdd []float64
+
+	for i := 0; i < 4; i++ {
+		val := (minTempsArr[i] + maxTempsArr[i]) / 2
+		val -= enums.ProductType.OAT.BaseTemp
+		expectedGdd = append(expectedGdd, ClipMinFloat(val, 0))
+	}
+
+	assert.Equal(t, expectedGdd, CalculateGddValues(minTempsArr[:], maxTempsArr[:], enums.ProductType.OAT, false))
+}
+
+func Test_CalculateGddValues_Corn(t *testing.T) {
+	var expectedGdd []float64
+
+	for i := 0; i < 4; i++ {
+		val := (ClipFloat(minTempsArr[i], 50, 86) + ClipFloat(maxTempsArr[i], 50, 86)) / 2
+		val -= enums.ProductType.CORN.BaseTemp
+		expectedGdd = append(expectedGdd, val)
+	}
+
+	assert.Equal(t, expectedGdd, CalculateGddValues(minTempsArr[:], maxTempsArr[:], enums.ProductType.CORN, false))
+}
+
+func Test_CalculateGddValues_Accumulated(t *testing.T) {
+	var expectedGdd []float64
+	for i := 0; i < 4; i++ {
+		val := (minTempsArr[i] + maxTempsArr[i]) / 2
+		val -= enums.ProductType.WHEAT.BaseTemp
+		val = ClipMinFloat(val, 0)
+		if i > 0 {
+			val += expectedGdd[len(expectedGdd)-1]
+		}
+		expectedGdd = append(expectedGdd, val)
+	}
+
+	assert.Equal(t, expectedGdd, CalculateGddValues(minTempsArr[:], maxTempsArr[:], enums.ProductType.WHEAT, true))
+}
+
+func Test_CalculateNormalGddValues(t *testing.T) {
+	var expectedGdd []float64
+	for i := 0; i < 4; i++ {
+		val := normalBaseArr[i] - enums.ProductType.PEANUT.BaseTemp
+		val = ClipMinFloat(val, 0)
+		expectedGdd = append(expectedGdd, val)
+	}
+
+	assert.Equal(t, expectedGdd, CalculateNormalGddValues(normalBaseArr[:], enums.ProductType.PEANUT, false))
+}
+
+func Test_CalculateNormalGddValues_Accumulated(t *testing.T) {
+	var expectedGdd []float64
+	for i := 0; i < 4; i++ {
+		val := normalBaseArr[i] - enums.ProductType.PEANUT.BaseTemp
+		val = ClipMinFloat(val, 0)
+		if i > 0 {
+			val += expectedGdd[len(expectedGdd)-1]
+		}
+		expectedGdd = append(expectedGdd, val)
+	}
+
+	assert.Equal(t, expectedGdd, CalculateNormalGddValues(normalBaseArr[:], enums.ProductType.PEANUT, true))
+}
-- 
GitLab