diff --git a/controllers/cartItemController.go b/controllers/cartItemController.go index aced86f..e5d2f4d 100644 --- a/controllers/cartItemController.go +++ b/controllers/cartItemController.go @@ -34,14 +34,6 @@ func NewCartItemController() CartItemController { return &cartItemController{} } -func GetShippingMethods() []models.Shipping { - return []models.Shipping{ - {Id: "germany", Name: "Germany (DHL)", Price: 3.99}, - {Id: "international", Name: "International (DHL)", Price: 5.99}, - {Id: "pickup", Name: "Pickup", Price: 0.00}, - } -} - func generateSessionId(length int) string { bytes := make([]byte, length) // 16 bytes = 128 bits _, err := rand.Read(bytes) @@ -63,7 +55,7 @@ func GetSessionId(ctx *gin.Context) string { } func GenerateToken() string { - return generateSessionId(8) + return generateSessionId(16) } func (rc *cartItemController) NewCartItemFromForm(ctx *gin.Context) (models.CartItem, error) { @@ -143,7 +135,7 @@ func (rc *cartItemController) NewAddressFromForm(ctx *gin.Context) (models.Addre func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order, error) { sessionId := GetSessionId(ctx) - status := models.OrderStatus("Received") + status := models.OrderStatus("AwaitingConfirmation") token := GenerateToken() email := ctx.PostForm("email") comment := ctx.PostForm("comment") @@ -162,14 +154,9 @@ func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order, // } //} - var shipping models.Shipping - for _, shippingMethod := range GetShippingMethods() { - if shippingMethod.Id == shippingStr { - shipping = shippingMethod - } - } + shipping, err := models.GetShippingMethod(shippingStr) - if shipping == (models.Shipping{}) { + if err != nil { return models.Order{}, fmt.Errorf("Invalid shipping method.") } @@ -267,7 +254,7 @@ func (rc *cartItemController) CartItemView(c *gin.Context) { data := CreateSessionData(c, gin.H{ "cartItems": cartItems, "priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals - "shipping": GetShippingMethods(), + "shipping": models.GetShippingMethods(), }) c.HTML(http.StatusOK, "cart.html", data) @@ -348,6 +335,11 @@ func (rc *cartItemController) EditItemHandler(c *gin.Context) { func (rc *cartItemController) CheckoutView(c *gin.Context) { shippingMethod := c.Query("shippingMethod") + if shippingMethod == "" { + rc.CartItemView(c) + return + } + c.HTML(http.StatusOK, "checkout.html", gin.H{ "askAddress": (shippingMethod != "pickup"), "shippingMethod": shippingMethod, @@ -366,10 +358,8 @@ func (rc *cartItemController) CheckoutHandler(c *gin.Context) { existingOrder, err := repositories.Orders.GetBySession(order.SessionId) if errors.Is(err, gorm.ErrRecordNotFound) { - fmt.Println("CREATE") _, err = repositories.Orders.Create(order) } else if err == nil { - fmt.Println("UPDATE") order.ID = existingOrder.ID order.CreatedAt = existingOrder.CreatedAt repositories.Orders.Update(order) @@ -385,19 +375,27 @@ func (rc *cartItemController) CheckoutHandler(c *gin.Context) { return } - var shipping models.Shipping - for _, shippingMethod := range GetShippingMethods() { - if shippingMethod.Id == order.Shipping { - shipping = shippingMethod - } + shipping, err := models.GetShippingMethod(order.Shipping) + if err != nil { + data := CreateSessionData(c, gin.H{ + "error": err, + "success": "", + }) + + c.HTML(http.StatusOK, "cart.html", data) + return } - priceProducts := 0.0 - for _, cartItem := range order.CartItems { - priceProducts += (float64(cartItem.Quantity) * cartItem.ItemVariant.Price) - } + priceProducts, priceTotal, err := order.CalculatePrices() + if err != nil { + data := CreateSessionData(c, gin.H{ + "error": err, + "success": "", + }) - priceTotal := priceProducts + shipping.Price + c.HTML(http.StatusOK, "cart.html", data) + return + } data := CreateSessionData(c, gin.H{ "error": "", @@ -411,19 +409,85 @@ func (rc *cartItemController) CheckoutHandler(c *gin.Context) { }) fmt.Println(order) - c.HTML(http.StatusOK, "order.html", data) + c.HTML(http.StatusOK, "orderpreview.html", data) } func (rc *cartItemController) OrderView(c *gin.Context) { - shippingMethod := c.Query("shippingMethod") + orderToken := c.Param("token") - c.HTML(http.StatusOK, "checkout.html", gin.H{ - "askAddress": (shippingMethod != "pickup"), - "shippingMethod": shippingMethod, - }) + order, err := repositories.Orders.GetByToken(orderToken) + if err != nil { + c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Order does not exist."}) + return + } + + shipping, err := models.GetShippingMethod(order.Shipping) + if err != nil { + c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Could not get shipping method"}) + return + } + + priceProducts, priceTotal, err := order.CalculatePrices() + if err != nil { + c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Could not calculate final prices"}) + return + } + + fmt.Printf("Order: %v\n", order) + fmt.Printf("PriceTotal: %v\n", priceTotal) + fmt.Printf("Amount Items: %v\n", len(order.CartItems)) + + for _, item := range order.CartItems { + fmt.Printf("Cartitem: %v", item) + } + + c.HTML(http.StatusOK, "order.html", CreateSessionData(c, gin.H{ + "error": "", + "success": "", + "order": order, + "shipping": shipping, + "priceProducts": fmt.Sprintf("%.2f", priceProducts), //round 2 decimals + "priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals + })) } func (rc *cartItemController) OrderHandler(c *gin.Context) { - //get order by session id - //generate token, preview payment info + confirmation := c.PostForm("confirm-order") + + if confirmation == "" { + c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Something went wrong, try again later"}) + return + } + + if confirmation != "true" { + c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Order was not confirmed."}) + return + } + + sessionId := GetSessionId(c) + order, err := repositories.Orders.GetBySession(sessionId) + + if err != nil { + c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Something went wrong, try again later"}) + return + } + + order.Status = models.AwaitingPayment + + err = order.Validate() + if err != nil { + c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err}) + return + } + + _, err = repositories.Orders.Update(order) + + if err != nil { + c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err}) + return + } + + //TODO: cartItemRepository delete all by session - otherwise items stay in cart after completing order.. + + c.Redirect(http.StatusFound, fmt.Sprintf("/order/%s", order.Token)) } diff --git a/main.go b/main.go index 99d0c16..499314b 100644 --- a/main.go +++ b/main.go @@ -90,7 +90,7 @@ func main() { viewRoutes.POST("/cart/edit", cartItemController.EditItemHandler) viewRoutes.GET("/checkout", cartItemController.CheckoutView) viewRoutes.POST("/checkout", cartItemController.CheckoutHandler) - viewRoutes.GET("/order", cartItemController.OrderView) + viewRoutes.GET("/order/:token", cartItemController.OrderView) viewRoutes.POST("/order", cartItemController.OrderHandler) //write middleware that redirects to homescreen on register/login/reset for logged in users diff --git a/models/cart.go b/models/cart.go index 4bfc04b..b7ba729 100644 --- a/models/cart.go +++ b/models/cart.go @@ -1,59 +1,161 @@ package models import ( + "fmt" "gorm.io/gorm" ) type OrderStatus string const ( - Received OrderStatus = "Received" - AwaitingPayment OrderStatus = "AwaitingPayment" - Payed OrderStatus = "Payed" - ReadyForPickup OrderStatus = "ReadyForPickup" - Shipped OrderStatus = "Shipped" - Cancelled OrderStatus = "Cancelled" + AwaitingConfirmation OrderStatus = "AwaitingConfirmation" + Received OrderStatus = "Received" + AwaitingPayment OrderStatus = "AwaitingPayment" + Payed OrderStatus = "Payed" + ReadyForPickup OrderStatus = "ReadyForPickup" + Shipped OrderStatus = "Shipped" + Cancelled OrderStatus = "Cancelled" ) type AddressInfo struct { - FirstName string `json:"firstname"` - LastName string `json:"lastname"` - Address string `json:"address"` + FirstName string `json:"firstname"` + LastName string `json:"lastname"` + Address string `json:"address"` PostalCode string `json:"postalcode"` - City string `json:"city"` - Country string `json:"country"` + City string `json:"city"` + Country string `json:"country"` } type Shipping struct { - Id string `json:"id"` - Name string `json:"name"` + Id string `json:"id"` + Name string `json:"name"` Price float64 `json:"price"` } +func GetShippingMethods() []Shipping { + return []Shipping{ + {Id: "germany", Name: "Germany (DHL)", Price: 3.99}, + {Id: "international", Name: "International (DHL)", Price: 5.99}, + {Id: "pickup", Name: "Pickup", Price: 0.00}, + } +} + +func GetShippingMethod(id string) (Shipping, error) { + var shipping Shipping + found := false + for _, shippingMethod := range GetShippingMethods() { + if shippingMethod.Id == id { + shipping = shippingMethod + found = true + } + } + + if !found { + return Shipping{}, fmt.Errorf("Shipping method does not exist.") + } + + return shipping, nil +} + type Order struct { gorm.Model - SessionId string `json:"sessionid" binding:"required" gorm:"not null"` - Status OrderStatus `json:"status"` - Token string `json:"token" binding:"required" gorm:"not null"` - Email string `json:"email"` - Comment string `json:"comment"` - FirstName string `json:"firstname"` - LastName string `json:"lastname"` - Address string `json:"address"` - PostalCode string `json:"postalcode"` - City string `json:"city"` - Country string `json:"country"` - Shipping string `json:"shipping"` - CartItems []CartItem `json:"cartitems" gorm:"foreignKey:OrderID"` + SessionId string `json:"sessionid" binding:"required" gorm:"not null"` + Status OrderStatus `json:"status"` + Token string `json:"token" binding:"required" gorm:"not null"` + Email string `json:"email"` + Comment string `json:"comment"` + FirstName string `json:"firstname"` + LastName string `json:"lastname"` + Address string `json:"address"` + PostalCode string `json:"postalcode"` + City string `json:"city"` + Country string `json:"country"` + Shipping string `json:"shipping"` + CartItems []CartItem `json:"cartitems" gorm:"foreignKey:OrderID"` +} + +func (o *Order) Validate() error { + //TODO: validate sessionId + if o.SessionId == "" { + return fmt.Errorf("Invalid SessionId") + } + + //TODO: validate token + if o.Token == "" { + return fmt.Errorf("Invalid Token") + } + + if o.Status == AwaitingConfirmation { + return fmt.Errorf("Order still awaiting confirmation.") + } + + if len(o.CartItems) == 0 { + return fmt.Errorf("Order is empty.") + } + + shipping, err := GetShippingMethod(o.Shipping) + + if err != nil { + return err + } + + //for pickup no address validation is necessary + if shipping.Id == "pickup" { + return nil + } + + return o.ValidateAddress() +} + +func (o *Order) ValidateAddress() error { + if o.FirstName == "" { + return fmt.Errorf("Firstname missing") + } + if o.LastName == "" { + return fmt.Errorf("Lastname missing") + } + + if o.Address == "" { + return fmt.Errorf("Address missing") + } + + if o.PostalCode == "" { + return fmt.Errorf("Postalcode missing") + } + + if o.City == "" { + return fmt.Errorf("City missing") + } + + if o.Country == "" { + return fmt.Errorf("Country missing") + } + return nil +} + +func (o *Order) CalculatePrices() (float64, float64, error) { + shipping, err := GetShippingMethod(o.Shipping) + if err != nil { + return 0, 0, err + } + + priceProducts := 0.0 + for _, cartItem := range o.CartItems { + priceProducts += (float64(cartItem.Quantity) * cartItem.ItemVariant.Price) + } + + priceTotal := priceProducts + shipping.Price + + return priceProducts, priceTotal, nil } type CartItem struct { gorm.Model - SessionId string `json:"sessionid" binding:"required" gorm:"not null"` - ShopItemId uint - ShopItem ShopItem `json:"shopitem" gorm:"foreignKey:ShopItemId"` //gorm one2one + SessionId string `json:"sessionid" binding:"required" gorm:"not null"` + ShopItemId uint + ShopItem ShopItem `json:"shopitem" gorm:"foreignKey:ShopItemId"` //gorm one2one ItemVariantId uint - ItemVariant ItemVariant `json:"itemvariant" gorm:"foreignKey:ItemVariantId"` //gorm one2one - Quantity int `json:"quantity" binding:"required"` - OrderID uint + ItemVariant ItemVariant `json:"itemvariant" gorm:"foreignKey:ItemVariantId"` //gorm one2one + Quantity int `json:"quantity" binding:"required"` + OrderID uint } diff --git a/repositories/OrderRepository.go b/repositories/OrderRepository.go index 41fa068..04d45a0 100644 --- a/repositories/OrderRepository.go +++ b/repositories/OrderRepository.go @@ -12,6 +12,7 @@ type OrderRepository interface { GetAll() ([]models.Order, error) GetById(string) (models.Order, error) GetBySession(string) (models.Order, error) + GetByToken(string) (models.Order, error) Update(models.Order) (models.Order, error) DeleteById(string) error } @@ -62,12 +63,19 @@ func (t *GORMOrderRepository) GetById(id string) (models.Order, error) { func (r *GORMOrderRepository) GetBySession(sessionId string) (models.Order, error) { var orders models.Order - result := r.DB.Preload("CartItems").Where("session_id = ?", sessionId).First(&orders) + result := r.DB.Preload("CartItems").Preload("CartItems.ShopItem").Preload("CartItems.ItemVariant").Where("session_id = ?", sessionId).First(&orders) return orders, result.Error } +func (r *GORMOrderRepository) GetByToken(token string) (models.Order, error) { + var orders models.Order + result := r.DB.Preload("CartItems").Preload("CartItems.ShopItem").Preload("CartItems.ItemVariant").Where("token = ?", token).First(&orders) + + return orders, result.Error +} + func (r *GORMOrderRepository) Update(order models.Order) (models.Order, error) { result := r.DB.Save(&order) if result.Error != nil { diff --git a/static/output.css b/static/output.css index 87db094..1823588 100644 --- a/static/output.css +++ b/static/output.css @@ -587,6 +587,22 @@ video { right: 0px; } +.-bottom-\[1\.75rem\] { + bottom: -1.75rem; +} + +.end-0 { + inset-inline-end: 0px; +} + +.left-1\/2 { + left: 50%; +} + +.start-0 { + inset-inline-start: 0px; +} + .col-span-12 { grid-column: span 12 / span 12; } @@ -698,6 +714,16 @@ video { aspect-ratio: 1 / 1; } +.size-5 { + width: 1.25rem; + height: 1.25rem; +} + +.size-6 { + width: 1.5rem; + height: 1.5rem; +} + .h-10 { height: 2.5rem; } @@ -798,6 +824,11 @@ video { flex-grow: 1; } +.-translate-x-1\/2 { + --tw-translate-x: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + .cursor-pointer { cursor: pointer; } @@ -810,6 +841,10 @@ video { grid-template-columns: repeat(12, minmax(0, 1fr)); } +.grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + .flex-col { flex-direction: column; } @@ -822,6 +857,14 @@ video { align-items: center; } +.justify-start { + justify-content: flex-start; +} + +.justify-end { + justify-content: flex-end; +} + .justify-center { justify-content: center; } @@ -1024,6 +1067,11 @@ video { background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); } +.bg-gray-600 { + --tw-bg-opacity: 1; + background-color: rgb(75 85 99 / var(--tw-bg-opacity, 1)); +} + .fill-red-50 { fill: #fef2f2; } @@ -1301,6 +1349,11 @@ video { color: rgb(255 255 255 / var(--tw-text-opacity, 1)); } +.text-blue-600 { + --tw-text-opacity: 1; + color: rgb(37 99 235 / var(--tw-text-opacity, 1)); +} + .underline { text-decoration-line: underline; } @@ -1357,6 +1410,37 @@ video { color: rgb(156 163 175 / var(--tw-text-opacity, 1)); } +.after\:mt-4::after { + content: var(--tw-content); + margin-top: 1rem; +} + +.after\:block::after { + content: var(--tw-content); + display: block; +} + +.after\:h-1::after { + content: var(--tw-content); + height: 0.25rem; +} + +.after\:w-full::after { + content: var(--tw-content); + width: 100%; +} + +.after\:rounded-lg::after { + content: var(--tw-content); + border-radius: 0.5rem; +} + +.after\:bg-gray-200::after { + content: var(--tw-content); + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1)); +} + .focus-within\:outline-none:focus-within { outline: 2px solid transparent; outline-offset: 2px; diff --git a/views/order.html b/views/order.html index d8dba46..a288d70 100644 --- a/views/order.html +++ b/views/order.html @@ -1,9 +1,37 @@ {{ template "header.html" . }}
-
+

Order summary

+
+ Thanks for your order! As soon as your payment arrived we will print your Order. +
+ +
+

Order status: {{ .data.order.Status }}

+
+
+ Order Code: {{ .data.order.Token }} +
+
+
+ +
+

Payment information

+ +
+
+ Either you transfer money to our bank account, or you come by and pay in cash.

+ + Miteinander Dresden e.V.*
+ IBAN: DE66500310001076201001
+ BIC: TRODDEF1 (Triodos Bank)
+ Subject: {{ .data.order.Token }}
+ Amount: {{ .data.priceTotal }}€ +
+
+

Delivery information

@@ -23,8 +51,6 @@

Comment: {{ .data.order.Comment }}

- - Edit
@@ -51,7 +77,6 @@
-

Order summary

@@ -72,21 +97,10 @@
{{ .data.priceTotal }}€
- -
- - -
- -
- - - -
- +
{{ template "footer.html" . }}