diff --git a/api/auth/middleware/authentication_middleware.go b/api/auth/middleware/authentication_middleware.go index b00d99e..2c1e485 100644 --- a/api/auth/middleware/authentication_middleware.go +++ b/api/auth/middleware/authentication_middleware.go @@ -157,6 +157,7 @@ func IsAuthenticated(auth *auth.Authenticator) gin.HandlerFunc { } else { if !HandleRefreshToken(session) { context.String(http.StatusUnauthorized, "Failed to refresh access token") + context.Abort() return } else { context.Next() diff --git a/api/auth/middleware/verification_middleware.go b/api/auth/middleware/verification_middleware.go index 7e6b47f..34cd504 100644 --- a/api/auth/middleware/verification_middleware.go +++ b/api/auth/middleware/verification_middleware.go @@ -30,6 +30,7 @@ func UserInDatabase(pool *pgxpool.Pool) gin.HandlerFunc { if query_err != nil { log.Println("UserInDatabase(): ERROR Failed to query for user count") ctx.AbortWithStatus(http.StatusInternalServerError) + return } if count == 0 { @@ -38,6 +39,7 @@ func UserInDatabase(pool *pgxpool.Pool) gin.HandlerFunc { if exec_err != nil { log.Printf("UserInDatabase(): ERROR Failed to create user... %s", exec_err.Error()) ctx.AbortWithStatus(http.StatusInternalServerError) + return } } } diff --git a/api/controllers/itemController.go b/api/controllers/itemController.go index e69de29..a9ab3c3 100644 --- a/api/controllers/itemController.go +++ b/api/controllers/itemController.go @@ -0,0 +1,233 @@ +package controllers + +import ( + "context" + "log" + "net/http" + "ordr-api/dto" + "ordr-api/queries" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +func CreateItem(pool *pgxpool.Pool) gin.HandlerFunc { + return func(ctx *gin.Context) { + conn, conn_err := pool.Acquire(ctx) + if conn_err != nil { + log.Printf("CreateItem(): ERROR: Failed to connect... %s", conn_err.Error()) + ctx.AbortWithStatus(http.StatusInternalServerError) + return + } + defer conn.Release() + + item_name := ctx.Query("item_name") + in_season := ctx.Query("in_season") == "1" + if item_name == "" { + ctx.String(http.StatusBadRequest, "CreateItem(): ERROR... Item Name not supplied") + } + + _, exec_err := conn.Exec(context.Background(), queries.CREATE_ITEM, item_name, in_season) + if exec_err != nil { + ctx.String(http.StatusInternalServerError, "CreateItem(): ERROR... Failed to create item") + log.Printf("CreateItem(): ERROR... Failed to create item... %s", exec_err.Error()) + return + } + + var item_id int + query_err := conn.QueryRow(context.Background(), "SELECT id FROM item WHERE item_name = $1", item_name).Scan(&item_id) + if query_err != nil { + ctx.String(http.StatusInternalServerError, "CreateItem(): ERROR... Failed to get nwely created item id") + log.Printf("CreateItem(): ERROR... Failed to get newly created item... %s", query_err.Error()) + return + } + + item_price := ctx.Query("item_price") + + if item_price == "" { + ctx.String(http.StatusBadRequest, "CreateItem(): ERROR... item_price not supplied") + return + } + + item_price_float, convert_err := strconv.ParseFloat(item_price, 64) + if convert_err != nil { + ctx.String(http.StatusBadRequest, "CreateUser(): ERROR... Failed to create item price: item price invalid format") + return + } + + _, exec_price_err := conn.Exec(context.Background(), queries.CREATE_ITEM_PRICE, item_id, item_price) + + if exec_price_err != nil { + ctx.String(http.StatusInternalServerError, "CreateItem(): ERROR... Failed to insert price...") + log.Printf("CreateItem(): ERROR... Failed to insert price... %s", exec_price_err.Error()) + return + } + + var item_price_object dto.ItemPriceResponse + item_price_object.ItemId = item_id + item_price_object.ItemName = item_name + item_price_object.ItemPrice = item_price_float + + ctx.JSON(http.StatusOK, item_price_object) + } +} + +func SetItemPrice(pool *pgxpool.Pool) gin.HandlerFunc { + return func(ctx *gin.Context) { + conn, conn_err := pool.Acquire(ctx) + if conn_err != nil { + log.Printf("GetUserTable(): ERROR: Failed to connect... %s", conn_err.Error()) + ctx.AbortWithStatus(http.StatusInternalServerError) + return + } + defer conn.Release() + + item_id_string := ctx.Query("item_id") + if item_id_string == "" { + ctx.String(http.StatusBadRequest, "SetItemPrice(): ERROR... item id not provided") + return + } + + _, exec_date_err := conn.Exec(context.Background(), queries.SET_ITEM_PRICE_VALID_TO_DATE, item_id_string) + if exec_date_err != nil { + ctx.String(http.StatusInternalServerError, "SetItemPrice(): ERROR... Failed to set valid_to date") + log.Printf("SetItemPrice(): ERROR... Failed to set valid_to date... %s", exec_date_err.Error()) + return + } + + item_price_string := ctx.Query("item_price") + if item_price_string == "" { + ctx.String(http.StatusBadRequest, "SetItemPrice(): ERROR... Item Price Not Provided") + return + } + + item_price_float, item_price_conv_err := strconv.ParseFloat(item_price_string, 64) + if item_price_conv_err != nil { + ctx.String(http.StatusBadRequest, "SetItemPrice(): ERROR... item price not valid") + return + } + + _, exec_new_price_err := conn.Exec(context.Background(), queries.CREATE_ITEM_PRICE, item_id_string, item_price_float) + + if exec_new_price_err != nil { + ctx.String(http.StatusInternalServerError, "SetItemPrice(): ERROR... failed to create item price") + log.Printf("SetItemPrice(): ERROR... failed to create item price... %s", exec_new_price_err.Error()) + return + } + + var item_price_object dto.ItemPriceResponse + query_item_price_err := conn.QueryRow(context.Background(), queries.GET_CURRENT_ITEM_PRICE, item_id_string).Scan(&item_price_object.ItemId, &item_price_object.ItemName, &item_price_object.ItemPrice) + if query_item_price_err != nil { + ctx.String(http.StatusInternalServerError, "SetItemPrice(): ERROR... Failed to query item price") + log.Println("SetItemPrice(): ERROR... Failed to query item price... %s", query_item_price_err.Error()) + return + } + + ctx.JSON(http.StatusOK, item_price_object) + } +} + +func GetCurrentItemPrice(pool *pgxpool.Pool) gin.HandlerFunc { + return func(ctx *gin.Context) { + conn, conn_err := pool.Acquire(ctx) + if conn_err != nil { + log.Printf("GetCurrentItemPrice(): ERROR: Failed to connect... %s", conn_err.Error()) + ctx.AbortWithStatus(http.StatusInternalServerError) + return + } + defer conn.Release() + + item_id := ctx.Query("item_id") + if item_id == "" { + ctx.String(http.StatusBadRequest, "GetCurrentItemPrice(): ERROR... item id not provided") + return + } + + var item_price dto.ItemPriceResponse + + query_err := conn.QueryRow(context.Background(), queries.GET_CURRENT_ITEM_PRICE, item_id).Scan(&item_price.ItemId, &item_price.ItemName, &item_price.ItemPrice) + + if query_err != nil { + if query_err == pgx.ErrNoRows { + ctx.String(http.StatusBadRequest, "GetCurrentItemPrice(): ERROR... no rows match id") + return + } + ctx.String(http.StatusInternalServerError, "GetCurrentItemPrice(): ERROR... failed to query db") + log.Printf("GetCurrentItemPrice(): ERROR... failed to query db... %s", query_err.Error()) + } + + ctx.JSON(http.StatusOK, item_price) + } +} + +func AddItemToOrder(pool *pgxpool.Pool) gin.HandlerFunc { + return func(ctx *gin.Context) { + conn, conn_err := pool.Acquire(ctx) + if conn_err != nil { + log.Printf("AddItemToOrder(): ERROR: Failed to connect... %s", conn_err.Error()) + ctx.AbortWithStatus(http.StatusInternalServerError) + return + } + defer conn.Release() + + item_id := ctx.Query("item_id") + order_id := ctx.Query("order_id") + quantity := ctx.Query("quantity") + created_at := ctx.Query("created_at") + + if item_id == "" || order_id == "" || quantity == "" { + ctx.String(http.StatusBadRequest, "AddItemToOrder(): ERROR... item_id, order_id, or quantity missing") + return + } + + if created_at == "" { + current_timestamp := time.Now() + created_at = current_timestamp.Format(time.RFC3339) + } + + int_item_id, item_id_parse_err := strconv.ParseInt(item_id, 10, 64) + if item_id_parse_err != nil { + ctx.String(http.StatusBadRequest, "AddItemToOrder: ERROR... item id is not an int") + return + } + int_order_id, order_id_parse_err := strconv.ParseInt(order_id, 10, 64) + if order_id_parse_err != nil { + ctx.String(http.StatusBadRequest, "AddItemToOrder: ERROR... order id is not an int") + return + } + int_quantity, quantity_parse_err := strconv.ParseInt(quantity, 10, 64) + if quantity_parse_err != nil { + ctx.String(http.StatusBadRequest, "AddItemToOrder: ERROR... quantity is not an int") + return + } + + _, time_parse_err := time.Parse(time.RFC3339, created_at) + if time_parse_err != nil { + ctx.String(http.StatusBadRequest, "AddItemToOrder(): ERROR... time is bad") + return + } + + _, exec_err := conn.Exec(context.Background(), queries.ADD_ITEM_TO_ORDER, int_item_id, int_order_id, int_quantity, created_at) + if exec_err != nil { + ctx.String(http.StatusInternalServerError, "AddItemToOrder(): ERROR... failed to insert rows") + log.Printf("AddItemToOrder(): ERROR... Failed to insert rows... %s", exec_err.Error()) + return + } + + var order_item_price dto.OrderItemPriceResponse + + price_data_row := conn.QueryRow(context.Background(), queries.GET_ORDER_ITEM_PRICE, item_id, order_id) + + price_scan_err := price_data_row.Scan(&order_item_price.ItemId, &order_item_price.OrderId, &order_item_price.ItemName, &order_item_price.Quantity, &order_item_price.Made, &order_item_price.CreatedAt, &order_item_price.TotalPrice, &order_item_price.UnitPrice) + if price_scan_err != nil { + ctx.String(http.StatusInternalServerError, "AddItemToOrder(): ERROR... Failed to scan price data") + log.Printf("AddItemToOrder(): ERROR... Failed to scan price data... %s", price_scan_err.Error()) + return + } + + ctx.JSON(http.StatusOK, order_item_price) + } +} diff --git a/api/controllers/orderController.go b/api/controllers/orderController.go index 2d32936..1bb4bea 100644 --- a/api/controllers/orderController.go +++ b/api/controllers/orderController.go @@ -1 +1,89 @@ package controllers + +import ( + "context" + "log" + "net/http" + "ordr-api/dto" + "ordr-api/queries" + "time" + + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5/pgxpool" +) + +func CreateOrder(pool *pgxpool.Pool) gin.HandlerFunc { + return func(ctx *gin.Context) { + conn, conn_err := pool.Acquire(ctx) + if conn_err != nil { + log.Printf("CreateItem(): ERROR: Failed to connect... %s", conn_err.Error()) + ctx.AbortWithStatus(http.StatusInternalServerError) + return + } + defer conn.Release() + + user_profile, _ := ctx.Get("user_profile") + + sub_id := user_profile.(dto.UserProfileResponse).Sub + + var current_user dto.UserResponse + + user_query_err := conn.QueryRow(context.Background(), queries.GET_CURRENT_USER_OBJECT, sub_id).Scan(¤t_user.Id, ¤t_user.Name, ¤t_user.Job_Position, ¤t_user.Active, ¤t_user.Admin) + + if user_query_err != nil { + log.Printf("CreateOrder(): ERROR Failed to query user... %s", user_query_err.Error()) + ctx.String(http.StatusInternalServerError, "CreateOrder(): ERROR Failed to query user") + return + } + + orderer := ctx.Query("orderer") + date_due := ctx.Query("date_due") + date_placed := ctx.Query("date_placed") + + if orderer == "" || date_due == "" { + ctx.String(http.StatusBadRequest, "CreateOrder(): ERROR orderer or date_due not supplied") + return + } + + if date_placed == "" { + currentTime := time.Now() + date_placed = currentTime.Format(time.RFC3339) + } + + _, placed_date_err := time.Parse(time.RFC3339, date_placed) + if placed_date_err != nil { + ctx.String(http.StatusBadRequest, "CreateOrder(): Bad time format") + return + } + + _, due_date_err := time.Parse(time.RFC3339, date_due) + if due_date_err != nil { + ctx.String(http.StatusBadRequest, "CreateOrder(): Bad Time format") + return + } + + _, exec_err := conn.Exec(context.Background(), queries.CREATE_ORDER, current_user.Id, orderer, date_due, date_placed) + + if exec_err != nil { + ctx.String(http.StatusInternalServerError, "CreateOrder(): Failed to create order") + log.Printf("CreateOrder(): ERROR... Failed to create order... %s", exec_err.Error()) + return + } + + var order dto.OrderResponse + order_query_err := conn.QueryRow(context.Background(), queries.GET_ORDER_BY_ORDER_INFORMATION, current_user.Id, orderer, date_placed).Scan(&order.Id, &order.UserId, &order.Orderer, &order.DateDue, &order.DatePlaced, &order.AmountPaid, &order.Filled, &order.Delivered) + if order_query_err != nil { + ctx.String(http.StatusInternalServerError, "CreateOrder(): Failed to create order") + log.Printf("CreateOrder(): ERROR... failed to query order after creation ... %s", order_query_err.Error()) + return + } + + ctx.JSON(http.StatusOK, order) + } +} + +func SetOrderFilled(pool *pgxpool.Pool) gin.HandlerFunc { + return func(ctx *gin.Context) { + + } +} diff --git a/api/controllers/userController.go b/api/controllers/userController.go index 42cab92..bd209d6 100644 --- a/api/controllers/userController.go +++ b/api/controllers/userController.go @@ -18,30 +18,24 @@ func SetUserName(pool *pgxpool.Pool) gin.HandlerFunc { conn, err := pool.Acquire(ctx) defer conn.Release() if err != nil { + // TODO: Log this error ctx.String(http.StatusInternalServerError, err.Error()) + return } user_profile, _ := ctx.Get("user_profile") user_name := ctx.Query("user_name") if user_name == "" { - log.Println("CreateUser(): ERROR: user name not supplied") + ctx.String(http.StatusBadRequest, "CreateUser(): ERROR: user name not supplied") return } - updateCommandTag, update_err := conn.Exec(context.Background(), queries.USER_UPDATE_QUERY, user_name, user_profile.(dto.UserProfileResponse).Sub) + _, update_err := conn.Exec(context.Background(), queries.USER_UPDATE_QUERY, user_name, user_profile.(dto.UserProfileResponse).Sub) if update_err != nil { log.Printf("%s", update_err.Error()) ctx.AbortWithStatus(http.StatusInternalServerError) } - - if updateCommandTag.RowsAffected() == 0 { - _, query_err := conn.Exec(context.Background(), queries.USER_CREATE_QUERY, user_profile.(dto.UserProfileResponse).Sub, user_name) - if query_err != nil { - log.Printf("%s", query_err.Error()) - ctx.String(http.StatusInternalServerError, query_err.Error()) - } - } } } diff --git a/api/dto/item_price_response.go b/api/dto/item_price_response.go new file mode 100644 index 0000000..6284ef3 --- /dev/null +++ b/api/dto/item_price_response.go @@ -0,0 +1,7 @@ +package dto + +type ItemPriceResponse struct { + ItemId int + ItemName string + ItemPrice float64 +} diff --git a/api/dto/order_item_price_response.go b/api/dto/order_item_price_response.go new file mode 100644 index 0000000..e55da52 --- /dev/null +++ b/api/dto/order_item_price_response.go @@ -0,0 +1,14 @@ +package dto + +import "time" + +type OrderItemPriceResponse struct { + ItemId int + OrderId int + ItemName string + Quantity int + Made int + CreatedAt time.Time + TotalPrice float64 + UnitPrice float64 +} diff --git a/api/dto/order_response.go b/api/dto/order_response.go new file mode 100644 index 0000000..efa67be --- /dev/null +++ b/api/dto/order_response.go @@ -0,0 +1,14 @@ +package dto + +import "time" + +type OrderResponse struct { + Id int + UserId int + Orderer string + DateDue time.Time + DatePlaced time.Time + AmountPaid float64 + Filled bool + Delivered bool +} diff --git a/api/main.go b/api/main.go index 390aac3..dc3504e 100644 --- a/api/main.go +++ b/api/main.go @@ -45,7 +45,6 @@ func main() { } pool, pool_err := init_db_pool(os.Getenv("CONNECTION_STRING")) - log.Printf("%s", pool) if pool_err != nil { log.Fatal("ERROR: Failed to initialize DB pooL") @@ -70,13 +69,18 @@ func main() { router.GET("/callback", auth.AuthenticationCallbackHandler(authenticator)) router.GET("/users", user_authenticated, middleware.GetUserProfile, user_in_db, user_active, user_is_admin, controllers.GetUserTable(pool)) router.GET("/user/current", user_authenticated, middleware.GetUserProfile, user_in_db, controllers.GetCurrentAuthenticatedUser(pool)) + router.GET("/item/price/current", user_authenticated, middleware.GetUserProfile, user_in_db, controllers.GetCurrentItemPrice(pool)) - router.POST("/user/name", user_authenticated, middleware.GetUserProfile, user_in_db, user_active, controllers.SetUserName(pool)) router.POST("/position/create", user_authenticated, middleware.GetUserProfile, user_in_db, user_active, user_is_admin, controllers.CreatePosition(pool)) + router.POST("/item/create", user_authenticated, middleware.GetUserProfile, user_in_db, user_active, user_is_admin, controllers.CreateItem(pool)) + router.POST("/order/create", user_authenticated, middleware.GetUserProfile, user_in_db, user_active, controllers.CreateOrder(pool)) + router.PUT("/user/name", user_authenticated, middleware.GetUserProfile, user_in_db, user_active, controllers.SetUserName(pool)) router.PUT("/user/promote", user_authenticated, middleware.GetUserProfile, user_in_db, user_active, user_is_admin, controllers.PromoteUser(pool)) router.PUT("/user/demote", user_authenticated, middleware.GetUserProfile, user_in_db, user_active, user_is_admin, controllers.DemoteUser(pool)) router.PUT("/user/position", user_authenticated, middleware.GetUserProfile, user_in_db, user_active, user_is_admin, controllers.SetUserPosition(pool)) + router.PUT("/item/price", user_authenticated, middleware.GetUserProfile, user_in_db, user_active, user_is_admin, controllers.SetItemPrice(pool)) + router.PUT("/order/item", user_authenticated, middleware.GetUserProfile, user_in_db, user_active, controllers.AddItemToOrder(pool)) router.DELETE("/user/deactivate", middleware.IsAuthenticated(authenticator), middleware.GetUserProfile, user_in_db, user_active, user_is_admin, controllers.DeactivateUser(pool)) diff --git a/api/queries/ItemQueries.go b/api/queries/ItemQueries.go index e09e3cc..bddf5f0 100644 --- a/api/queries/ItemQueries.go +++ b/api/queries/ItemQueries.go @@ -10,13 +10,23 @@ INSERT INTO item_price_history(item_id, price) VALUES ($1, $2); const SET_ITEM_PRICE_VALID_TO_DATE = ` UPDATE item_price_history SET valid_to = now() -FROM ( - SELECT item_id, MAX(valid_from) FROM item_price_history WHERE item_id = $1 -) as o -WHERE item_id IN o.item_id +WHERE item_id IN ( + SELECT item_id FROM item_price_history WHERE item_id = $1 ORDER BY valid_from DESC LIMIT 1 +) ` -const GET_ITEM_PRICE = ` +const GET_CURRENT_ITEM_PRICE = ` +SELECT + i.id, + i.item_name, + iph.price AS unit_price + FROM item i + INNER JOIN item_price_history iph ON iph.item_id = i.id + AND i.id = $1 + ORDER BY iph.valid_from DESC + LIMIT 1; +` +const GET_ORDER_ITEM_PRICE = ` SELECT oi.item_id, oi.order_id, diff --git a/api/queries/OrderQueries.go b/api/queries/OrderQueries.go index db51478..31f22e2 100644 --- a/api/queries/OrderQueries.go +++ b/api/queries/OrderQueries.go @@ -4,6 +4,24 @@ const CREATE_ORDER = ` INSERT INTO order_record(user_id, orderer, date_due, date_placed) VALUES ($1, $2, $3, $4); ` +const GET_ORDER_BY_ORDER_INFORMATION = ` +SELECT + id, + user_id, + orderer, + date_due, + date_placed, + amount_paid, + filled, + delivered +FROM + order_record +WHERE + user_id = $1 + AND orderer = $2 + AND date_placed = $3; +` + const SET_ORDER_FILLED = ` UPDATE order_record SET filled = $1 WHERE id = $2 ` @@ -17,15 +35,23 @@ UPDATE order_record SET amount_paid = $1 WHERE id = $2 ` const GET_ORDER_TOTAL_AND_BALANCE = ` -SELECT - SUM(oi.quantity * iph.price) AS order_total, +SELECT + orec.id, order_total - orec.amount_paid AS balance FROM - order_item oi - INNER JOIN item i ON oi.item_id = i.init_db_pool - AND oi.order_id = $1 - INNER JOIN order_record orec ON oi.order_id = orec.order_id - INNER JOIN item_price_history iph ON iph.item_id = i.id - AND iph.valid_from <= oi.created_at - AND (iph.valid_to IS NULL OR iph.valid_to > oi.created_at); + order_record orec + INNER JOIN ( + SELECT + orec.id, + SUM(quantity * price) AS order_total + FROM + order_record orec + INNER JOIN order_item oi ON oi.order_id = orec.id + INNER JOIN item i ON oi.item_id = i.id + AND oi.order_id = 1 + INNER JOIN item_price_history iph ON iph.item_id = i.id + AND iph.valid_from <= oi.created_at + AND (iph.valid_to IS NULL OR iph.valid_to > oi.created_at) + GROUP BY orec.id + ) totals ON orec.id = totals.id ` diff --git a/api/queries/UserQueries.go b/api/queries/UserQueries.go index d77c828..df5bdea 100644 --- a/api/queries/UserQueries.go +++ b/api/queries/UserQueries.go @@ -41,8 +41,8 @@ SELECT ordr_user.id, user_name, position_name, - active, - is_admin + active::boolean, + is_admin::boolean FROM ordr_user INNER JOIN ordr_position