Restructure project

This commit is contained in:
Florian Hoss 2023-07-04 23:07:23 +02:00
parent e44e7caa11
commit 16b2f17301
46 changed files with 1744 additions and 1265 deletions

124
internal/controller/bill.go Normal file
View file

@ -0,0 +1,124 @@
package controller
import (
"fmt"
"strconv"
"time"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/types"
)
type (
Bill struct {
ID uint64 `gorm:"primaryKey" json:"id" validate:"optional"`
TableID uint64 `json:"table_id" validate:"required"`
Total float32 `json:"total" validate:"required"`
CreatedAt int64 `json:"created_at" validate:"optional"`
}
BillItem struct {
ID uint64 `gorm:"primaryKey" json:"id" validate:"optional"`
BillID uint64 `json:"bill_id" validate:"required"`
Description string `json:"description" validate:"required"`
Total float32 `json:"total" validate:"required"`
Price float32 `json:"price" validate:"required"`
Amount uint64 `json:"amount" validate:"required"`
ItemType types.ItemType `json:"item_type" validate:"required"`
}
)
func (c *Controller) DoesBillExist(id string) (Bill, error) {
var bill Bill
result := c.orm.Limit(1).Find(&bill, id)
if result.RowsAffected == 0 {
return bill, fmt.Errorf(types.CannotFind.String())
}
return bill, nil
}
func (c *Controller) GetAllBillItems(billId uint64) ([]BillItem, error) {
var billItems []BillItem
result := c.orm.Where("bill_id = ?", billId).Find(&billItems)
if result.RowsAffected == 0 {
return billItems, fmt.Errorf(types.CannotFind.String())
}
return billItems, nil
}
func getDate(year string, month string, day string) (time.Time, error) {
yearI, yearErr := strconv.Atoi(year)
if yearErr != nil {
return time.Time{}, fmt.Errorf("jahr " + types.CannotParse.String())
}
monthI, monthErr := strconv.Atoi(month)
if monthErr != nil {
return time.Time{}, fmt.Errorf("monat " + types.CannotParse.String())
}
dayI, dayErr := strconv.Atoi(day)
if dayErr != nil {
return time.Time{}, fmt.Errorf("tag " + types.CannotParse.String())
}
loc, locErr := time.LoadLocation("Local")
if locErr != nil {
return time.Time{}, fmt.Errorf("timezone " + types.CannotParse.String())
}
return time.Date(yearI, time.Month(monthI), dayI, 0, 0, 0, 0, loc), nil
}
func (c *Controller) GetAllBills(year string, month string, day string) ([]Bill, error) {
var bills []Bill
today, err := getDate(year, month, day)
if err != nil {
return bills, err
}
beginningOfDay := today.Unix()
endOfDay := today.Add(23 * time.Hour).Add(59 * time.Minute).Add(59 * time.Second).Unix()
c.orm.Where("created_at BETWEEN ? AND ?", beginningOfDay, endOfDay).Order("created_at").Find(&bills)
return bills, nil
}
func (c *Controller) createBill(options GetOrderOptions) (Bill, error) {
orders := c.getAllOrdersForTable(options)
var bill Bill
var total float32 = 0
for _, order := range orders {
total += order.Total
}
bill.TableID = options.TableId
bill.Total = total
err := c.orm.Create(&bill).Error
if err != nil {
return bill, fmt.Errorf(types.CannotCreate.String())
}
for _, order := range orders {
billItem := BillItem{
BillID: bill.ID,
Description: order.OrderItem.Description,
Total: order.Total,
Price: order.OrderItem.Price,
Amount: order.OrderCount,
ItemType: order.OrderItem.ItemType,
}
c.orm.Create(&billItem)
}
ordersToDelete := c.getAllOrdersForTable(GetOrderOptions{TableId: options.TableId, Grouped: false, Filter: options.Filter})
err = c.orm.Delete(&ordersToDelete).Error
if err != nil {
return bill, fmt.Errorf(types.CannotDelete.String())
}
c.publishMessage(StatusMessage{
Type: types.DeleteAll,
Payload: ordersToDelete,
})
return bill, nil
}
func (c *Controller) deleteBill(bill *Bill) error {
err := c.orm.Delete(bill).Error
if err != nil {
return fmt.Errorf(types.CannotDelete.String())
}
billItemsToDelete, _ := c.GetAllBillItems(bill.ID)
c.orm.Delete(&billItemsToDelete)
return nil
}

View file

@ -0,0 +1,34 @@
package controller
import (
"github.com/r3labs/sse/v2"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/database"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/env"
"gorm.io/gorm"
)
type Controller struct {
orm *gorm.DB
env *env.Config
SSE *sse.Server
}
func NewController(env *env.Config) *Controller {
db := database.NewDatabaseConnection(&database.Database{
Host: env.DB_Host,
User: env.DB_User,
Password: env.DB_Password,
Database: env.DB_Database,
})
db.AutoMigrate(&Table{})
db.AutoMigrate(&Order{})
db.AutoMigrate(&OrderItem{})
db.AutoMigrate(&Bill{})
db.AutoMigrate(&BillItem{})
db.AutoMigrate(&User{})
ctrl := Controller{orm: db, env: env, SSE: sse.New()}
ctrl.setupEventChannel()
return &ctrl
}

View file

@ -0,0 +1,25 @@
package controller
import (
"encoding/json"
"github.com/r3labs/sse/v2"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/types"
)
const ServerSideEvent = "sse"
type StatusMessage struct {
Type types.NotifierType `json:"type"`
Payload []Order `json:"payload"`
}
func (c *Controller) setupEventChannel() {
c.SSE.AutoReplay = false
c.SSE.CreateStream(ServerSideEvent)
}
func (c *Controller) publishMessage(msg StatusMessage) {
json, _ := json.Marshal(msg)
c.SSE.Publish(ServerSideEvent, &sse.Event{Data: json})
}

View file

@ -0,0 +1,168 @@
package controller
import (
"fmt"
"time"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/types"
"gorm.io/gorm"
)
type (
Order struct {
ID uint64 `gorm:"primaryKey" json:"id" validate:"optional"`
TableID uint64 `json:"table_id" validate:"required"`
OrderItemID uint64 `json:"order_item_id" validate:"required"`
OrderItem OrderItem `json:"order_item" validate:"required"`
UpdatedAt int64 `json:"updated_at" validate:"optional"`
IsServed bool `json:"is_served" default:"false" validate:"required"`
Total float32 `json:"total" validate:"required"`
OrderCount uint64 `json:"order_count" validate:"required"`
}
OrderItem struct {
ID uint64 `gorm:"primaryKey" json:"id" validate:"optional"`
ItemType types.ItemType `json:"item_type" validate:"required"`
Description string `json:"description" validate:"required"`
Price float32 `json:"price" validate:"required"`
}
GetOrderOptions struct {
TableId uint64 `json:"table_id"`
Grouped bool `json:"grouped"`
Filter []string `json:"filter"`
}
)
func updateTableUpdatedAt(tx *gorm.DB, o *Order) {
var table Table
tx.Where("id = ?", o.TableID).First(&table)
table.UpdatedAt = time.Now().Unix()
tx.Save(&table)
}
func (o *Order) AfterCreate(tx *gorm.DB) (err error) {
updateTableUpdatedAt(tx, o)
return
}
func (o *Order) AfterDelete(tx *gorm.DB) (err error) {
updateTableUpdatedAt(tx, o)
return
}
func (c *Controller) doesOrderItemExist(id string) (OrderItem, error) {
var orderItem OrderItem
result := c.orm.Limit(1).Find(&orderItem, id)
if result.RowsAffected == 0 {
return orderItem, fmt.Errorf(types.CannotFind.String())
}
return orderItem, nil
}
func (c *Controller) doesOrderExist(id string) (Order, error) {
var order Order
result := c.orm.Limit(1).Find(&order, id)
if result.RowsAffected == 0 {
return order, fmt.Errorf(types.CannotFind.String())
}
return order, nil
}
func (c *Controller) getAllActiveOrders() []Order {
var orders []Order
c.orm.Model(&Order{}).Joins("OrderItem").Where("is_served = ?", 0).Order("updated_at").Find(&orders)
return orders
}
func (c *Controller) getAllOrdersForTable(options GetOrderOptions) []Order {
var orders []Order
if options.Grouped {
if len(options.Filter) == 0 {
c.orm.Model(&Order{}).Joins("OrderItem").Select("table_id, order_item_id, sum(price) as total, count(order_item_id) as order_count").Group("order_item_id").Where("table_id = ?", options.TableId).Order("item_type, description").Find(&orders)
} else {
c.orm.Model(&Order{}).Find(&orders, options.Filter).Joins("OrderItem").Select("table_id, order_item_id, sum(price) as total, count(order_item_id) as order_count").Group("order_item_id").Where("table_id = ?", options.TableId).Order("item_type, description").Find(&orders)
}
} else {
if len(options.Filter) == 0 {
c.orm.Model(&Order{}).Joins("OrderItem").Where("table_id = ?", options.TableId).Order("item_type, description").Find(&orders)
} else {
c.orm.Model(&Order{}).Find(&orders, options.Filter).Where("table_id = ?", options.TableId).Find(&orders)
}
}
return orders
}
func (c *Controller) createOrder(order *Order) error {
err := c.orm.Create(order).Error
if err != nil {
return fmt.Errorf(types.CannotCreate.String())
}
c.orm.Model(&Order{}).Joins("OrderItem").First(order)
c.publishMessage(StatusMessage{
Type: types.Create,
Payload: []Order{*order},
})
return nil
}
func (c *Controller) updateOrder(old *Order, new *Order) error {
err := c.orm.First(old).Updates(new).Error
if err != nil {
return fmt.Errorf(types.CannotUpdate.String())
}
if new.IsServed {
c.publishMessage(StatusMessage{
Type: types.Delete,
Payload: []Order{*new},
})
}
return nil
}
func (c *Controller) deleteOrder(tableId string, orderItemId string) error {
var order Order
err := c.orm.Where("table_id = ? AND order_item_id = ?", tableId, orderItemId).Last(&order).Error
if err != nil {
return fmt.Errorf(types.CannotFind.String())
}
err = c.orm.Delete(&order).Error
if err != nil {
return fmt.Errorf(types.CannotDelete.String())
}
c.publishMessage(StatusMessage{
Type: types.Delete,
Payload: []Order{order},
})
return nil
}
func (c *Controller) getOrderItemsForType(itemType string) []OrderItem {
var orderItems []OrderItem
c.orm.Order("description").Where("item_type = ?", types.ParseItemType(itemType)).Find(&orderItems)
return orderItems
}
func (c *Controller) createOrderItem(oderItem *OrderItem) error {
err := c.orm.Create(oderItem).Error
if err != nil {
return fmt.Errorf(types.CannotCreate.String())
}
return nil
}
func (c *Controller) updateOrderItem(old *OrderItem, new *OrderItem) error {
err := c.orm.First(old).Updates(new).Error
if err != nil {
return fmt.Errorf(types.CannotUpdate.String())
}
return nil
}
func (c *Controller) deleteOrderItem(oderItem *OrderItem) error {
err := c.orm.Delete(oderItem).Error
if err != nil {
return fmt.Errorf(types.CannotDelete.String())
}
return nil
}

View file

@ -0,0 +1,105 @@
package controller
import (
"net/http"
"strconv"
"strings"
"github.com/labstack/echo/v4"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/types"
)
// @Schemes
// @Summary get all bills
// @Description gets all bills as array
// @Tags bills
// @Produce json
// @Param year query int true "year"
// @Param month query int true "month (1-12)"
// @Param day query int true "day (1-31)"
// @Success 200 {array} Bill
// @Router /bills [get]
func (c *Controller) GetBills(ctx echo.Context) error {
year := ctx.QueryParam("year")
month := ctx.QueryParam("month")
day := ctx.QueryParam("day")
if year == "" || month == "" || day == "" {
return echo.NewHTTPError(http.StatusInternalServerError, types.MissingInformation.String())
}
bills, err := c.GetAllBills(year, month, day)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusOK, bills)
}
// @Schemes
// @Summary get all billItems
// @Description gets all billItems for bill
// @Tags bills
// @Produce json
// @Param bill query int true "Bill ID"
// @Success 200 {array} BillItem
// @Router /bills/items [get]
func (c *Controller) GetBillItems(ctx echo.Context) error {
bill, err := c.DoesBillExist(ctx.QueryParam("bill"))
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
billItems, err := c.GetAllBillItems(bill.ID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusOK, billItems)
}
// @Schemes
// @Summary create new bill
// @Description creates a new bill and returns it
// @Tags bills
// @Produce json
// @Param table query int true "Table ID"
// @Param filter query string false "filter"
// @Success 201 {object} Bill
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /bills [post]
func (c *Controller) CreateBill(ctx echo.Context) error {
table, tableErr := strconv.ParseUint(ctx.QueryParam("table"), 10, 64)
if tableErr != nil {
return echo.NewHTTPError(http.StatusBadRequest, types.MissingInformation.String())
}
stringFiler := ctx.QueryParam("filter")
var filter []string
if stringFiler != "" {
filter = strings.Split(stringFiler, ",")
}
bill, err := c.createBill(GetOrderOptions{TableId: table, Grouped: true, Filter: filter})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusCreated, bill)
}
// @Schemes
// @Summary delete a bill
// @Description deletes a bill
// @Tags bills
// @Produce json
// @Param id path int true "Bill ID"
// @Success 200 "OK"
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /bills/{id} [delete]
func (c *Controller) DeleteBill(ctx echo.Context) error {
id := ctx.Param("id")
bill, err := c.DoesBillExist(id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
err = c.deleteBill(&bill)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.NoContent(http.StatusOK)
}

View file

@ -0,0 +1,209 @@
package controller
import (
"net/http"
"strconv"
"strings"
"github.com/labstack/echo/v4"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/types"
)
// @Schemes
// @Summary get all orders
// @Description gets all orders as array
// @Tags orders
// @Produce json
// @Param table query int false "Table ID"
// @Param grouping query bool false "grouping"
// @Param filter query string false "filter"
// @Success 200 {array} Order
// @Router /orders [get]
func (c *Controller) GetOrders(ctx echo.Context) error {
table, _ := strconv.ParseUint(ctx.QueryParam("table"), 10, 64)
grouping, _ := strconv.ParseBool(ctx.QueryParam("grouping"))
stringFiler := ctx.QueryParam("filter")
var filter []string
if stringFiler != "" {
filter = strings.Split(stringFiler, ",")
}
options := GetOrderOptions{TableId: table, Grouped: grouping, Filter: filter}
var orders []Order
if options.TableId == 0 {
orders = c.getAllActiveOrders()
} else {
orders = c.getAllOrdersForTable(options)
}
return ctx.JSON(http.StatusOK, orders)
}
// @Schemes
// @Summary create new order
// @Description creates a new order and returns it
// @Tags orders
// @Accept json
// @Produce json
// @Param item query int true "OrderItem ID"
// @Param table query int true "Table ID"
// @Success 201 {object} Order
// @Failure 400
// @Failure 500 "Internal Server Error"
// @Router /orders [post]
func (c *Controller) CreateOrder(ctx echo.Context) error {
table, err1 := strconv.ParseUint(ctx.QueryParam("table"), 10, 64)
item, err2 := strconv.ParseUint(ctx.QueryParam("item"), 10, 64)
if err1 != nil || err2 != nil {
return echo.NewHTTPError(http.StatusBadRequest, types.MissingInformation.String())
}
order := Order{TableID: table, OrderItemID: item, IsServed: false}
err := c.createOrder(&order)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusCreated, order)
}
// @Schemes
// @Summary delete an order
// @Description deletes an order from the database
// @Tags orders
// @Produce json
// @Param item query int true "OrderItem ID"
// @Param table query int true "Table ID"
// @Success 200 "OK"
// @Failure 400 "Bad Request"
// @Failure 500 "Internal Server Error"
// @Router /orders [delete]
func (c *Controller) DeleteOrder(ctx echo.Context) error {
item := ctx.QueryParam("item")
table := ctx.QueryParam("table")
if table == "" || item == "" {
return echo.NewHTTPError(http.StatusBadRequest, types.MissingInformation.String())
}
err := c.deleteOrder(table, item)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.NoContent(http.StatusOK)
}
// @Schemes
// @Summary update an order
// @Description updates an order with provided information
// @Tags orders
// @Accept json
// @Produce json
// @Param order body Order true "updated Order"
// @Success 200 {object} Order
// @Failure 400 "Bad Request"
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /orders [put]
func (c *Controller) UpdateOrder(ctx echo.Context) error {
var newOrder Order
err := ctx.Bind(&newOrder)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
oldOrder, err := c.doesOrderExist(strconv.Itoa(int(newOrder.ID)))
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
err = c.updateOrder(&oldOrder, &newOrder)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusOK, newOrder)
}
// @Schemes
// @Summary get all orderItems
// @Description gets all orderItems as array
// @Tags orderItems
// @Produce json
// @Param type query int true "ItemType"
// @Success 200 {array} OrderItem
// @Router /orders/items [get]
func (c *Controller) GetOrderItems(ctx echo.Context) error {
orderType := ctx.QueryParam("type")
if orderType == "" {
return echo.NewHTTPError(http.StatusBadRequest, types.MissingInformation.String())
}
return ctx.JSON(http.StatusOK, c.getOrderItemsForType(orderType))
}
// @Schemes
// @Summary create new orderItem
// @Description creates a new orderItem and returns it
// @Tags orderItems
// @Accept json
// @Produce json
// @Param order body OrderItem true "OrderItem ID"
// @Success 201 {object} OrderItem
// @Failure 400 "Bad Request"
// @Failure 500 "Internal Server Error"
// @Router /orders/items [post]
func (c *Controller) CreateOrderItem(ctx echo.Context) error {
var orderItem OrderItem
err := ctx.Bind(&orderItem)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
err = c.createOrderItem(&orderItem)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusCreated, orderItem)
}
// @Schemes
// @Summary update a orderItem
// @Description updates a orderItem with provided information
// @Tags orderItems
// @Accept json
// @Produce json
// @Param orderItem body OrderItem true "updated OrderItem"
// @Success 200 {object} OrderItem
// @Failure 400 "Bad Request"
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /orders/items [put]
func (c *Controller) UpdateOrderItem(ctx echo.Context) error {
var newOrderItem OrderItem
err := ctx.Bind(&newOrderItem)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
oldOrderItem, err := c.doesOrderItemExist(strconv.Itoa(int(newOrderItem.ID)))
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
err = c.updateOrderItem(&oldOrderItem, &newOrderItem)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusOK, newOrderItem)
}
// @Schemes
// @Summary delete an orderItem
// @Description deletes an orderItem from the database
// @Tags orderItems
// @Produce json
// @Param id path int true "OrderItem ID"
// @Success 200 "OK"
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /orders/items/{id} [delete]
func (c *Controller) DeleteOrderItem(ctx echo.Context) error {
id := ctx.Param("id")
orderItem, err := c.doesOrderItemExist(id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
err = c.deleteOrderItem(&orderItem)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.NoContent(http.StatusOK)
}

View file

@ -0,0 +1,51 @@
package controller
import (
"net/http"
"github.com/labstack/echo/v4"
)
// @Schemes
// @Summary get all active tables
// @Description gets all active tables as array
// @Tags tables
// @Produce json
// @Success 200 {array} Table
// @Router /tables [get]
func (c *Controller) GetTables(ctx echo.Context) error {
return ctx.JSON(http.StatusOK, c.GetAllTables())
}
// @Schemes
// @Summary create new table
// @Description creates a new table and returns it
// @Tags tables
// @Accept json
// @Produce json
// @Success 201 {object} Table "Table has been created"
// @Failure 500 "Internal Server Error"
// @Router /tables [post]
func (c *Controller) CreateTable(ctx echo.Context) error {
table, err := c.CreateNewTable()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusCreated, table)
}
// @Schemes
// @Summary delete the latest table
// @Description deletes the latest table from the database
// @Tags tables
// @Produce json
// @Success 200 "OK"
// @Failure 500 "Internal Server Error"
// @Router /tables [delete]
func (c *Controller) DeleteTable(ctx echo.Context) error {
err := c.DeleteLatestTable()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.NoContent(http.StatusOK)
}

View file

@ -0,0 +1,54 @@
package controller
import (
"net/http"
"github.com/labstack/echo/v4"
)
// @Schemes
// @Summary get a user
// @Description gets a user
// @Tags users
// @Produce json
// @Param username path string true "Username"
// @Success 200 {object} User
// @Failure 500 "Internal Server Error"
// @Router /users/{username} [get]
func (c *Controller) GetUser(ctx echo.Context) error {
username := ctx.Param("username")
u, err := c.getUserOrCreate(username)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return ctx.JSON(http.StatusOK, u)
}
// @Schemes
// @Summary update a user
// @Description updates a user with provided information
// @Tags users
// @Accept json
// @Produce json
// @Param user body User true "updated User"
// @Success 200 {object} User
// @Failure 400 "Bad Request"
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /users [put]
func (c *Controller) UpdateUser(ctx echo.Context) error {
var newUser User
err := ctx.Bind(&newUser)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
oldUser, err := c.doesUserExist(newUser.Username)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
err = c.updateUser(&oldUser, &newUser)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusOK, newUser)
}

View file

@ -0,0 +1,74 @@
package controller
import (
"fmt"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/types"
"gorm.io/plugin/soft_delete"
)
type Table struct {
ID uint64 `gorm:"primaryKey" json:"id" validate:"optional"`
OrderCount uint64 `json:"order_count" validate:"required"`
Total float32 `json:"total" validate:"required"`
UpdatedAt int64 `json:"updated_at" validate:"optional"`
IsDeleted soft_delete.DeletedAt `gorm:"softDelete:flag" json:"is_deleted" swaggerignore:"true"`
}
func (c *Controller) GetAllTables() []Table {
var tables []Table
c.orm.Model(
&Table{},
).Joins(
"left join orders on tables.id = orders.table_id",
).Joins(
"left join order_items on orders.order_item_id = order_items.id",
).Select(
"tables.id, tables.updated_at, sum(order_items.price) as total, count(orders.id) as order_count",
).Group(
"tables.id",
).Order("tables.id").Find(&tables)
return tables
}
func (c *Controller) CreateNewTable() (Table, error) {
var table Table
var err error
result := c.orm.Unscoped().Where("is_deleted = ?", 1).Limit(1).Find(&table)
if result.RowsAffected == 0 {
err = c.orm.Create(&table).Error
} else {
table.IsDeleted = 0
err = c.orm.Unscoped().Save(&table).Error
}
if err != nil {
return table, fmt.Errorf(types.CannotCreate.String())
}
return table, nil
}
func (c *Controller) DeleteLatestTable() error {
var table Table
err := c.orm.Model(
&Table{},
).Joins(
"left join orders on tables.id = orders.table_id",
).Joins(
"left join order_items on orders.order_item_id = order_items.id",
).Select(
"tables.id, count(orders.id) as order_count",
).Group(
"tables.id",
).Last(&table).Error
if err != nil {
return fmt.Errorf(types.CannotFind.String())
}
if table.OrderCount != 0 {
return fmt.Errorf(types.StillInUse.String())
}
err = c.orm.Delete(&table).Error
if err != nil {
return fmt.Errorf(types.CannotDelete.String())
}
return nil
}

View file

@ -0,0 +1,42 @@
package controller
import (
"fmt"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/types"
)
type User struct {
Username string `gorm:"primaryKey" json:"username" validate:"required"`
ShowColdDrinks bool `json:"show_cold_drinks" validate:"required"`
ShowHotDrinks bool `json:"show_hot_drinks" validate:"required"`
}
func (c *Controller) doesUserExist(username string) (User, error) {
var user User
result := c.orm.Limit(1).Find(&user, "username = ?", username)
if result.RowsAffected == 0 {
return user, fmt.Errorf(types.CannotFind.String())
}
return user, nil
}
func (c *Controller) getUserOrCreate(username string) (User, error) {
var user User
err := c.orm.Where(User{Username: username}).Attrs(User{ShowHotDrinks: true, ShowColdDrinks: true}).FirstOrCreate(&user).Error
if err != nil {
return user, fmt.Errorf(types.CannotCreate.String())
}
return user, nil
}
func (c *Controller) updateUser(old *User, new *User) error {
err := c.orm.First(old).Updates(map[string]interface{}{
"Username": new.Username,
"ShowColdDrinks": new.ShowColdDrinks,
"ShowHotDrinks": new.ShowHotDrinks}).Error
if err != nil {
return fmt.Errorf(types.CannotUpdate.String())
}
return nil
}

View file

@ -0,0 +1,81 @@
package database
import (
"fmt"
"net"
"os"
"time"
"go.uber.org/zap"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"moul.io/zapgorm2"
)
type Database struct {
Host string
User string
Password string
Database string
}
const Storage = "storage/"
func init() {
os.Mkdir(Storage, os.ModePerm)
}
func (d *Database) tryDbConnection() {
i := 1
total := 20
for i <= total {
ln, err := net.DialTimeout("tcp", d.Host, 1*time.Second)
if err != nil {
if i == total {
zap.L().Fatal("Failed connecting to database", zap.Int("attempt", i))
}
zap.L().Warn("Connecting to database", zap.Int("attempt", i))
time.Sleep(2 * time.Second)
i++
} else {
_ = ln.Close()
zap.L().Info("Connected to database", zap.Int("attempt", i))
i = total + 1
}
}
}
func (d *Database) initializeMySql(conf *gorm.Config) *gorm.DB {
var err error
d.tryDbConnection()
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
d.User,
d.Password,
d.Host,
d.Database,
)
orm, err := gorm.Open(mysql.Open(dsn), conf)
if err != nil {
zap.L().Error(err.Error())
}
return orm
}
func (d *Database) initializeSqLite(conf *gorm.Config) *gorm.DB {
var err error
orm, err := gorm.Open(sqlite.Open(Storage+"db.sqlite?_pragma=foreign_keys(1)"), conf)
if err != nil {
zap.L().Error(err.Error())
}
return orm
}
func NewDatabaseConnection(d *Database) *gorm.DB {
logger := zapgorm2.New(zap.L())
conf := &gorm.Config{Logger: logger, PrepareStmt: true}
if d.Host == "" {
return d.initializeSqLite(conf)
}
return d.initializeMySql(conf)
}

70
internal/env/env.go vendored Normal file
View file

@ -0,0 +1,70 @@
package env
import (
"errors"
"fmt"
"os"
"github.com/caarlos0/env/v8"
"github.com/containrrr/shoutrrr"
"github.com/go-playground/validator/v10"
)
type Config struct {
TimeZone string `env:"TZ" envDefault:"Etc/UTC" validate:"timezone"`
Port int `env:"PORT" envDefault:"8080" validate:"min=1024,max=49151"`
LogLevel string `env:"LOG_LEVEL" envDefault:"info" validate:"oneof=debug info warn error panic fatal"`
Version string `env:"APP_VERSION" envDefault:"v0.0.0"`
SwaggerHost string `env:"SWAGGER_HOST" envDefault:"https://cafe.test"`
DB_Host string `env:"DB_HOST"`
DB_User string `env:"DB_USER"`
DB_Password string `env:"DB_PASSWORD"`
DB_Database string `env:"DB_DATABASE"`
}
var errParse = errors.New("error parsing environment variables")
func Parse() (*Config, error) {
cfg := &Config{}
if err := env.Parse(cfg); err != nil {
return cfg, err
}
if err := validateContent(cfg); err != nil {
return cfg, err
}
setAllDefaultEnvs(cfg)
return cfg, nil
}
func newEnvValidator() *validator.Validate {
validate := validator.New()
validate.RegisterValidation(`shoutrrr`, func(fl validator.FieldLevel) bool {
value := fl.Field().Interface().(string)
_, err := shoutrrr.CreateSender(value)
return err == nil
})
return validate
}
func validateContent(cfg *Config) error {
validate := newEnvValidator()
err := validate.Struct(cfg)
if err != nil {
if _, ok := err.(*validator.InvalidValidationError); ok {
return err
} else {
for _, err := range err.(validator.ValidationErrors) {
return err
}
}
return errParse
}
return nil
}
func setAllDefaultEnvs(cfg *Config) {
os.Setenv("TZ", cfg.TimeZone)
os.Setenv("PORT", fmt.Sprintf("%d", cfg.Port))
os.Setenv("LOG_LEVEL", cfg.LogLevel)
}

56
internal/env/env_test.go vendored Normal file
View file

@ -0,0 +1,56 @@
package env
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPortParser(t *testing.T) {
key := "PORT"
var err error
defer func() {
os.Unsetenv(key)
}()
os.Setenv(key, "1024")
_, err = Parse()
assert.Equal(t, err, nil, "Parsing should pass")
os.Setenv(key, "-12")
_, err = Parse()
assert.Equal(t, err.Error(), "Key: 'Config.Port' Error:Field validation for 'Port' failed on the 'min' tag", "Validation should fail")
os.Setenv(key, "60000")
_, err = Parse()
assert.Equal(t, err.Error(), "Key: 'Config.Port' Error:Field validation for 'Port' failed on the 'max' tag", "Validation should fail")
os.Setenv(key, "abc")
_, err = Parse()
assert.Equal(t, err.Error(), "env: parse error on field \"Port\" of type \"int\": strconv.ParseInt: parsing \"abc\": invalid syntax", "Parsing should fail")
}
func TestTimeZoneParser(t *testing.T) {
key := "TZ"
var err error
defer func() {
os.Unsetenv(key)
}()
os.Setenv(key, "Europe/Berlin")
_, err = Parse()
assert.Equal(t, err, nil, "Parsing should pass")
os.Setenv(key, "Etc/UTC")
_, err = Parse()
assert.Equal(t, err, nil, "Parsing should pass")
os.Setenv(key, "abc")
_, err = Parse()
assert.Equal(t, err.Error(), "Key: 'Config.TimeZone' Error:Field validation for 'TimeZone' failed on the 'timezone' tag", "Validation should fail")
os.Setenv(key, "-1")
_, err = Parse()
assert.Equal(t, err.Error(), "Key: 'Config.TimeZone' Error:Field validation for 'TimeZone' failed on the 'timezone' tag", "Validation should fail")
}

View file

@ -0,0 +1,44 @@
package logging
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestInfoLogger(t *testing.T) {
level := "info"
log := CreateLogger(level)
defer log.Sync()
assert.NotEmpty(t, log, "Logger should not be nil")
assert.Equal(t, log.Level().String(), level, fmt.Sprintf("Level should be %s", level))
}
func TestWarnLogger(t *testing.T) {
level := "warn"
log := CreateLogger(level)
defer log.Sync()
assert.NotEmpty(t, log, "Logger should not be nil")
assert.Equal(t, log.Level().String(), level, fmt.Sprintf("Level should be %s", level))
}
func TestDebugLogger(t *testing.T) {
level := "debug"
log := CreateLogger(level)
defer log.Sync()
assert.NotEmpty(t, log, "Logger should not be nil")
assert.Equal(t, log.Level().String(), level, fmt.Sprintf("Level should be %s", level))
}
func TestInvalidLogger(t *testing.T) {
level := "invalid"
log := CreateLogger(level)
defer log.Sync()
assert.NotEmpty(t, log, "Logger should not be nil")
assert.Equal(t, log.Level().String(), "info", "Level should be info")
}

View file

@ -0,0 +1,33 @@
package logging
import (
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func CreateLogger(logLevel string) *zap.Logger {
encoderCfg := zap.NewProductionEncoderConfig()
encoderCfg.TimeKey = "time"
encoderCfg.EncodeTime = zapcore.TimeEncoderOfLayout(time.StampMilli)
level := zap.NewAtomicLevelAt(zap.InfoLevel)
zapLevel, err := zap.ParseAtomicLevel(logLevel)
if err == nil {
level = zapLevel
}
config := zap.Config{
Level: level,
Development: false,
DisableCaller: false,
DisableStacktrace: false,
Sampling: nil,
Encoding: "json",
EncoderConfig: encoderCfg,
OutputPaths: []string{"stdout"},
ErrorOutputPaths: []string{"stderr"},
}
return zap.Must(config.Build())
}

View file

@ -0,0 +1,13 @@
package router
import (
"github.com/labstack/echo/v4"
)
func authHeader(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set("Remote-Groups", c.Request().Header.Get("Remote-Groups"))
c.Response().Header().Set("Remote-Name", c.Request().Header.Get("Remote-Name"))
return next(c)
}
}

94
internal/router/router.go Normal file
View file

@ -0,0 +1,94 @@
package router
import (
"net/http"
"net/url"
"strings"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
echoSwagger "github.com/swaggo/echo-swagger"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/docs"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/controller"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/env"
"go.uber.org/zap"
)
func InitRouter() *echo.Echo {
e := echo.New()
e.HideBanner = true
e.HidePort = true
e.Use(middleware.Recover())
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Skipper: func(c echo.Context) bool {
return strings.Contains(c.Request().URL.Path, "swagger")
},
}))
e.Pre(middleware.RemoveTrailingSlash())
e.Validator = &CustomValidator{Validator: newValidator()}
return e
}
func SetupRoutes(e *echo.Echo, ctrl *controller.Controller, env *env.Config) {
api := e.Group("/api")
{
tableGroup := api.Group("/tables")
{
tableGroup.GET("", ctrl.GetTables)
tableGroup.POST("", ctrl.CreateTable)
tableGroup.DELETE("", ctrl.DeleteTable)
}
orderGroup := api.Group("/orders")
{
orderGroup.GET("", ctrl.GetOrders)
orderGroup.POST("", ctrl.CreateOrder)
orderGroup.DELETE("", ctrl.DeleteOrder)
orderGroup.PUT("", ctrl.UpdateOrder)
orderGroup.GET("/sse", echo.WrapHandler(http.HandlerFunc(ctrl.SSE.ServeHTTP)))
orderItemGroup := orderGroup.Group("/items")
{
orderItemGroup.GET("", ctrl.GetOrderItems)
orderItemGroup.POST("", ctrl.CreateOrderItem)
orderItemGroup.PUT("", ctrl.UpdateOrderItem)
orderItemGroup.DELETE("/:id", ctrl.DeleteOrderItem)
}
}
billGroup := api.Group("/bills")
{
billGroup.GET("", ctrl.GetBills)
billGroup.POST("", ctrl.CreateBill)
billGroup.DELETE("/:id", ctrl.DeleteBill)
billItemGroup := billGroup.Group("/items")
{
billItemGroup.GET("", ctrl.GetBillItems)
}
}
userGroup := api.Group("/users")
{
userGroup.GET("/:username", ctrl.GetUser)
userGroup.PUT("", ctrl.UpdateUser)
}
health := api.Group("/health", authHeader)
{
health.GET("", func(ctx echo.Context) error {
return ctx.String(http.StatusOK, ".")
})
}
if env.SwaggerHost != "" {
docs.SwaggerInfo.Title = "Cafe"
docs.SwaggerInfo.Description = "This is the backend of a cafe"
docs.SwaggerInfo.Version = env.Version
docs.SwaggerInfo.BasePath = "/api"
parsed, _ := url.Parse(env.SwaggerHost)
docs.SwaggerInfo.Host = parsed.Host
api.GET("/swagger/*", echoSwagger.WrapHandler)
zap.L().Info("swagger running", zap.String("url", env.SwaggerHost+"/api/swagger/index.html"))
}
}
}

View file

@ -0,0 +1,17 @@
package router
import (
"github.com/go-playground/validator/v10"
)
type CustomValidator struct {
Validator *validator.Validate
}
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.Validator.Struct(i)
}
func newValidator() *validator.Validate {
return validator.New()
}

59
internal/types/types.go Normal file
View file

@ -0,0 +1,59 @@
package types
type (
ErrorResponses uint
ItemType uint
NotifierType uint
)
const (
Create NotifierType = iota
Delete
DeleteAll
)
const (
Food ItemType = iota
ColdDrink
HotDrink
)
const (
MissingInformation ErrorResponses = iota
CannotCreate
CannotUpdate
CannotDelete
CannotFind
StillInUse
CannotParse
)
func ParseItemType(itemType string) ItemType {
switch itemType {
case "0":
return Food
case "1":
return ColdDrink
default:
return HotDrink
}
}
func (e ErrorResponses) String() string {
switch e {
case MissingInformation:
return "fehlende Informationen"
case CannotCreate:
return "kann nicht gespeichert werden"
case CannotUpdate:
return "kann nicht geändert werden"
case CannotDelete:
return "kann nicht gelöscht werden"
case CannotFind:
return "kann nicht gefunden werden"
case StillInUse:
return "noch in Verwendung"
default:
return "kann nicht verarbeitet werden"
}
}