diff --git a/src/checkoutservice/money.go b/src/checkoutservice/money.go
index 4777d1d..584b72e 100644
--- a/src/checkoutservice/money.go
+++ b/src/checkoutservice/money.go
@@ -9,7 +9,13 @@ import (
func sum(m1, m2 pb.MoneyAmount) pb.MoneyAmount {
f1, f2 := float64(m1.Fractional), float64(m2.Fractional)
lg1 := math.Max(1, math.Ceil(math.Log10(f1)))
+ if f1 == math.Pow(10, lg1) {
+ lg1++
+ }
lg2 := math.Max(1, math.Ceil(math.Log10(f2)))
+ if f2 == math.Pow(10, lg2) {
+ lg2++
+ }
lgMax := math.Max(lg1, lg2)
dSum := m1.Decimal + m2.Decimal
diff --git a/src/frontend/handlers.go b/src/frontend/handlers.go
index e0893f8..2170c1a 100644
--- a/src/frontend/handlers.go
+++ b/src/frontend/handlers.go
@@ -6,6 +6,7 @@ import (
"html/template"
"log"
"net/http"
+ "strconv"
"time"
"github.com/google/uuid"
@@ -43,7 +44,7 @@ func ensureSessionID(next http.HandlerFunc) http.HandlerFunc {
}
func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) {
- log.Printf("[home] session_id=%+v", sessionID(r))
+ log.Printf("[home] session_id=%+v currency=%s", sessionID(r), currentCurrency(r))
currencies, err := fe.getCurrencies(r.Context())
if err != nil {
http.Error(w, fmt.Sprintf("could not retrieve currencies: %+v", err), http.StatusInternalServerError)
@@ -100,12 +101,16 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request)
http.Error(w, fmt.Sprintf("could not retrieve product: %+v", err), http.StatusInternalServerError)
return
}
-
currencies, err := fe.getCurrencies(r.Context())
if err != nil {
http.Error(w, fmt.Sprintf("could not retrieve currencies: %+v", err), http.StatusInternalServerError)
return
}
+ cart, err := fe.getCart(r.Context(), sessionID(r))
+ if err != nil {
+ http.Error(w, fmt.Sprintf("could not retrieve cart: %+v", err), http.StatusInternalServerError)
+ return
+ }
price, err := fe.convertCurrency(r.Context(), &pb.Money{
Amount: p.GetPriceUsd(),
@@ -125,6 +130,92 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request)
"currencies": currencies,
"product": product,
"session_id": sessionID(r),
+ "cart_size": len(cart),
+ }); err != nil {
+ log.Println(err)
+ }
+}
+
+func (fe *frontendServer) addToCartHandler(w http.ResponseWriter, r *http.Request) {
+ quantity, _ := strconv.ParseUint(r.FormValue("quantity"), 10, 32)
+ productID := r.FormValue("product_id")
+ if productID == "" || quantity == 0 {
+ http.Error(w, "invalid form input", http.StatusBadRequest)
+ return
+ }
+ log.Printf("[addToCart] product_id=%s qty=%d session_id=%+v", productID, quantity, sessionID(r))
+
+ p, err := fe.getProduct(r.Context(), productID)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("could not retrieve product: %+v", err), http.StatusInternalServerError)
+ return
+ }
+
+ if err := fe.insertCart(r.Context(), sessionID(r), p.GetId(), int32(quantity)); err != nil {
+ http.Error(w, fmt.Sprintf("failed to add to cart: %+v", err), http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("location", "/cart")
+ w.WriteHeader(http.StatusFound)
+}
+
+func (fe *frontendServer) emptyCartHandler(w http.ResponseWriter, r *http.Request) {
+ log.Printf("[emptyCart] session_id=%+v", sessionID(r))
+
+ if err := fe.emptyCart(r.Context(), sessionID(r)); err != nil {
+ http.Error(w, fmt.Sprintf("failed to empty cart: %+v", err), http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("location", "/")
+ w.WriteHeader(http.StatusFound)
+}
+
+func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request) {
+ log.Printf("[viewCart] session_id=%+v", sessionID(r))
+ currencies, err := fe.getCurrencies(r.Context())
+ if err != nil {
+ http.Error(w, fmt.Sprintf("could not retrieve currencies: %+v", err), http.StatusInternalServerError)
+ return
+ }
+ cart, err := fe.getCart(r.Context(), sessionID(r))
+ if err != nil {
+ http.Error(w, fmt.Sprintf("could not retrieve cart: %+v", err), http.StatusInternalServerError)
+ return
+ }
+
+ type cartItemView struct {
+ Item *pb.Product
+ Quantity int32
+ Price *pb.Money
+ }
+ items := make([]cartItemView, len(cart))
+ for i, item := range cart {
+ p, err := fe.getProduct(r.Context(), item.GetProductId())
+ if err != nil {
+ http.Error(w, fmt.Sprintf("could not retrieve product #%s: %+v", item.GetProductId(), err), http.StatusInternalServerError)
+ return
+ }
+ price, err := fe.convertCurrency(r.Context(), &pb.Money{
+ Amount: p.GetPriceUsd(),
+ CurrencyCode: defaultCurrency}, currentCurrency(r))
+ if err != nil {
+ http.Error(w, fmt.Sprintf("could not convert currency for product #%s: %+v", item.GetProductId(), err), http.StatusInternalServerError)
+ return
+ }
+
+ multPrice := multMoney(*price.GetAmount(), uint32(item.GetQuantity()))
+ items[i] = cartItemView{
+ Item: p,
+ Quantity: item.GetQuantity(),
+ Price: &pb.Money{Amount: &multPrice, CurrencyCode: price.GetCurrencyCode()}}
+ }
+
+ if err := templates.ExecuteTemplate(w, "cart", map[string]interface{}{
+ "user_currency": currentCurrency(r),
+ "currencies": currencies,
+ "session_id": sessionID(r),
+ "cart_size": len(cart),
+ "items": items,
}); err != nil {
log.Println(err)
}
diff --git a/src/frontend/main.go b/src/frontend/main.go
index 6b3584d..cda4a22 100644
--- a/src/frontend/main.go
+++ b/src/frontend/main.go
@@ -69,9 +69,12 @@ func main() {
r := mux.NewRouter()
r.HandleFunc("/", ensureSessionID(svc.homeHandler)).Methods(http.MethodGet, http.MethodHead)
r.HandleFunc("/product/{id}", ensureSessionID(svc.productHandler)).Methods(http.MethodGet, http.MethodHead)
- r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
- r.HandleFunc("/logout", svc.logoutHandler).Methods(http.MethodGet)
+ r.HandleFunc("/cart", ensureSessionID(svc.viewCartHandler)).Methods(http.MethodGet, http.MethodHead)
+ r.HandleFunc("/cart", ensureSessionID(svc.addToCartHandler)).Methods(http.MethodPost)
+ r.HandleFunc("/cart/empty", ensureSessionID(svc.emptyCartHandler)).Methods(http.MethodPost)
r.HandleFunc("/setCurrency", ensureSessionID(svc.setCurrencyHandler)).Methods(http.MethodPost)
+ r.HandleFunc("/logout", svc.logoutHandler).Methods(http.MethodGet)
+ r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
log.Printf("starting server on :" + srvPort)
log.Fatal(http.ListenAndServe("localhost:"+srvPort, r))
}
diff --git a/src/frontend/money.go b/src/frontend/money.go
new file mode 100644
index 0000000..a97e1d3
--- /dev/null
+++ b/src/frontend/money.go
@@ -0,0 +1,50 @@
+package main
+
+import (
+ "math"
+
+ pb "frontend/genproto"
+)
+
+// TODO(ahmetb): any logic below is flawed because I just realized we have no
+// way of representing amounts like 17.07 because Fractional cannot store 07.
+func multMoney(m pb.MoneyAmount, n uint32) pb.MoneyAmount {
+ out := m
+ for n > 1 {
+ out = sum(out, m)
+ n--
+ }
+ return out
+}
+
+func sum(m1, m2 pb.MoneyAmount) pb.MoneyAmount {
+ // TODO(ahmetb) this is copied from ./checkoutservice/money.go, find a
+ // better mult function.
+ f1, f2 := float64(m1.Fractional), float64(m2.Fractional)
+ lg1 := math.Max(1, math.Ceil(math.Log10(f1)))
+ if f1 == math.Pow(10, lg1) {
+ lg1++
+ }
+ lg2 := math.Max(1, math.Ceil(math.Log10(f2)))
+ if f2 == math.Pow(10, lg2) {
+ lg2++
+ }
+ lgMax := math.Max(lg1, lg2)
+
+ dSum := m1.Decimal + m2.Decimal
+ o1 := f1 * math.Pow(10, lgMax-lg1)
+ o2 := f2 * math.Pow(10, lgMax-lg2)
+ fSum := o1 + o2
+ if fSum >= math.Pow(10, lgMax) {
+ fSum -= math.Pow(10, lgMax)
+ dSum++
+ }
+
+ for int(fSum)%10 == 0 && fSum != 0 {
+ fSum = float64(int(fSum) / 10)
+ }
+
+ return pb.MoneyAmount{
+ Decimal: dSum,
+ Fractional: uint32(fSum)}
+}
diff --git a/src/frontend/rpc.go b/src/frontend/rpc.go
index 54c32bf..f295506 100644
--- a/src/frontend/rpc.go
+++ b/src/frontend/rpc.go
@@ -50,6 +50,21 @@ func (fe *frontendServer) getCart(ctx context.Context, userID string) ([]*pb.Car
return resp.GetItems(), err
}
+func (fe *frontendServer) emptyCart(ctx context.Context, userID string) error {
+ _, err := pb.NewCartServiceClient(fe.cartSvcConn).EmptyCart(ctx, &pb.EmptyCartRequest{UserId: userID})
+ return err
+}
+
+func (fe *frontendServer) insertCart(ctx context.Context, userID, productID string, quantity int32) error {
+ _, err := pb.NewCartServiceClient(fe.cartSvcConn).AddItem(ctx, &pb.AddItemRequest{
+ UserId: userID,
+ Item: &pb.CartItem{
+ ProductId: productID,
+ Quantity: quantity},
+ })
+ return err
+}
+
func (fe *frontendServer) convertCurrency(ctx context.Context, money *pb.Money, currency string) (*pb.Money, error) {
if avoidNoopCurrencyConversionRPC && money.GetCurrencyCode() == currency {
return money, nil
diff --git a/src/frontend/templates/cart.html b/src/frontend/templates/cart.html
new file mode 100644
index 0000000..f20057c
--- /dev/null
+++ b/src/frontend/templates/cart.html
@@ -0,0 +1,53 @@
+{{ define "cart" }}
+ {{ template "header" . }}
+
+
+
+
+
Shopping Cart
+
+ {{ if eq (len $.items) 0 }}
+
Your shopping cart is empty.
+
Browse Products
+ {{ end }}
+
+ {{ range $.items }}
+
+
+
![]({{.Item.Picture}})
+
+
+ {{.Item.Name}}
+ SKU: #{{.Item.Id}}
+
+
+ Qty: {{.Quantity}}
+
+ {{.Price.CurrencyCode}}
+ {{.Price.Amount.Decimal}}.
+ {{- .Price.Amount.Fractional}}{{- if lt .Price.Amount.Fractional 10}}0{{end}}
+
+
+
+ {{ end }}
+
+ {{ if $.items }}
+
+ {{ end }}
+
+
+
+
+
+ {{ template "footer" . }}
+{{ end }}
diff --git a/src/frontend/templates/header.html b/src/frontend/templates/header.html
index 51ba64b..76bb700 100644
--- a/src/frontend/templates/header.html
+++ b/src/frontend/templates/header.html
@@ -23,7 +23,7 @@
{{end}}
- View Cart ({{$.cart_size}})
+ View Cart ({{$.cart_size}})
diff --git a/src/frontend/templates/product.html b/src/frontend/templates/product.html
index 558e2a2..a4dae1d 100644
--- a/src/frontend/templates/product.html
+++ b/src/frontend/templates/product.html
@@ -24,7 +24,7 @@
-