Skip to content

Commit

Permalink
Merge branch 'test'
Browse files Browse the repository at this point in the history
  • Loading branch information
kweeuhree committed Jan 2, 2025
2 parents d74ed73 + 9416d6f commit fd6c294
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 60 deletions.
55 changes: 37 additions & 18 deletions cmd/web/handlers.go → cmd/handlers/handlers.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
package main
package handlers

import (
"log"
"net/http"

"github.com/google/uuid"
"kweeuhree.receipt-processor-challenge/cmd/helpers"
"kweeuhree.receipt-processor-challenge/cmd/utils"
"kweeuhree.receipt-processor-challenge/internal/models"
"kweeuhree.receipt-processor-challenge/internal/validator"
)

type Handlers struct {
ErrorLog *log.Logger
ReceiptStore *models.ReceiptStore
Utils *utils.Utils
Helpers *helpers.Helpers
}

type ReceiptInput struct {
Retailer string `json:"retailer"`
PurchaseDate string `json:"purchaseDate"`
Expand All @@ -25,30 +35,39 @@ type PointsResponse struct {
Points int `json:"points"`
}

func NewHandlers(errorLog *log.Logger, receiptStore *models.ReceiptStore, utils *utils.Utils, helpers *helpers.Helpers) *Handlers {
return &Handlers{
ErrorLog: errorLog,
ReceiptStore: receiptStore,
Utils: utils,
Helpers: helpers,
}
}

// Process the receipt and return its id
func (app *application) ProcessReceipt(w http.ResponseWriter, r *http.Request) {
func (h *Handlers) ProcessReceipt(w http.ResponseWriter, r *http.Request) {
// Decode the JSON body into the input struct
var input ReceiptInput
err := decodeJSON(w, r, &input)
err := h.Helpers.DecodeJSON(w, r, &input)
if err != nil {
app.errorLog.Printf("Exiting after decoding attempt: %s", err)
h.ErrorLog.Printf("Exiting after decoding attempt: %s", err)
return
}

// Validate input
input.Validate()
if !input.Valid() {
encodeJSON(w, http.StatusBadRequest, input.FieldErrors)
h.Helpers.EncodeJSON(w, http.StatusBadRequest, input.FieldErrors)
return
}

// Prepare new receipt for storage
newReceipt := app.ReceiptFactory(input)
newReceipt := h.ReceiptFactory(input)

// Store the receipt in memory
err = app.receiptStore.Insert(newReceipt)
err = h.ReceiptStore.Insert(newReceipt)
if err != nil {
app.serverError(w, err)
h.Helpers.ServerError(w, err)
return
}

Expand All @@ -58,27 +77,27 @@ func (app *application) ProcessReceipt(w http.ResponseWriter, r *http.Request) {
}

// Write the response struct as JSON
err = encodeJSON(w, http.StatusOK, response)
err = h.Helpers.EncodeJSON(w, http.StatusOK, response)
if err != nil {
app.serverError(w, err)
h.Helpers.ServerError(w, err)
return
}
}

// Process the receipt and return its id
func (app *application) GetReceiptPoints(w http.ResponseWriter, r *http.Request) {
func (h *Handlers) GetReceiptPoints(w http.ResponseWriter, r *http.Request) {
// Get receipt id from params
receiptID := app.GetIdFromParams(r, "id")
receiptID := h.Helpers.GetIdFromParams(r, "id")
if receiptID == "" {
app.notFound(w)
h.Helpers.NotFound(w)
return
}

// Calculate points
points, err := app.CalculatePoints(receiptID)
points, err := h.Utils.CalculatePoints(receiptID)
if err != nil {
msg := map[string]string{"error": "No receipt found for that ID."}
encodeJSON(w, http.StatusNotFound, msg)
h.Helpers.EncodeJSON(w, http.StatusNotFound, msg)
return
}

Expand All @@ -88,14 +107,14 @@ func (app *application) GetReceiptPoints(w http.ResponseWriter, r *http.Request)
}

// Write the response struct to the response as JSON
err = encodeJSON(w, http.StatusOK, response)
err = h.Helpers.EncodeJSON(w, http.StatusOK, response)
if err != nil {
app.serverError(w, err)
h.Helpers.ServerError(w, err)
return
}
}

func (app *application) ReceiptFactory(input ReceiptInput) models.Receipt {
func (h *Handlers) ReceiptFactory(input ReceiptInput) models.Receipt {
receiptID := uuid.New().String()

newReceipt := models.Receipt{
Expand Down
2 changes: 1 addition & 1 deletion cmd/web/validate.go → cmd/handlers/validate.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package handlers

import (
"kweeuhree.receipt-processor-challenge/internal/validator"
Expand Down
29 changes: 20 additions & 9 deletions cmd/web/helpers.go → cmd/helpers/helpers.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,51 @@
package main
package helpers

import (
"encoding/json"
"fmt"
"log"
"net/http"
"runtime/debug"

"github.com/julienschmidt/httprouter"
)

type Helpers struct {
ErrorLog *log.Logger
}

func NewHelpers(errorLog *log.Logger) *Helpers {
return &Helpers{
ErrorLog: errorLog,
}
}

// The serverError helper writes an error message and stack trace to the errorLog,
// then sends a generic 500 Internal Server Error response to the user.
func (app *application) serverError(w http.ResponseWriter, err error) {
func (h *Helpers) ServerError(w http.ResponseWriter, err error) {
// Use the debug.Stack() function to get a stack trace for the current goroutine and append it to the log message
trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack())
// Report the file name and line number one step back in the stack trace
// to have a clearer idea of where the error actually originated from
// set frame depth to 2
app.errorLog.Output(2, trace)
h.ErrorLog.Output(2, trace)

http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}

// The clientError helper sends a specific status code and corresponding description
// to the user
func (app *application) clientError(w http.ResponseWriter, status int) {
func (h *Helpers) ClientError(w http.ResponseWriter, status int) {
http.Error(w, http.StatusText(status), status)
}

// Not found helper
func (app *application) notFound(w http.ResponseWriter) {
app.clientError(w, http.StatusNotFound)
func (h *Helpers) NotFound(w http.ResponseWriter) {
h.ClientError(w, http.StatusNotFound)
}

// Decode the JSON body of a request into the destination struct
func decodeJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
func (h *Helpers) DecodeJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
err := json.NewDecoder(r.Body).Decode(dst)
if err != nil {
http.Error(w, "The receipt is invalid.", http.StatusBadRequest)
Expand All @@ -44,14 +55,14 @@ func decodeJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
}

// Encodes provided data into a JSON response
func encodeJSON(w http.ResponseWriter, status int, data interface{}) error {
func (h *Helpers) EncodeJSON(w http.ResponseWriter, status int, data interface{}) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
return json.NewEncoder(w).Encode(data)
}

// Get parameter id from the request
func (app *application) GetIdFromParams(r *http.Request, paramsId string) string {
func (h *Helpers) GetIdFromParams(r *http.Request, paramsId string) string {
params := httprouter.ParamsFromContext(r.Context())
id := params.ByName(paramsId)
return id
Expand Down
67 changes: 41 additions & 26 deletions cmd/web/calculate-utils.go → cmd/utils/calculate-utils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main
package utils

import (
"log"
"math"
"regexp"
"strconv"
Expand All @@ -10,11 +11,25 @@ import (
"kweeuhree.receipt-processor-challenge/internal/models"
)

func (app *application) CalculatePoints(id string) (int, error) {
app.infoLog.Printf("Calculating points for receipt with id: %s", id)
type Utils struct {
ErrorLog *log.Logger
InfoLog *log.Logger
ReceiptStore *models.ReceiptStore
}

func NewUtils(errorLog *log.Logger, infoLog *log.Logger, receiptStore *models.ReceiptStore) *Utils {
return &Utils{
ErrorLog: errorLog,
InfoLog: infoLog,
ReceiptStore: receiptStore,
}
}

func (u *Utils) CalculatePoints(id string) (int, error) {
u.InfoLog.Printf("Calculating points for receipt with id: %s", id)

// Get receipt by its id
receipt, err := app.receiptStore.Get(id)
receipt, err := u.ReceiptStore.Get(id)
if err != nil {
return 0, err
}
Expand All @@ -27,14 +42,14 @@ func (app *application) CalculatePoints(id string) (int, error) {

// Get all points of the receipt
points := []int{
app.getRetailerNamePoints(receipt.Retailer),
app.getRoundTotalPoints(floatTotal),
app.getQuartersPoints(floatTotal),
app.getEveryTwoItemsPoints(receipt.Items),
app.getItemDescriptionPoints(receipt.Items),
app.getLlmGeneratedPoints(floatTotal),
app.getOddDayPoints(receipt.PurchaseDate),
app.getPurchaseTimePoints(receipt.PurchaseTime),
u.getRetailerNamePoints(receipt.Retailer),
u.getRoundTotalPoints(floatTotal),
u.getQuartersPoints(floatTotal),
u.getEveryTwoItemsPoints(receipt.Items),
u.getItemDescriptionPoints(receipt.Items),
u.getLlmGeneratedPoints(floatTotal),
u.getOddDayPoints(receipt.PurchaseDate),
u.getPurchaseTimePoints(receipt.PurchaseTime),
}

// Calculate total points of the receipt
Expand All @@ -43,21 +58,21 @@ func (app *application) CalculatePoints(id string) (int, error) {
total += point
}

app.infoLog.Printf("Total Points: %d", total)
u.InfoLog.Printf("Total Points: %d", total)

return total, nil
}

// Assigns one point for every alphanumeric character in the retailer name
func (app *application) getRetailerNamePoints(retailerName string) int {
func (u *Utils) getRetailerNamePoints(retailerName string) int {
points := 0

// Split the retailers name
splitChars := strings.Split(retailerName, "")

// For each character is splitChars, check if the character is alphanumeric
for _, char := range splitChars {
if app.isAlphanumeric(char) {
if u.isAlphanumeric(char) {
points += 1
}
}
Expand All @@ -66,12 +81,12 @@ func (app *application) getRetailerNamePoints(retailerName string) int {
}

// Checks if the character is alphanumeric
func (app *application) isAlphanumeric(char string) bool {
func (u *Utils) isAlphanumeric(char string) bool {
return regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(char)
}

// Assigns 50 points if the total is a round dollar amount with no cents
func (app *application) getRoundTotalPoints(total float64) int {
func (u *Utils) getRoundTotalPoints(total float64) int {
points := 0

// Use modulo operator to determine points
Expand All @@ -83,7 +98,7 @@ func (app *application) getRoundTotalPoints(total float64) int {
}

// Assigns 25 points if the total is a multiple of `0.25`
func (app *application) getQuartersPoints(total float64) int {
func (u *Utils) getQuartersPoints(total float64) int {
points := 0

// Use modulo operator to determine points
Expand All @@ -95,7 +110,7 @@ func (app *application) getQuartersPoints(total float64) int {
}

// Assigns 5 points for every two items on the receipt
func (app *application) getEveryTwoItemsPoints(items []models.Item) int {
func (u *Utils) getEveryTwoItemsPoints(items []models.Item) int {
points := 0

// Get length of items and determine pairs
Expand All @@ -110,7 +125,7 @@ func (app *application) getEveryTwoItemsPoints(items []models.Item) int {
}

// Assigns points based on item description
func (app *application) getItemDescriptionPoints(items []models.Item) int {
func (u *Utils) getItemDescriptionPoints(items []models.Item) int {
// If the trimmed length of the item description is a multiple of 3,
// multiply the price by `0.2` and round up to the nearest integer.
// The result is the number of points earned.
Expand All @@ -127,7 +142,7 @@ func (app *application) getItemDescriptionPoints(items []models.Item) int {
parsedPrice, err := strconv.ParseFloat(item.Price, 64)

if err != nil {
app.errorLog.Printf("Error parsing price for item %s: %v", item.ShortDescription, err)
u.ErrorLog.Printf("Error parsing price for item %s: %v", item.ShortDescription, err)
return 0
}
// Round up
Expand All @@ -140,7 +155,7 @@ func (app *application) getItemDescriptionPoints(items []models.Item) int {
}

// Assigns points if the program is generated by an LLM
func (app *application) getLlmGeneratedPoints(total float64) int {
func (u *Utils) getLlmGeneratedPoints(total float64) int {
// If and only if this program is generated using a large language model,
// 5 points if the total is greater than 10.00.
points := 0
Expand All @@ -155,14 +170,14 @@ func (app *application) getLlmGeneratedPoints(total float64) int {
}

// Assigns 6 points if the day in the purchase date is odd
func (app *application) getOddDayPoints(purchaseDate string) int {
func (u *Utils) getOddDayPoints(purchaseDate string) int {
points := 0

// Determine the layout for time parsing
layout := "2006-01-02"
date, err := time.Parse(layout, purchaseDate)
if err != nil {
app.errorLog.Print("Error parsing date:", err)
u.ErrorLog.Print("Error parsing date:", err)
return 0

}
Expand All @@ -178,13 +193,13 @@ func (app *application) getOddDayPoints(purchaseDate string) int {
}

// Assigns 10 points if the time of purchase is after 2:00pm and before 4:00pm
func (app *application) getPurchaseTimePoints(purchaseTime string) int {
func (u *Utils) getPurchaseTimePoints(purchaseTime string) int {
points := 0
// Determine the layout for time parsing
layout := "15:04"
parsedTime, err := time.Parse(layout, purchaseTime)
if err != nil {
app.errorLog.Print("Error parsing time:", err)
u.ErrorLog.Print("Error parsing time:", err)
return 0
}
// Define starting end ending time for extra bonus
Expand Down
Loading

0 comments on commit fd6c294

Please sign in to comment.